diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe534c954d..59950d427b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ name: Phoebus build -on: [push, pull_request] +on: + push: + branches-ignore: + - 'master' + pull_request: jobs: build: diff --git a/.github/workflows/build_latest.yml b/.github/workflows/build_latest.yml new file mode 100644 index 0000000000..1b5b2bceb5 --- /dev/null +++ b/.github/workflows/build_latest.yml @@ -0,0 +1,31 @@ +name: Phoebus build + +on: + push: + branches: + - master + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest", "macos-latest"] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Build + run: mvn --batch-mode install -DskipTests + + - name: Archive build artifacts + uses: actions/upload-artifact@v3 + with: + name: Phoebus product ${{ matrix.os }} + path: | + ${{ github.workspace }}/phoebus-product/target/*.tar.gz + ${{ github.workspace }}/phoebus-product/target/*.zip diff --git a/.gitignore b/.gitignore index 1380dc8c54..4fa8fe7d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,9 @@ phoebus-product/settings_template.ini # hs_err* files **/hs_err* + +# doc files generated by docs/source/conf.py +docs/source/applications.rst +docs/source/preference_properties.rst +docs/source/services.rst + diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..ea78cabcff --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.12" + +# Build from the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Explicitly set the version of Python and its requirements +python: + install: + - requirements: docs/source/requirements.txt diff --git a/README.md b/README.md index e54ea588bf..78025c81a6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://control-system-studio.readthedocs.io ## Requirements - [JDK11 or later, suggested is OpenJDK](http://jdk.java.net/12). - - [maven 2.x](https://maven.apache.org/) or [ant](http://ant.apache.org/) + - [maven 3.x](https://maven.apache.org/) or [ant](http://ant.apache.org/) ## Target Platform @@ -218,9 +218,12 @@ To build for a different platform, create the `dependencies` in one of these way # Either create the build platform for Linux.. ( cd phoebus; mvn clean verify -Djavafx.platform=linux -f dependencies/pom.xml ) - # or Mac OS X .. + # or Mac OS X (Intel) ( cd phoebus; mvn clean verify -Djavafx.platform=mac -f dependencies/pom.xml ) + # or Mac OS X (M2) + ( cd phoebus; mvn clean verify -Djavafx.platform=mac-aarch64 -f dependencies/pom.xml ) + # or Windows: ( cd phoebus; mvn clean verify -Djavafx.platform=win -f dependencies/pom.xml ) @@ -249,10 +252,10 @@ Create a sonatype account and update the maven settings.xml file with your sonat **Prepare the release** `mvn release:prepare` In this step will ensure there are no uncommitted changes, ensure the versions number are correct, tag the scm, etc. -A full list of checks is documented [here](https://maven.apache.org/maven-release/maven-release-plugin/examples/prepare-release.html). +A full list of checks is documented [here](https://maven.apache.org/maven-release/maven-release-plugin/usage/prepare-release.html). **Perform the release** -`mvn -Pdocs release:perform` +`mvn -Darguments="-Dskip-executable-jar" -Pdocs,releases release:perform` Checkout the release tag, build, sign and push the build binaries to sonatype. The `docs` profile is needed in order to create required javadocs jars. diff --git a/app/3d-viewer/pom.xml b/app/3d-viewer/pom.xml index a7a4bac40b..a1e766b402 100644 --- a/app/3d-viewer/pom.xml +++ b/app/3d-viewer/pom.xml @@ -3,19 +3,19 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-3d-viewer org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/app/alarm/datasource/doc/index.rst b/app/alarm/datasource/doc/index.rst index 93f20185b8..710e087bd6 100644 --- a/app/alarm/datasource/doc/index.rst +++ b/app/alarm/datasource/doc/index.rst @@ -10,7 +10,7 @@ to take a set of actions needed to effectively handle an alarm. The alarm datasource provides a subsection of the alarm information and functionality. This makes it possible for user to access beast alarm information of any other cs-studio application. -OPI screens can now embed informatino about from the alarm server, like the acknowledgement state of a pv, etc.. +OPI screens can now embed information about from the alarm server, like the acknowledgement state of a pv, etc.. PV syntax --------- diff --git a/app/alarm/datasource/pom.xml b/app/alarm/datasource/pom.xml index ca209aaa5b..f65e3b0b6e 100644 --- a/app/alarm/datasource/pom.xml +++ b/app/alarm/datasource/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-alarm - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-alarm-datasouce @@ -22,32 +22,32 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT net.sf.sociaal diff --git a/app/alarm/datasource/src/main/java/org/phoebus/pv/alarm/AlarmContext.java b/app/alarm/datasource/src/main/java/org/phoebus/pv/alarm/AlarmContext.java index c4b077d009..3c9547876c 100644 --- a/app/alarm/datasource/src/main/java/org/phoebus/pv/alarm/AlarmContext.java +++ b/app/alarm/datasource/src/main/java/org/phoebus/pv/alarm/AlarmContext.java @@ -116,11 +116,14 @@ public static synchronized void acknowledgePV(AlarmPV alarmPV, boolean ack) } if (node != null) { - try { - alarmModels.get(alarmPV.getInfo().getRoot()).acknowledge(node, ack); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to acknowledge alarm", e); - } + AlarmTreeItem alarmClientNode = node; + JobManager.schedule("Acknowledge/unacknowledge alarm", monitor -> { + try { + alarmModels.get(alarmPV.getInfo().getRoot()).acknowledge(alarmClientNode, ack); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to acknowledge alarm", e); + } + }); } } } @@ -153,8 +156,14 @@ public static synchronized void enablePV(AlarmPV alarmPV, boolean enable) for (AlarmClientLeaf pv : pvs) { final AlarmClientLeaf copy = pv.createDetachedCopy(); - if (copy.setEnabled(enable)) - alarmModels.get(alarmPV.getInfo().getRoot()).sendItemConfigurationUpdate(pv.getPathName(), copy); + if (copy.setEnabled(enable)){ + try { + alarmModels.get(alarmPV.getInfo().getRoot()).sendItemConfigurationUpdate(pv.getPathName(), copy); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to send item configuration update to " + alarmPV.getInfo().getRoot(), e); + throw e; + } + } } }); } diff --git a/app/alarm/logging-ui/pom.xml b/app/alarm/logging-ui/pom.xml index 129532d2dd..8b503e9519 100644 --- a/app/alarm/logging-ui/pom.xml +++ b/app/alarm/logging-ui/pom.xml @@ -3,39 +3,39 @@ org.phoebus app-alarm - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-alarm-logging-ui org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile diff --git a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogSearchJob.java b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogSearchJob.java index 271f00c54e..0367b3f36d 100644 --- a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogSearchJob.java +++ b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogSearchJob.java @@ -6,19 +6,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.jersey.api.client.WebResource; import javafx.collections.ObservableMap; +import org.phoebus.applications.alarm.logging.ui.AlarmLogTableQueryUtil.Keys; import org.phoebus.framework.jobs.Job; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.jobs.JobMonitor; import org.phoebus.framework.jobs.JobRunnable; import org.phoebus.framework.preferences.PreferencesReader; -import org.phoebus.applications.alarm.logging.ui.AlarmLogTableQueryUtil.Keys; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -26,12 +23,13 @@ import java.util.stream.Collectors; import static org.phoebus.applications.alarm.logging.ui.AlarmLogTableApp.logger; + /** * A Job to search for alarm messages logged by the alarm logging service + * * @author Kunal Shroff */ public class AlarmLogSearchJob implements JobRunnable { - private final Boolean isNodeTable; private final ObservableMap searchParameters; private final Consumer> alarmMessageHandler; private final BiConsumer errorHandler; @@ -39,8 +37,6 @@ public class AlarmLogSearchJob implements JobRunnable { private final ObjectMapper objectMapper; private final WebResource client; - private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") - .withZone(ZoneId.of("UTC")); private final PreferencesReader prefs = new PreferencesReader(AlarmLogTableApp.class, "/alarm_logging_preferences.properties"); @@ -58,7 +54,6 @@ private AlarmLogSearchJob(WebResource client, Boolean isNodeTable, ObservableMap Consumer> alarmMessageHandler, BiConsumer errorHandler) { super(); this.client = client; - this.isNodeTable = isNodeTable; this.searchParameters = searchParameters; this.alarmMessageHandler = alarmMessageHandler; this.errorHandler = errorHandler; @@ -68,31 +63,30 @@ private AlarmLogSearchJob(WebResource client, Boolean isNodeTable, ObservableMap @Override public void run(JobMonitor monitor) { - monitor.beginTask("searching for alarm log entires : " + - searchParameters.entrySet().stream().map(e-> e.getKey() + ":" + e.getValue()).collect(Collectors.joining())); + String searchString = "searching for alarm log entries : " + + searchParameters.entrySet().stream().filter(e -> !e.getValue().equals("")).map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining(",")); + logger.log(Level.INFO, "Searching for alarm entries: " + searchString); + monitor.beginTask(searchString); int size = prefs.getInt("results_max_size"); MultivaluedMap map = new MultivaluedHashMap<>(); - searchParameters.entrySet().forEach(e -> { - map.add(e.getKey().getName(), e.getValue()); - } - ); - map.putIfAbsent("size", Arrays.asList(String.valueOf(size))); + searchParameters.forEach((key, value) -> { if (!value.equals("")) map.add(key.getName(), value); }); + map.putIfAbsent("size", List.of(String.valueOf(size))); try { long start = System.currentTimeMillis(); - String resultStr = client.path("/search/alarm") + String resultStr = client.path("/search/alarm") .queryParams(map) .accept(MediaType.APPLICATION_JSON).get(String.class); - logger.log(Level.FINE,"String response = " + (System.currentTimeMillis() - start)); + logger.log(Level.FINE, "String response = " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); - List result = objectMapper.readValue(resultStr, new TypeReference>() { + List result = objectMapper.readValue(resultStr, new TypeReference<>() { }); - logger.log(Level.FINE,"Object mapper response = " + (System.currentTimeMillis() - start)); + logger.log(Level.FINE, "Object mapper response = " + (System.currentTimeMillis() - start)); alarmMessageHandler.accept(result); - } catch (JsonProcessingException e) { + } catch (Exception e) { errorHandler.accept("Failed to search for alarm logs ", e); } } diff --git a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableApp.java b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableApp.java index 700c98219c..2824d097cd 100644 --- a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableApp.java +++ b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableApp.java @@ -44,7 +44,7 @@ public AppInstance create() { } /** - * Support the launching of alarmLogtable using resource alarmLog://? + * Support the launching of alarmLogtable using resource {@literal alarmLog://?} * e.g. * -resource alarmLog://?pv=SR* */ diff --git a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableController.java b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableController.java index 54bd026729..21abe8c271 100644 --- a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableController.java +++ b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/AlarmLogTableController.java @@ -10,29 +10,33 @@ import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.fxml.FXML; +import javafx.geometry.Side; import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.SelectionMode; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; -import javafx.scene.control.TableColumn.CellDataFeatures; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; -import javafx.scene.paint.Color; -import javafx.util.Callback; import javafx.util.Duration; +import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.logging.ui.AlarmLogTableQueryUtil.Keys; import org.phoebus.applications.alarm.model.SeverityLevel; import org.phoebus.applications.alarm.ui.AlarmUI; @@ -40,17 +44,20 @@ import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.javafx.JFXUtil; import org.phoebus.util.time.TimeParser; import org.phoebus.util.time.TimestampFormats; import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; -import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -66,6 +73,7 @@ public class AlarmLogTableController { @FXML TableView tableView; @FXML + @SuppressWarnings("unused") private AdvancedSearchViewController advancedSearchViewController; @FXML TableColumn configCol; @@ -104,15 +112,15 @@ public class AlarmLogTableController { @FXML GridPane ViewSearchPane; @FXML - TextField searchText; + private TextField configSelection; @FXML - TextField startTime; + private ToggleButton configDropdownButton; + @FXML - TextField endTime; - TableColumn sortTableCol = null; + TableColumn sortTableCol = null; SortType sortColType = null; // Search parameters - ObservableMap searchParameters = FXCollections.observableHashMap(); + ObservableMap searchParameters = FXCollections.observableHashMap(); // The search string // TODO need to standardize the search string so that it can be easily parsed private String searchString = "*"; @@ -122,10 +130,15 @@ public class AlarmLogTableController { private Job alarmLogSearchJob; private WebResource searchClient; - + @FXML private ProgressIndicator progressIndicator; - private SimpleBooleanProperty searchInProgress = new SimpleBooleanProperty(false); + private final SimpleBooleanProperty searchInProgress = new SimpleBooleanProperty(false); + private final ObservableList alarmConfigs = FXCollections.observableArrayList(); + private final SimpleStringProperty selectedConfigsString = new SimpleStringProperty(); + + + private final ContextMenu configsContextMenu = new ContextMenu(); public AlarmLogTableController(WebResource client) { setClient(client); @@ -137,45 +150,28 @@ public void initialize() { tableView.getColumns().clear(); configCol = new TableColumn<>("Config"); configCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getConfig()); - } - }); + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getConfig())); tableView.getColumns().add(configCol); pvCol = new TableColumn<>("PV"); - pvCol.setCellValueFactory(new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getPv()); - } - }); + pvCol.setCellValueFactory(alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getPv())); tableView.getColumns().add(pvCol); severityCol = new TableColumn<>("Severity"); severityCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getSeverity()); - } - }); - severityCol.setCellFactory(alarmLogTableTypeStringTableColumn -> new TableCell() { + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getSeverity())); + severityCol.setCellFactory(alarmLogTableTypeStringTableColumn -> new TableCell<>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) - { + if (empty || item == null) { + setStyle("-fx-text-fill: black; -fx-background-color: transparent"); setText(""); - setTextFill(Color.BLACK); - } - else - { + } else { setText(item); - setTextFill(AlarmUI.getColor(parseSeverityLevel(item))); + SeverityLevel severityLevel = parseSeverityLevel(item); + setStyle("-fx-alignment: center; -fx-border-color: transparent; -fx-border-width: 2 0 2 0; -fx-background-insets: 2 0 2 0; -fx-text-fill: " + JFXUtil.webRGB(AlarmUI.getColor(severityLevel)) + "; -fx-background-color: " + JFXUtil.webRGB(AlarmUI.getBackgroundColor(severityLevel))); } } }); @@ -183,36 +179,25 @@ protected void updateItem(String item, boolean empty) { messageCol = new TableColumn<>("Message"); messageCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getMessage()); - } - }); + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getMessage())); tableView.getColumns().add(messageCol); timeCol = new TableColumn<>("Time"); timeCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - if (alarmMessage.getValue().getTime() != null) { - String time = TimestampFormats.MILLI_FORMAT.format(alarmMessage.getValue().getTime()); - return new SimpleStringProperty(time); - } - return null; + alarmMessage -> { + if (alarmMessage.getValue().getTime() != null) { + String time = TimestampFormats.MILLI_FORMAT.format(alarmMessage.getValue().getTime()); + return new SimpleStringProperty(time); } + return null; }); tableView.getColumns().add(timeCol); msgTimeCol = new TableColumn<>("Message Time"); msgTimeCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - String time = TimestampFormats.MILLI_FORMAT.format(alarmMessage.getValue().getMessage_time()); - return new SimpleStringProperty(time); - } + alarmMessage -> { + String time = TimestampFormats.MILLI_FORMAT.format(alarmMessage.getValue().getMessage_time()); + return new SimpleStringProperty(time); }); tableView.getColumns().add(msgTimeCol); @@ -228,26 +213,20 @@ public ObservableValue call(CellDataFeatures currentSeverityCol = new TableColumn<>("Current Severity"); currentSeverityCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getCurrent_severity()); - } - }); - currentSeverityCol.setCellFactory(alarmLogTableTypeStringTableColumn -> new TableCell() { + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getCurrent_severity())); + currentSeverityCol.setCellFactory(alarmLogTableTypeStringTableColumn -> new TableCell<>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) - { + if (empty || item == null) { + setStyle("-fx-text-fill: black; -fx-background-color: transparent"); setText(""); - setTextFill(Color.BLACK); - } - else - { + } else { + setText(item); - setTextFill(AlarmUI.getColor(parseSeverityLevel(item))); + SeverityLevel severityLevel = parseSeverityLevel(item); + setStyle("-fx-alignment: center; -fx-border-color: transparent; -fx-border-width: 2 0 2 0; -fx-background-insets: 2 0 2 0; -fx-text-fill: " + JFXUtil.webRGB(AlarmUI.getColor(severityLevel)) + "; -fx-background-color: " + JFXUtil.webRGB(AlarmUI.getBackgroundColor(severityLevel))); } } }); @@ -255,54 +234,36 @@ protected void updateItem(String item, boolean empty) { currentMessageCol = new TableColumn<>("Current Message"); currentMessageCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getCurrent_message()); - } - }); + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getCurrent_message())); tableView.getColumns().add(currentMessageCol); commandCol = new TableColumn<>("Command"); commandCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - String action = alarmMessage.getValue().getCommand(); - if (action != null) { - return new SimpleStringProperty(action); - } - boolean en = alarmMessage.getValue().isEnabled(); - if (alarmMessage.getValue().getUser() != null && alarmMessage.getValue().getHost() != null) { - if (en == false) { - return new SimpleStringProperty("Disabled"); - } else if (en == true) { - return new SimpleStringProperty("Enabled"); - } + alarmMessage -> { + String action = alarmMessage.getValue().getCommand(); + if (action != null) { + return new SimpleStringProperty(action); + } + boolean en = alarmMessage.getValue().isEnabled(); + if (alarmMessage.getValue().getUser() != null && alarmMessage.getValue().getHost() != null) { + if (!en) { + return new SimpleStringProperty("Disabled"); + } else { + return new SimpleStringProperty("Enabled"); } - return null; } + return null; }); tableView.getColumns().add(commandCol); userCol = new TableColumn<>("User"); userCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getUser()); - } - }); + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getUser())); tableView.getColumns().add(userCol); hostCol = new TableColumn<>("Host"); hostCol.setCellValueFactory( - new Callback, ObservableValue>() { - @Override - public ObservableValue call(CellDataFeatures alarmMessage) { - return new SimpleStringProperty(alarmMessage.getValue().getHost()); - } - }); + alarmMessage -> new SimpleStringProperty(alarmMessage.getValue().getHost())); tableView.getColumns().add(hostCol); tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); @@ -321,15 +282,19 @@ public ObservableValue call(CellDataFeatures query.setText( searchParameters.entrySet().stream() + .filter(e -> !e.getKey().getName().equals(Keys.ROOT.getName())) // Exclude alarm config (root) as selection is managed in drop-down .sorted(Map.Entry.comparingByKey()) .map((e) -> e.getKey().getName().trim() + "=" + e.getValue().trim()) .collect(Collectors.joining("&"))); searchParameters.addListener( - (MapChangeListener) change -> query.setText(searchParameters.entrySet().stream() - .sorted(Entry.comparingByKey()) - .map((e) -> e.getKey().getName().trim() + "=" + e.getValue().trim()) - .collect(Collectors.joining("&")))); + (MapChangeListener) change -> + query.setText(searchParameters.entrySet().stream() + .sorted(Entry.comparingByKey()) + .filter(e -> !e.getKey().getName().equals(Keys.ROOT.getName())) // Exclude alarm config (root) as selection is managed in drop-down + .filter(e -> !e.getValue().equals("")) + .map((e) -> e.getKey().getName().trim() + "=" + e.getValue().trim()) + .collect(Collectors.joining("&")))); query.setOnKeyPressed(keyEvent -> { if (keyEvent.getCode() == KeyCode.ENTER) { @@ -337,17 +302,41 @@ public ObservableValue call(CellDataFeatures } }); - progressIndicator.visibleProperty().bind(searchInProgress); - searchInProgress.addListener((observable, oldValue, newValue) -> { - tableView.setDisable(newValue.booleanValue()); - }); + progressIndicator.visibleProperty().bind(searchInProgress); + searchInProgress.addListener((observable, oldValue, newValue) -> tableView.setDisable(newValue)); search.disableProperty().bind(searchInProgress); + String[] configNames = AlarmSystem.config_names; + for (String configName : configNames) { + AlarmConfiguration alarmConfiguration = new AlarmConfiguration(configName, false); + alarmConfigs.add(alarmConfiguration); + CheckBox checkBox = new CheckBox(configName); + CustomMenuItem configItem = new CustomMenuItem(checkBox); + configItem.setHideOnClick(false); + checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> { + alarmConfiguration.setSelected(newValue); + setSelectedConfigsString(); + search(); + }); + configsContextMenu.getItems().add(configItem); + } + + Image downIcon = ImageCache.getImage(AlarmLogTableController.class, "/icons/down_triangle.png"); + configDropdownButton.setGraphic(new ImageView(downIcon)); + configDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> + { + if (!newVal && !configsContextMenu.isShowing() && !configsContextMenu.isShowing()) + configDropdownButton.setSelected(false); + }); + + configSelection.textProperty().bind(selectedConfigsString); + + setSelectedConfigsString(); periodicSearch(); } - private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture runningTask; private void periodicSearch() { @@ -362,14 +351,14 @@ private void periodicSearch() { sortTableCol = null; sortColType = null; if (!tableView.getSortOrder().isEmpty()) { - sortTableCol = (TableColumn) tableView.getSortOrder().get(0); + sortTableCol = tableView.getSortOrder().get(0); sortColType = sortTableCol.getSortType(); } alarmLogSearchJob = AlarmLogSearchJob.submit(searchClient, searchString, isNodeTable, searchParameters, result -> Platform.runLater(() -> { setAlarmMessages(result); searchInProgress.set(false); - }), (url, ex) -> { + }), (url, ex) -> { searchInProgress.set(false); logger.log(Level.WARNING, "Shutting down alarm log message scheduler.", ex); runningTask.cancel(true); @@ -383,7 +372,7 @@ public void setSearchString(String searchString) { public void setIsNodeTable(Boolean isNodeTable) { this.isNodeTable = isNodeTable; - if (isNodeTable == false) { + if (!isNodeTable) { searchParameters.put(Keys.PV, this.searchString); } else { searchParameters.put(Keys.PV, "*"); @@ -399,13 +388,7 @@ public void setIsNodeTable(Boolean isNodeTable) { searchParameters.put(Keys.STARTTIME, TimeParser.format(java.time.Duration.ofDays(7))); searchParameters.put(Keys.ENDTIME, TimeParser.format(java.time.Duration.ZERO)); - query.setText(searchParameters.entrySet().stream().sorted(Map.Entry.comparingByKey()).map((e) -> { - return e.getKey().getName().trim() + "=" + e.getValue().trim(); - }).collect(Collectors.joining("&"))); - } - - public List getAlarmMessages() { - return alarmMessages; + query.setText(searchParameters.entrySet().stream().sorted(Map.Entry.comparingByKey()).map((e) -> e.getKey().getName().trim() + "=" + e.getValue().trim()).collect(Collectors.joining("&"))); } public void setAlarmMessages(List alarmMessages) { @@ -425,7 +408,8 @@ public void setClient(WebResource client) { /** * A Helper method which returns the appropriate {@link SeverityLevel} matching the * string level - * @param level + * + * @param level Severity level */ private static SeverityLevel parseSeverityLevel(String level) { switch (level.toUpperCase()) { @@ -454,7 +438,7 @@ private static SeverityLevel parseSeverityLevel(String level) { // Keeps track of when the animation is active. Multiple clicks will be ignored // until a give resize action is completed - private AtomicBoolean moving = new AtomicBoolean(false); + private final AtomicBoolean moving = new AtomicBoolean(false); @FXML public void resize() { @@ -484,21 +468,35 @@ public void resize() { } } - Map lookup = Arrays.stream(Keys.values()).collect(Collectors.toMap(Keys::getName, k -> { - return k; - })); + Map lookup = + Arrays.stream(Keys.values()).filter(k -> !k.getName().equals(Keys.ROOT.getName())).collect(Collectors.toMap(Keys::getName, k -> k)); @FXML void updateQuery() { List searchTerms = Arrays.asList(query.getText().split("&")); - searchTerms.stream().forEach(s -> { - String key = s.split("=")[0]; - String value = s.split("=")[1]; - if(lookup.containsKey(key)) { - searchParameters.put(lookup.get(key), value); + Set searchKeywords = new TreeSet<>(); + searchTerms.forEach(s -> { + String[] splitString = s.split("="); + if (splitString.length > 1) { + String key = splitString[0]; + searchKeywords.add(key); + String value = splitString[1]; + if (lookup.containsKey(key)) { + searchParameters.put(lookup.get(key), value); + } } }); + for (Keys key : searchParameters.keySet()) { + if (!searchKeywords.contains(key.toString())) { + searchParameters.put(key, ""); + } + } + + // Add root (alarm config) separately as it is selected differently by user, + // i.e. from drop-down rather than typing into the text field. + searchParameters.put(Keys.ROOT, configSelection.getText()); + } @FXML @@ -506,9 +504,12 @@ public void search() { searchInProgress.set(true); tableView.getSortOrder().clear(); updateQuery(); + alarmLogSearchJob.cancel(); + periodicSearch(); } private static final ObjectMapper objectMapper = new ObjectMapper(); + static { objectMapper.registerModule(new JavaTimeModule()); } @@ -517,7 +518,7 @@ public void search() { public void createContextMenu() { final ContextMenu contextMenu = new ContextMenu(); MenuItem configurationInfo = new MenuItem(Messages.ConfigurationInfo); - configurationInfo.setOnAction( actionEvent -> { + configurationInfo.setOnAction(actionEvent -> { List configs = tableView.getSelectionModel().getSelectedItems() .stream().map(e -> { try { @@ -544,13 +545,13 @@ public void createContextMenu() { try { String newLine = System.lineSeparator(); StringBuilder sb = new StringBuilder(); - sb.append("message_time: " + TimestampFormats.MILLI_FORMAT.format(result.getMessage_time()) + newLine); - sb.append("config: " + result.getConfig() + newLine); - sb.append("user: " + result.getUser() + newLine); - sb.append("host: " + result.getHost() + newLine); - sb.append("enabled: " + result.isEnabled() + newLine); + sb.append("message_time: ").append(TimestampFormats.MILLI_FORMAT.format(result.getMessage_time())).append(newLine); + sb.append("config: ").append(result.getConfig()).append(newLine); + sb.append("user: ").append(result.getUser()).append(newLine); + sb.append("host: ").append(result.getHost()).append(newLine); + sb.append("enabled: ").append(result.isEnabled()).append(newLine); Object jsonObject = objectMapper.readValue(result.getConfig_msg(), Object.class); - sb.append("config_msg: " + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject) + newLine); + sb.append("config_msg: ").append(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject)).append(newLine); alarmInfo.setContentText(sb.toString()); } catch (JsonProcessingException e) { alarmInfo.setContentText(Messages.ConfigurationInfoNotFound); @@ -558,7 +559,7 @@ public void createContextMenu() { } alarmInfo.show(); }), - (url, ex) -> ExceptionDetailsErrorDialog.openError("Alarm Log Info Error", ex.getMessage(), ex) + (url, ex) -> ExceptionDetailsErrorDialog.openError("Alarm Log Info Error", ex) ); }); @@ -566,7 +567,7 @@ public void createContextMenu() { contextMenu.getItems().add(new SeparatorMenuItem()); - // search for other context menu actions registered for AlarmLogTableType + // search for other context menu actions registered for AlarmLogTableType SelectionService.getInstance().setSelection("AlarmLogTable", tableView.getSelectionModel().getSelectedItems()); ContextMenuHelper.addSupportedEntries(tableView, contextMenu); @@ -580,4 +581,45 @@ public void shutdown() { } } + private static class AlarmConfiguration { + private final String configurationName; + private boolean selected; + + public AlarmConfiguration(String configurationName, boolean selected) { + this.configurationName = configurationName; + this.selected = selected; + } + + public String getConfigurationName() { + return configurationName; + } + + public boolean isSelected() { + return selected; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + } + + private void setSelectedConfigsString() { + List selectedConfigs = + alarmConfigs.stream().filter(AlarmConfiguration::isSelected).collect(Collectors.toList()); + if (selectedConfigs.size() == alarmConfigs.size() || selectedConfigs.size() == 0) { + selectedConfigsString.set("*"); + } else { + selectedConfigsString.set(alarmConfigs.stream().filter(AlarmConfiguration::isSelected) + .map(AlarmConfiguration::getConfigurationName).collect(Collectors.joining(","))); + } + } + + @FXML + public void selectConfigs() { + if (configDropdownButton.isSelected()) { + configsContextMenu.show(configSelection, Side.BOTTOM, 0, 0); + } else { + configsContextMenu.hide(); + } + } } diff --git a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/Messages.java b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/Messages.java index 23f7cc1bcb..b26abd2431 100644 --- a/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/Messages.java +++ b/app/alarm/logging-ui/src/main/java/org/phoebus/applications/alarm/logging/ui/Messages.java @@ -21,7 +21,6 @@ import org.phoebus.framework.nls.NLS; public class Messages { - public static String AlarmInformation; public static String ConfigurationInfo; public static String ConfigurationInfoNotFound; diff --git a/app/alarm/logging-ui/src/main/resources/alarm_logging_preferences.properties b/app/alarm/logging-ui/src/main/resources/alarm_logging_preferences.properties index 64fd169c5b..153c87f406 100644 --- a/app/alarm/logging-ui/src/main/resources/alarm_logging_preferences.properties +++ b/app/alarm/logging-ui/src/main/resources/alarm_logging_preferences.properties @@ -2,5 +2,7 @@ # Package org.phoebus.applications.alarm.logging.ui # ------------------------------------------------- +# The URL of the REST API exposed by the alarm logger service (not the elasticsearch port as it was prior to Phoebus 4.0) service_uri = http://localhost:9000 + results_max_size = 10000 diff --git a/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.fxml b/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.fxml index a58c0719c8..ed62f43390 100644 --- a/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.fxml +++ b/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/AlarmLogTable.fxml @@ -1,83 +1,96 @@ - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/messages.properties b/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/messages.properties index 689034bc40..b16ed39c8a 100644 --- a/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/messages.properties +++ b/app/alarm/logging-ui/src/main/resources/org/phoebus/applications/alarm/logging/ui/messages.properties @@ -20,6 +20,7 @@ Command=Command Config=Config ConfigurationInfo=Configuration Info ConfigurationInfoNotFound=Configuration info not found. +Configurations=Configurations: CurrentMessage=Current Message CurrentSeverity=Current Severity EndTime=End Time @@ -30,6 +31,7 @@ Mode=Mode NoSearchResults=No Search Results Query=Query: Search=Search +Configuration=Configuration(s): Severity=Severity StartTime=Start Time Time=Time diff --git a/app/alarm/model/pom.xml b/app/alarm/model/pom.xml index 6ae1a39282..d117bd7883 100644 --- a/app/alarm/model/pom.xml +++ b/app/alarm/model/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-alarm - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-alarm-model @@ -47,12 +47,12 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/AlarmSystem.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/AlarmSystem.java index 2d816cb08b..07d64c9706 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/AlarmSystem.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/AlarmSystem.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2022 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -48,6 +48,9 @@ public class AlarmSystem extends AlarmSystemConstants /** Timeout in seconds for initial PV connection */ @Preference public static int connection_timeout; + /** Timeout in seconds for "sevrpv:" updates */ + @Preference public static int severity_pv_timeout; + /** Item level of alarm area. A level of 2 would show all the root levels children. */ @Preference public static int alarm_area_level; @@ -115,6 +118,8 @@ public class AlarmSystem extends AlarmSystemConstants /** "Disable until.." shortcuts */ @Preference public static String[] shelving_options; + @Preference public static int max_block_ms; + /** Macros used in UI display/command/web links */ public static MacroValueProvider macros; diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/ResettableTimeout.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/ResettableTimeout.java index c0cdf70a60..aca0d22074 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/ResettableTimeout.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/ResettableTimeout.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -24,26 +24,37 @@ * Eventually, if there are no more resets, * it will time out. * + * Timeout can be adjusted on each 'reset' + * * @author Kay Kasemir */ @SuppressWarnings("nls") public class ResettableTimeout { - private final long timeout_secs; + private long timeout_secs; - private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("ResettableTimeout")); + private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("ResettableTimeout")); private final CountDownLatch no_more_messages = new CountDownLatch(1); private final Runnable signal_no_more_messages = () -> no_more_messages.countDown(); private final AtomicReference> timeout = new AtomicReference<>(); /** @param timeout_secs Seconds after which we time out */ public ResettableTimeout(final long timeout_secs) - { - this.timeout_secs = timeout_secs; - reset(); - } + { + this.timeout_secs = timeout_secs; + reset(); + } + + /** Reset the timer. As long as this is called within the timeout, we keep running + * @param timeout_secs New timeout in seconds + */ + public void reset(final long timeout_secs) + { + this.timeout_secs = timeout_secs; + reset(); + } - /** Reset the timer. As long as this is called within the timeout, we keep running */ + /** Reset the timer. As long as this is called within the timeout, we keep running */ public void reset() { final ScheduledFuture previous = timeout.getAndSet(timer.schedule(signal_no_more_messages, timeout_secs, TimeUnit.SECONDS)); diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java index 44ca4c0426..80d60fa470 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java @@ -7,18 +7,6 @@ *******************************************************************************/ package org.phoebus.applications.alarm.client; -import static org.phoebus.applications.alarm.AlarmSystem.logger; - -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; - import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; @@ -33,71 +21,111 @@ import org.phoebus.applications.alarm.model.json.JsonTags; import org.phoebus.util.time.TimestampFormats; -/** Alarm client model +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +import static org.phoebus.applications.alarm.AlarmSystem.logger; + +/** + * Alarm client model * - *

Given an alarm configuration name like "Accelerator", - * subscribes to the "Accelerator" topic for configuration updates - * and the "AcceleratorState" topic for alarm state updates. + *

Given an alarm configuration name like "Accelerator", + * subscribes to the "Accelerator" topic for configuration updates + * and the "AcceleratorState" topic for alarm state updates. * - *

Updates from either topic are merged into an in-memory model - * of the complete alarm information, - * updating listeners with all changes. + *

Updates from either topic are merged into an in-memory model + * of the complete alarm information, + * updating listeners with all changes. * - * @author Kay Kasemir + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class AlarmClient -{ - /** Kafka topics for config/status and commands */ +public class AlarmClient { + /** + * Kafka topics for config/status and commands + */ private final String config_topic, command_topic; - /** Listeners to this client */ + /** + * Listeners to this client + */ private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); - /** Alarm tree root */ + /** + * Alarm tree root + */ private final AlarmClientNode root; - /** Alarm tree Paths that have been deleted. + /** + * Timeout in seconds waiting for response from Kafka when sending producer messages. + */ + private static final int KAFKA_CLIENT_TIMEOUT = 10; + + /** + * Alarm tree Paths that have been deleted. * - *

Used to distinguish between paths that are not in the alarm tree - * because we have never seen a config or status update for them, - * and entries that have been deleted, so further state updates - * should be ignored until the item is again added (config message). + *

Used to distinguish between paths that are not in the alarm tree + * because we have never seen a config or status update for them, + * and entries that have been deleted, so further state updates + * should be ignored until the item is again added (config message). */ private final Set deleted_paths = ConcurrentHashMap.newKeySet(); - /** Flag for message handling thread to run or exit */ + /** + * Flag for message handling thread to run or exit + */ private final AtomicBoolean running = new AtomicBoolean(true); - /** Currently in maintenance mode? */ + /** + * Currently in maintenance mode? + */ private final AtomicBoolean maintenance_mode = new AtomicBoolean(false); - /** Currently in silent mode? */ + /** + * Currently in silent mode? + */ private final AtomicBoolean disable_notify = new AtomicBoolean(false); - /** Kafka consumer */ + /** + * Kafka consumer + */ private final Consumer consumer; - /** Kafka producer */ + /** + * Kafka producer + */ private final Producer producer; - /** Message handling thread */ + /** + * Message handling thread + */ private final Thread thread; - /** Time of last state update (ms), - * used to determine timeout + /** + * Time of last state update (ms), + * used to determine timeout */ private long last_state_update = 0; - /** Timeout, not seen any messages from server? */ + /** + * Timeout, not seen any messages from server? + */ private volatile boolean has_timed_out = false; - /** @param server Kafka Server host:port - * @param config_name Name of alarm tree root - * @param kafka_properties_file File to load additional kafka properties from + /** + * @param server Kafka Server host:port + * @param config_name Name of alarm tree root + * @param kafka_properties_file File to load additional kafka properties from */ - public AlarmClient(final String server, final String config_name, final String kafka_properties_file) - { + public AlarmClient(final String server, final String config_name, final String kafka_properties_file) { Objects.requireNonNull(server); Objects.requireNonNull(config_name); @@ -113,116 +141,125 @@ public AlarmClient(final String server, final String config_name, final String k thread.setDaemon(true); } - /** @param listener Listener to add */ - public void addListener(final AlarmClientListener listener) - { + /** + * @param listener Listener to add + */ + public void addListener(final AlarmClientListener listener) { listeners.add(listener); } - /** @param listener Listener to remove */ - public void removeListener(final AlarmClientListener listener) - { - if (! listeners.remove(listener)) + /** + * @param listener Listener to remove + */ + public void removeListener(final AlarmClientListener listener) { + if (!listeners.remove(listener)) throw new IllegalStateException("Unknown listener"); } - /** Start client - * @see #shutdown() + /** + * Start client + * + * @see #shutdown() */ - public void start() - { + public void start() { thread.start(); } - /** @return true if start() had been called */ - public boolean isRunning() - { + /** + * @return true if start() had been called + */ + public boolean isRunning() { return thread.isAlive(); } - /** @return Root of alarm configuration */ - public AlarmClientNode getRoot() - { + /** + * @return Root of alarm configuration + */ + public AlarmClientNode getRoot() { return root; } - /** @return Is alarm server in maintenance mode? */ - public boolean isMaintenanceMode() - { + /** + * @return Is alarm server in maintenance mode? + */ + public boolean isMaintenanceMode() { return maintenance_mode.get(); } - /** @return Is alarm server in disable notify mode? */ - public boolean isDisableNotify() - { + /** + * @return Is alarm server in disable notify mode? + */ + public boolean isDisableNotify() { return disable_notify.get(); } - /** @param maintenance Select maintenance mode? */ - public void setMode(final boolean maintenance) - { + /** + * Client code must not call this on the UI thread as it may block up to {@link #KAFKA_CLIENT_TIMEOUT} seconds. + * + * @param maintenance Select maintenance mode? + * @throws Exception if Kafka interaction fails for any reason. + */ + public void setMode(final boolean maintenance) throws Exception { final String cmd = maintenance ? JsonTags.MAINTENANCE : JsonTags.NORMAL; - try - { - final String json = new String (JsonModelWriter.commandToBytes(cmd)); + try { + final String json = new String(JsonModelWriter.commandToBytes(cmd)); final ProducerRecord record = new ProducerRecord<>(command_topic, AlarmSystem.COMMAND_PREFIX + root.getPathName(), json); - producer.send(record); - } - catch (final Exception ex) - { + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); + } catch (final Exception ex) { logger.log(Level.WARNING, "Cannot set mode for " + root + " to " + cmd, ex); + throw ex; } } - /** @param disable_notify Select notify disable ? */ - public void setNotify(final boolean disable_notify) - { + /** + * Client must not call this on the UI thread as it may block up to {@link #KAFKA_CLIENT_TIMEOUT} seconds. + * + * @param disable_notify Select notify disable ? + * @throws Exception if Kafka interaction fails for any reason. + */ + public void setNotify(final boolean disable_notify) throws Exception { final String cmd = disable_notify ? JsonTags.DISABLE_NOTIFY : JsonTags.ENABLE_NOTIFY; - try - { - final String json = new String (JsonModelWriter.commandToBytes(cmd)); + try { + final String json = new String(JsonModelWriter.commandToBytes(cmd)); final ProducerRecord record = new ProducerRecord<>(command_topic, AlarmSystem.COMMAND_PREFIX + root.getPathName(), json); - producer.send(record); - } - catch (final Exception ex) - { + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); + } catch (final Exception ex) { logger.log(Level.WARNING, "Cannot set mode for " + root + " to " + cmd, ex); + throw ex; } } - /** Background thread loop that checks for alarm tree updates */ - private void run() - { + /** + * Background thread loop that checks for alarm tree updates + */ + private void run() { // Send an initial "no server" notification, // to be cleared once we receive data from server. checkServerState(); - try - { - while (running.get()) - { + try { + while (running.get()) { checkUpdates(); checkServerState(); } - } - catch (final Throwable ex) - { + } catch (final Throwable ex) { if (running.get()) logger.log(Level.SEVERE, "Alarm client model error", ex); // else: Intended shutdown - } - finally - { + } finally { consumer.close(); producer.close(); } } - /** Time spent in checkUpdates() waiting for, well, updates */ + /** + * Time spent in checkUpdates() waiting for, well, updates + */ private static final Duration POLL_PERIOD = Duration.ofMillis(100); - /** Perform one check for updates */ - private void checkUpdates() - { + /** + * Perform one check for updates + */ + private void checkUpdates() { // Check for messages, with timeout. // TODO Because of Kafka bug, this will hang if Kafka isn't running. // Fixed according to https://issues.apache.org/jira/browse/KAFKA-1894 , @@ -232,20 +269,20 @@ private void checkUpdates() handleUpdate(record); } - /** Handle one received update - * @param record Kafka record + /** + * Handle one received update + * + * @param record Kafka record */ - private void handleUpdate(final ConsumerRecord record) - { + private void handleUpdate(final ConsumerRecord record) { final int sep = record.key().indexOf(':'); - if (sep < 0) - { + if (sep < 0) { logger.log(Level.WARNING, "Invalid key, expecting type:path, got " + record.key()); return; } - final String type = record.key().substring(0, sep+1); - final String path = record.key().substring(sep+1); + final String type = record.key().substring(0, sep + 1); + final String path = record.key().substring(sep + 1); final long timestamp = record.timestamp(); final String node_config = record.value(); @@ -253,34 +290,27 @@ private void handleUpdate(final ConsumerRecord record) logger.log(Level.WARNING, "Expect updates with CreateTime, got " + record.timestampType() + ": " + record.timestamp() + " " + path + " = " + node_config); logger.log(Level.FINE, () -> - record.topic() + " @ " + - TimestampFormats.MILLI_FORMAT.format(Instant.ofEpochMilli(timestamp)) + " " + - type + path + " = " + node_config); + record.topic() + " @ " + + TimestampFormats.MILLI_FORMAT.format(Instant.ofEpochMilli(timestamp)) + " " + + type + path + " = " + node_config); - try - { + try { // Only update listeners if the node changed AlarmTreeItem changed_node = null; final Object json = node_config == null ? null : JsonModelReader.parseJsonText(node_config); - if (type.equals(AlarmSystem.CONFIG_PREFIX)) - { - if (json == null) - { // No config -> Delete node + if (type.equals(AlarmSystem.CONFIG_PREFIX)) { + if (json == null) { // No config -> Delete node final AlarmTreeItem node = deleteNode(path); // If this was a known node, notify listeners - if (node != null) - { + if (node != null) { logger.log(Level.FINE, () -> "Delete " + path); for (final AlarmClientListener listener : listeners) listener.itemRemoved(node); } - } - else - { // Configuration update + } else { // Configuration update if (JsonModelReader.isStateUpdate(json)) logger.log(Level.WARNING, "Got config update with state content: " + record.key() + " " + node_config); - else - { + else { AlarmTreeItem node = findNode(path); // New node? Will need to send update. Otherwise update when there's a change if (node == null) @@ -289,27 +319,18 @@ private void handleUpdate(final ConsumerRecord record) changed_node = node; } } - } - else if (type.equals(AlarmSystem.STATE_PREFIX)) - { // State update - if (json == null) - { // State update for deleted node, ignore + } else if (type.equals(AlarmSystem.STATE_PREFIX)) { // State update + if (json == null) { // State update for deleted node, ignore logger.log(Level.FINE, () -> "Got state update for deleted node: " + record.key() + " " + node_config); return; - } - else if (! JsonModelReader.isStateUpdate(json)) - { + } else if (!JsonModelReader.isStateUpdate(json)) { logger.log(Level.WARNING, "Got state update with config content: " + record.key() + " " + node_config); return; - } - else if (deleted_paths.contains(path)) - { + } else if (deleted_paths.contains(path)) { // It it _deleted_?? logger.log(Level.FINE, () -> "Ignoring state for deleted item: " + record.key() + " " + node_config); return; - } - else - { + } else { AlarmTreeItem node = findNode(path); // New node? Create, and remember to notify if (node == null) @@ -334,40 +355,36 @@ else if (deleted_paths.contains(path)) // else: Neither config nor state update; ignore. // If there were changes, notify listeners - if (changed_node != null) - { + if (changed_node != null) { logger.log(Level.FINE, "Update " + path + " to " + changed_node.getState()); for (final AlarmClientListener listener : listeners) listener.itemUpdated(changed_node); } - } - catch (final Exception ex) - { + } catch (final Exception ex) { logger.log(Level.WARNING, - "Alarm config update error for path " + path + - ", config " + node_config, ex); + "Alarm config update error for path " + path + + ", config " + node_config, ex); } } - /** Find existing node + /** + * Find existing node * - * @param path Path to node - * @return Node, null if model does not contain the node - * @throws Exception on error + * @param path Path to node + * @return Node, null if model does not contain the node + * @throws Exception on error */ - private AlarmTreeItem findNode(final String path) throws Exception - { + private AlarmTreeItem findNode(final String path) throws Exception { final String[] path_elements = AlarmTreePath.splitPath(path); // Start of path must match the alarm tree root - if (path_elements.length < 1 || - !root.getName().equals(path_elements[0])) + if (path_elements.length < 1 || + !root.getName().equals(path_elements[0])) throw new Exception("Invalid path for alarm configuration " + root.getName() + ": " + path); // Walk down the path AlarmTreeItem node = root; - for (int i=1; i findNode(final String path) throws Exception return node; } - /** Delete node + /** + * Delete node * - *

It's OK to try delete an unknown node: - * The node might have once existed, but was then deleted. - * The last entry in the configuration database is then the deletion hint. - * A new model that reads this node-to-delete information - * thus never knew the node. + *

It's OK to try delete an unknown node: + * The node might have once existed, but was then deleted. + * The last entry in the configuration database is then the deletion hint. + * A new model that reads this node-to-delete information + * thus never knew the node. * - * @param path Path to node to delete - * @return Node that was removed, or null if model never knew that node - * @throws Exception on error + * @param path Path to node to delete + * @return Node that was removed, or null if model never knew that node + * @throws Exception on error */ - private AlarmTreeItem deleteNode(final String path) throws Exception - { + private AlarmTreeItem deleteNode(final String path) throws Exception { // Mark path as deleted so we ignore state updates deleted_paths.add(path); @@ -402,49 +419,44 @@ private AlarmTreeItem deleteNode(final String path) throws Exception return node; } - /** Find an existing alarm tree item or create a new one + /** + * Find an existing alarm tree item or create a new one * - *

Informs listener about created nodes, - * if necessary one notification for each created node along the path. + *

Informs listener about created nodes, + * if necessary one notification for each created node along the path. * - * @param path Alarm tree path - * @param is_leaf Is this the path to a leaf? - * @return {@link AlarmTreeItem} - * @throws Exception on error + * @param path Alarm tree path + * @param is_leaf Is this the path to a leaf? + * @return {@link AlarmTreeItem} + * @throws Exception on error */ - private AlarmTreeItem findOrCreateNode(final String path, final boolean is_leaf) throws Exception - { + private AlarmTreeItem findOrCreateNode(final String path, final boolean is_leaf) throws Exception { // In case it was previously deleted: deleted_paths.remove(path); final String[] path_elements = AlarmTreePath.splitPath(path); // Start of path must match the alarm tree root - if (path_elements.length < 1 || - !root.getName().equals(path_elements[0])) + if (path_elements.length < 1 || + !root.getName().equals(path_elements[0])) throw new Exception("Invalid path for alarm configuration " + root.getName() + ": " + path); // Walk down the path AlarmClientNode parent = root; - for (int i=1; i node = parent.getChild(name); // Create missing nodes - if (node == null) - { // Done when creating leaf - if (last && is_leaf) - { + if (node == null) { // Done when creating leaf + if (last && is_leaf) { node = new AlarmClientLeaf(parent.getPathName(), name); node.addToParent(parent); logger.log(Level.FINE, "Create " + path); for (final AlarmClientListener listener : listeners) listener.itemAdded(node); return node; - } - else - { + } else { node = new AlarmClientNode(parent.getPathName(), name); node.addToParent(parent); for (final AlarmClientListener listener : listeners) @@ -455,10 +467,10 @@ private AlarmTreeItem findOrCreateNode(final String path, final boolean is_le if (last) return node; // Found or created intermediate node; continue walking down the path - if (! (node instanceof AlarmClientNode)) + if (!(node instanceof AlarmClientNode)) throw new Exception("Expected intermediate node, found " + - node.getClass().getSimpleName() + " " + node.getName() + - " while traversing " + path); + node.getClass().getSimpleName() + " " + node.getName() + + " while traversing " + path); parent = (AlarmClientNode) node; } @@ -466,73 +478,69 @@ private AlarmTreeItem findOrCreateNode(final String path, final boolean is_le return parent; } - /** Add a component to the alarm tree - * @param path_name to parent Root or parent component under which to add the component - * @param new_name Name of the new component + /** + * Add a component to the alarm tree + * + * @param path_name to parent Root or parent component under which to add the component + * @param new_name Name of the new component */ - public void addComponent(final String path_name, final String new_name) throws Exception - { - try - { + public void addComponent(final String path_name, final String new_name) { + try { sendNewItemInfo(path_name, new_name, new AlarmClientNode(null, new_name)); - } - catch (final Exception ex) - { + } catch (final Exception ex) { logger.log(Level.WARNING, "Cannot add component " + new_name + " to " + path_name, ex); } } - /** Add a component to the alarm tree - * @param path_name to parent Root or parent component under which to add the component - * @param new_name Name of the new component + /** + * Add a component to the alarm tree + * + * @param path_name to parent Root or parent component under which to add the component + * @param new_name Name of the new component */ - public void addPV(final String path_name, final String new_name) - { - try - { + public void addPV(final String path_name, final String new_name) { + try { sendNewItemInfo(path_name, new_name, new AlarmClientLeaf(null, new_name)); - } - catch (final Exception ex) - { + } catch (final Exception ex) { logger.log(Level.WARNING, "Cannot add pv " + new_name + " to " + path_name, ex); } } - private void sendNewItemInfo(String path_name, final String new_name, final AlarmTreeItem content) throws Exception - { + private void sendNewItemInfo(String path_name, final String new_name, final AlarmTreeItem content) throws Exception { // Send message about new component. // All clients, including this one, will receive and then add the new component. final String new_path = AlarmTreePath.makePath(path_name, new_name); sendItemConfigurationUpdate(new_path, content); } - /** Send item configuration - * - *

All clients, including this one, will update when they receive the message + /** + * Client code must not call this on the UI thread as it may block up to {@link #KAFKA_CLIENT_TIMEOUT} seconds. + * Send item configuration. + *

All clients, including this one, will update when they receive the message * - * @param path Path to the item - * @param config A prototype item (path is ignored) that holds the new configuration - * @throws Exception on error + * @param path Path to the item + * @param config A prototype item (path is ignored) that holds the new configuration + * @throws Exception on error */ - public void sendItemConfigurationUpdate(final String path, final AlarmTreeItem config) throws Exception - { + public void sendItemConfigurationUpdate(final String path, final AlarmTreeItem config) throws Exception { final String json = new String(JsonModelWriter.toJsonBytes(config)); final ProducerRecord record = new ProducerRecord<>(config_topic, AlarmSystem.CONFIG_PREFIX + path, json); - producer.send(record); + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); } - /** Remove a component (and sub-items) from alarm tree - * @param item Item to remove - * @throws Exception on error + /** + * Client must should not call this on the UI thread as it may block up to 2x{@link #KAFKA_CLIENT_TIMEOUT} seconds. + * Remove a component (and sub-items) from alarm tree + * + * @param item Item to remove + * @throws Exception on error */ - public void removeComponent(final AlarmTreeItem item) throws Exception - { - try - { - // Depth first deletion of all child nodes. - final List> children = item.getChildren(); - for (final AlarmTreeItem child : children) - removeComponent(child); + public void removeComponent(final AlarmTreeItem item) throws Exception { + try { + // Depth first deletion of all child nodes. + final List> children = item.getChildren(); + for (final AlarmTreeItem child : children) + removeComponent(child); // Send message about item to remove // All clients, including this one, will receive and then remove the item. @@ -542,75 +550,67 @@ public void removeComponent(final AlarmTreeItem item) throws Exception // The id message must arrive before the tombstone. final String json = new String(JsonModelWriter.deleteMessageToBytes()); final ProducerRecord id = new ProducerRecord<>(config_topic, AlarmSystem.CONFIG_PREFIX + item.getPathName(), json); - producer.send(id); + producer.send(id).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); final ProducerRecord tombstone = new ProducerRecord<>(config_topic, AlarmSystem.CONFIG_PREFIX + item.getPathName(), null); - producer.send(tombstone); - } - catch (Exception ex) - { + producer.send(tombstone).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); + } catch (Exception ex) { throw new Exception("Error deleting " + item.getPathName(), ex); } } - /** @param item Item for which to acknowledge alarm - * @param acknowledge true to acknowledge, else un-acknowledge + /** + * Client must should not call this on the UI thread as it may block up to {@link #KAFKA_CLIENT_TIMEOUT} seconds. + * + * @param item Item for which to acknowledge alarm + * @param acknowledge true to acknowledge, else un-acknowledge */ - public void acknowledge(final AlarmTreeItem item, final boolean acknowledge) throws Exception - { - try - { + public void acknowledge(final AlarmTreeItem item, final boolean acknowledge) throws Exception { + try { final String cmd = acknowledge ? "acknowledge" : "unacknowledge"; - final String json = new String (JsonModelWriter.commandToBytes(cmd)); + final String json = new String(JsonModelWriter.commandToBytes(cmd)); final ProducerRecord record = new ProducerRecord<>(command_topic, AlarmSystem.COMMAND_PREFIX + item.getPathName(), json); - producer.send(record); - } - catch (final Exception ex) - { + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); + } catch (final Exception ex) { logger.log(Level.WARNING, "Cannot acknowledge component " + item, ex); throw ex; } } - /** @return true if connected to server, else updates have timed out */ - public boolean isServerAlive() - { + /** + * @return true if connected to server, else updates have timed out + */ + public boolean isServerAlive() { return !has_timed_out; } - /** Check if there have been any messages from server */ - private void checkServerState() - { + /** + * Check if there have been any messages from server + */ + private void checkServerState() { final long now = System.currentTimeMillis(); - if (now - last_state_update > AlarmSystem.idle_timeout_ms*3) - { - if (! has_timed_out) - { + if (now - last_state_update > AlarmSystem.idle_timeout_ms * 3) { + if (!has_timed_out) { has_timed_out = true; for (final AlarmClientListener listener : listeners) listener.serverStateChanged(false); } + } else if (has_timed_out) { + has_timed_out = false; + for (final AlarmClientListener listener : listeners) + listener.serverStateChanged(true); } - else - if (has_timed_out) - { - has_timed_out = false; - for (final AlarmClientListener listener : listeners) - listener.serverStateChanged(true); - } } - /** Stop client */ - public void shutdown() - { + /** + * Stop client + */ + public void shutdown() { running.set(false); consumer.wakeup(); - try - { + try { thread.join(2000); - } - catch (final InterruptedException ex) - { + } catch (final InterruptedException ex) { logger.log(Level.WARNING, thread.getName() + " thread doesn't shut down", ex); } logger.log(Level.INFO, () -> thread.getName() + " shut down"); diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmConfigMonitor.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmConfigMonitor.java index 0daf5cf327..45bcadc231 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmConfigMonitor.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmConfigMonitor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -24,7 +24,12 @@ * Not receiving any alarm client updates for a while * likely means that we have a stable configuration. * - *

This helper awaits such a pause in updates. + *

This helper first waits for an initial config message, + * allowing for the connection to take some time. + * Based on past experience, we then receive a flurry of + * config messages. + * By then awaiting a pause in configuration updates, + * we assume that a complete configuration snapshot has been received. * * @author Kay Kasemir * @author Evan Smith @@ -34,9 +39,11 @@ public class AlarmConfigMonitor { private final AlarmClient client; private final ResettableTimeout timer; + private final long idle_secs; private final AtomicInteger updates = new AtomicInteger(); - private final AlarmClientListener updateListener = new AlarmClientListener() + /** Listener to messages, resetting timer on config messages */ + private final AlarmClientListener config_listener = new AlarmClientListener() { @Override public void serverStateChanged(final boolean alive) @@ -50,7 +57,7 @@ public void serverModeChanged(boolean maintenance_mode) //NOP } - @Override + @Override public void serverDisableNotifyChanged(boolean disable_notify) { //NOP @@ -59,16 +66,16 @@ public void serverDisableNotifyChanged(boolean disable_notify) @Override public void itemAdded(final AlarmTreeItem item) { - // Reset the timer when receiving update - timer.reset(); + // Reset the timer when receiving config update + timer.reset(idle_secs); updates.incrementAndGet(); } @Override public void itemRemoved(final AlarmTreeItem item) { - // Reset the timer when receiving update - timer.reset(); + // Reset the timer when receiving config update + timer.reset(idle_secs); updates.incrementAndGet(); } @@ -79,13 +86,15 @@ public void itemUpdated(final AlarmTreeItem item) } }; - /** @param idle_secs Seconds after which we decide that there's a pause in configuration updates + /** @param initial_secs Seconds to wait for the initial config message (a 'connection' timeout) + * @param idle_secs Seconds after which we decide that there's a pause in configuration updates (assuming we received complete config snapshot) * @param client AlarmClient to check for a pause in updates */ - public AlarmConfigMonitor(final long idle_secs, final AlarmClient client) + public AlarmConfigMonitor(final long initial_secs, final long idle_secs, final AlarmClient client) { this.client = client; - timer = new ResettableTimeout(idle_secs); + this.idle_secs = idle_secs; + timer = new ResettableTimeout(initial_secs); } /** Wait for a pause in configuration updates @@ -94,7 +103,7 @@ public AlarmConfigMonitor(final long idle_secs, final AlarmClient client) */ public void waitForPauseInUpdates(final long timeout) throws Exception { - client.addListener(updateListener); + client.addListener(config_listener); if (! timer.awaitTimeout(timeout)) throw new Exception(timeout + " seconds have passed, I give up waiting for updates to subside."); // Reset the counter to count any updates received after we decide to continue. @@ -110,7 +119,7 @@ public int getCount() /** Call when no longer interested in checking updates */ public void dispose() { - client.removeListener(updateListener); + client.removeListener(config_listener); timer.shutdown(); } } diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/KafkaHelper.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/KafkaHelper.java index d3dcf4362c..17fd53d4a3 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/KafkaHelper.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/KafkaHelper.java @@ -31,6 +31,7 @@ import org.apache.kafka.streams.KafkaStreams; import org.apache.kafka.streams.StreamsBuilder; import org.apache.kafka.streams.StreamsConfig; +import org.phoebus.applications.alarm.AlarmSystem; /** Alarm client model * @@ -117,13 +118,12 @@ public static Producer connectProducer(final String kafka_server kafka_props.put("bootstrap.servers", kafka_servers); // Collect messages for 20ms until sending them out as a batch kafka_props.put("linger.ms", 20); + kafka_props.put("max.block.ms", AlarmSystem.max_block_ms == 0 ? 10000 : AlarmSystem.max_block_ms); // Write String key, value final Serializer serializer = new StringSerializer(); - final Producer producer = new KafkaProducer<>(kafka_props, serializer, serializer); - - return producer; + return new KafkaProducer<>(kafka_props, serializer, serializer); } /** @@ -131,7 +131,7 @@ public static Producer connectProducer(final String kafka_server * @param kafka_servers - Sever to connect to. * @param topics List of topics to aggregate. * @param aggregate_topic - Name of topic to aggregate to. - * @param kafka_props File name to load additional settings for the kafka stream + * @param properties_file File name to load additional settings for the kafka stream * @return aggregate_stream - KafkaStreams * @author Evan Smith */ @@ -162,7 +162,7 @@ static public Properties loadPropsFromFile(String filePath) { logger.fine("loading file from path: " + filePath); Properties properties = new Properties(); if(filePath != null && !filePath.isBlank()){ - try(FileInputStream file = new FileInputStream(filePath);){ + try(FileInputStream file = new FileInputStream(filePath)){ properties.load(file); } catch(IOException e) { logger.log(Level.SEVERE, "failed to load kafka properties", e); diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/messages/AlarmMessageUtil.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/messages/AlarmMessageUtil.java index 1ad0393a25..3f1dc0b8e6 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/messages/AlarmMessageUtil.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/messages/AlarmMessageUtil.java @@ -45,6 +45,9 @@ public class AlarmMessageUtil implements Serializable{ // Object mapper for all other alarm messages @JsonIgnore static final ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.registerModule(new JavaTimeModule()); + } private static class AlarmStateJsonMessage { @JsonIgnore diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/EnabledState.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/EnabledState.java index e1cc86b99e..ca4dab087b 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/EnabledState.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/EnabledState.java @@ -54,7 +54,7 @@ public int hashCode() { final int prime = 31; final int enabled_check = this.enabled ? 1 : 0; - int result = enabled_date.hashCode(); + int result = enabled_date == null ? 0 : enabled_date.hashCode(); result = prime * result + enabled_check; return result; } diff --git a/app/alarm/model/src/main/resources/alarm_preferences.properties b/app/alarm/model/src/main/resources/alarm_preferences.properties index 6d418ef185..d2ba480fed 100644 --- a/app/alarm/model/src/main/resources/alarm_preferences.properties +++ b/app/alarm/model/src/main/resources/alarm_preferences.properties @@ -5,13 +5,15 @@ # Kafka Server host:port server=localhost:9092 -# A file to configure the properites of kafka clients +# A file to configure the properties of kafka clients kafka_properties= -# Name of alarm tree root +# Name of alarm tree root. +# Configures the alarm configuration used by the alarm server. +# For the UI, it sets the default alarm configuration. config_name=Accelerator -# Names of selectable alarm configurations +# Names of selectable alarm configurations for UI. # The `config_name` will be used as the default for newly opened tools, # and if `config_names` is empty, it remains the only option. # When one or more comma-separated configurations are listed, @@ -22,6 +24,8 @@ config_names=Accelerator, Demo # Timeout in seconds for initial PV connection connection_timeout=30 +# Timeout in seconds for "sevrpv:" updates +severity_pv_timeout=5 ## Area Panel @@ -136,3 +140,6 @@ shelving_options=1 hour, 6 hours, 12 hours, 1 day, 7 days, 30 days # # Format: M1=Value1, M2=Value2 macros=TOP=/home/controls/displays,WEBROOT=http://localhost/controls/displays + +# Max time in ms a producer call will block. +max_block_ms=10000 \ No newline at end of file diff --git a/app/alarm/model/src/test/java/org/phoebus/applications/alarm/AlarmModelSnapshotDemo.java b/app/alarm/model/src/test/java/org/phoebus/applications/alarm/AlarmModelSnapshotDemo.java index 409144f1ae..b682be0603 100644 --- a/app/alarm/model/src/test/java/org/phoebus/applications/alarm/AlarmModelSnapshotDemo.java +++ b/app/alarm/model/src/test/java/org/phoebus/applications/alarm/AlarmModelSnapshotDemo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -20,25 +20,25 @@ @SuppressWarnings("nls") public class AlarmModelSnapshotDemo { - @Test - public void testAlarmModelWriter() throws Exception - { - // Get alarm configuration - final AlarmClient client = new AlarmClient(AlarmDemoSettings.SERVERS, AlarmDemoSettings.ROOT, AlarmDemoSettings.KAFKA_PROPERTIES_FILE); - - System.out.println("Wait for stable configuration, i.e. no changes for 4 seconds..."); - - // Wait until we have a snapshot, i.e. no more changes for 4 seconds - final AlarmConfigMonitor monitor = new AlarmConfigMonitor(4, client); - monitor.waitForPauseInUpdates(30); - - System.out.println("Alarm configuration:"); - - - final ByteArrayOutputStream buf = new ByteArrayOutputStream(); - final XmlModelWriter xmlWriter = new XmlModelWriter(buf); - xmlWriter.write(client.getRoot()); - xmlWriter.close(); + @Test + public void testAlarmModelWriter() throws Exception + { + // Get alarm configuration + final AlarmClient client = new AlarmClient(AlarmDemoSettings.SERVERS, AlarmDemoSettings.ROOT, AlarmDemoSettings.KAFKA_PROPERTIES_FILE); + client.start(); + + System.out.println("Wait 10 secs for connection, then for stable configuration, i.e. no changes for 4 seconds..."); + final long start = System.currentTimeMillis(); + + final AlarmConfigMonitor monitor = new AlarmConfigMonitor(10, 4, client); + monitor.waitForPauseInUpdates(30); + final double secs = (System.currentTimeMillis() - start) / 1000.0; + System.out.format("Alarm configuration after %.3f seconds:\n\n", secs); + + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + final XmlModelWriter xmlWriter = new XmlModelWriter(buf); + xmlWriter.write(client.getRoot()); + xmlWriter.close(); final String xml = buf.toString(); System.out.println(xml); @@ -47,5 +47,5 @@ public void testAlarmModelWriter() throws Exception System.out.println("Bummer, there were " + changes + " updates to the configuration, might have to try this again..."); monitor.dispose(); client.shutdown(); - } + } } diff --git a/app/alarm/model/src/test/java/org/phoebus/applications/alarm/ResettableTimeoutTest.java b/app/alarm/model/src/test/java/org/phoebus/applications/alarm/ResettableTimeoutTest.java index 24eb6b2ada..0488abd727 100644 --- a/app/alarm/model/src/test/java/org/phoebus/applications/alarm/ResettableTimeoutTest.java +++ b/app/alarm/model/src/test/java/org/phoebus/applications/alarm/ResettableTimeoutTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2012-2018 Oak Ridge National Laboratory. + * Copyright (c) 2012-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -61,4 +61,23 @@ public void testReset() assertThat(timer.awaitTimeout(6), equalTo(true)); timer.shutdown(); } + + @Test + public void testChangingTimeout() + { + ResettableTimeout timer = new ResettableTimeout(4); + System.out.println("Should time out in 4 secs"); + assertThat(timer.awaitTimeout(8), equalTo(true)); + timer.shutdown(); + + + timer = new ResettableTimeout(4); + System.out.println("Now resetting after just 1 second to a 1 second timeout..."); + assertThat(timer.awaitTimeout(1), equalTo(false)); + timer.reset(1); + + System.out.println(".. and expecting time out in 1 second..."); + assertThat(timer.awaitTimeout(4), equalTo(true)); + timer.shutdown(); + } } diff --git a/app/alarm/pom.xml b/app/alarm/pom.xml index 321627d9bd..a5b1307493 100644 --- a/app/alarm/pom.xml +++ b/app/alarm/pom.xml @@ -5,7 +5,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT model diff --git a/app/alarm/ui/pom.xml b/app/alarm/ui/pom.xml index 5aad4eaab0..d36aa6b31f 100644 --- a/app/alarm/ui/pom.xml +++ b/app/alarm/ui/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-alarm - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-alarm-ui @@ -22,27 +22,27 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT net.sf.sociaal diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/AlarmUI.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/AlarmUI.java index 92760cdb67..e62af387bd 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/AlarmUI.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/AlarmUI.java @@ -50,6 +50,19 @@ public class AlarmUI createColor(Preferences.undefined_severity_text_color) // UNDEFINED }; + private static final Color[] alarm_area_panel_severity_colors = new Color[] + { + createColor(Preferences.alarm_area_panel_ok_severity_text_color), // OK + createColor(Preferences.alarm_area_panel_minor_severity_text_color) .deriveColor(0, 1.0, ADJUST, 1.0), // MINOR_ACK + createColor(Preferences.alarm_area_panel_major_severity_text_color) .deriveColor(0, 1.0, ADJUST, 1.0), // MAJOR_ACK + createColor(Preferences.alarm_area_panel_invalid_severity_text_color) .deriveColor(0, 1.0, ADJUST, 1.0), // INVALID_ACK + createColor(Preferences.alarm_area_panel_undefined_severity_text_color).deriveColor(0, 1.0, ADJUST, 1.0), // UNDEFINED_ACK + createColor(Preferences.alarm_area_panel_minor_severity_text_color), // MINOR + createColor(Preferences.alarm_area_panel_major_severity_text_color), // MAJOR + createColor(Preferences.alarm_area_panel_invalid_severity_text_color), // INVALID + createColor(Preferences.alarm_area_panel_undefined_severity_text_color) // UNDEFINED + }; + private static Color createColor(int[] rgb) { if (rgb.length == 3) @@ -75,27 +88,53 @@ else if (rgb.length == 4) private static final Background[] severity_backgrounds = new Background[] { new Background(new BackgroundFill(createColor(Preferences.ok_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // OK - new Background(new BackgroundFill(createColor(Preferences.minor_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR_ACK - new Background(new BackgroundFill(createColor(Preferences.major_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR_ACK - new Background(new BackgroundFill(createColor(Preferences.invalid_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID_ACK + new Background(new BackgroundFill(createColor(Preferences.minor_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR_ACK + new Background(new BackgroundFill(createColor(Preferences.major_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR_ACK + new Background(new BackgroundFill(createColor(Preferences.invalid_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID_ACK new Background(new BackgroundFill(createColor(Preferences.undefined_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // UNDEFINED_ACK - new Background(new BackgroundFill(createColor(Preferences.minor_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR - new Background(new BackgroundFill(createColor(Preferences.major_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR - new Background(new BackgroundFill(createColor(Preferences.invalid_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID - new Background(new BackgroundFill(createColor(Preferences.undefined_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // UNDEFINED + new Background(new BackgroundFill(createColor(Preferences.minor_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR + new Background(new BackgroundFill(createColor(Preferences.major_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR + new Background(new BackgroundFill(createColor(Preferences.invalid_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID + new Background(new BackgroundFill(createColor(Preferences.undefined_severity_background_color), CornerRadii.EMPTY, Insets.EMPTY)), // UNDEFINED }; - private static final Background[] legacy_table_severity_backgrounds = new Background[] - { - new Background(new BackgroundFill(createColor(Preferences.ok_severity_text_color), CornerRadii.EMPTY, Insets.EMPTY)), // OK - new Background(new BackgroundFill(createColor(Preferences.minor_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR_ACK - new Background(new BackgroundFill(createColor(Preferences.major_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR_ACK - new Background(new BackgroundFill(createColor(Preferences.invalid_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID_ACK - new Background(new BackgroundFill(createColor(Preferences.undefined_severity_text_color).deriveColor(0, ADJUST, 1.0, 1.0), CornerRadii.EMPTY, Insets.EMPTY)), // UNDEFINED_ACK - new Background(new BackgroundFill(createColor(Preferences.minor_severity_text_color), CornerRadii.EMPTY, Insets.EMPTY)), // MINOR - new Background(new BackgroundFill(createColor(Preferences.major_severity_text_color), CornerRadii.EMPTY, Insets.EMPTY)), // MAJOR - new Background(new BackgroundFill(createColor(Preferences.invalid_severity_text_color), CornerRadii.EMPTY, Insets.EMPTY)), // INVALID - new Background(new BackgroundFill(createColor(Preferences.undefined_severity_text_color), CornerRadii.EMPTY, Insets.EMPTY)), // UNDEFINED + private static final Color[] severity_background_colors = new Color[] + { + createColor(Preferences.ok_severity_background_color), // OK + createColor(Preferences.minor_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MINOR_ACK + createColor(Preferences.major_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MAJOR_ACK + createColor(Preferences.invalid_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // INVALID_ACK + createColor(Preferences.undefined_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), // UNDEFINED_ACK + createColor(Preferences.minor_severity_background_color) , // MINOR + createColor(Preferences.major_severity_background_color) , // MAJOR + createColor(Preferences.invalid_severity_background_color) , // INVALID + createColor(Preferences.undefined_severity_background_color) , // UNDEFINED + }; + + private static final Color[] alarm_area_panel_severity_backgrounds = new Color[] + { + createColor(Preferences.alarm_area_panel_ok_severity_background_color), // OK + createColor(Preferences.alarm_area_panel_minor_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MINOR_ACK + createColor(Preferences.alarm_area_panel_major_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MAJOR_ACK + createColor(Preferences.alarm_area_panel_invalid_severity_background_color) .deriveColor(0, ADJUST, 1.0, 1.0), // INVALID_ACK + createColor(Preferences.alarm_area_panel_undefined_severity_background_color).deriveColor(0, ADJUST, 1.0, 1.0), // UNDEFINED_ACK + createColor(Preferences.alarm_area_panel_minor_severity_background_color), // MINOR + createColor(Preferences.alarm_area_panel_major_severity_background_color), // MAJOR + createColor(Preferences.alarm_area_panel_invalid_severity_background_color), // INVALID + createColor(Preferences.alarm_area_panel_undefined_severity_background_color), // UNDEFINED + }; + + private static final Color[] legacy_table_severity_backgrounds = new Color[] + { + createColor(Preferences.ok_severity_text_color), // OK + createColor(Preferences.minor_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MINOR_ACK + createColor(Preferences.major_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), // MAJOR_ACK + createColor(Preferences.invalid_severity_text_color) .deriveColor(0, ADJUST, 1.0, 1.0), // INVALID_ACK + createColor(Preferences.undefined_severity_text_color).deriveColor(0, ADJUST, 1.0, 1.0), // UNDEFINED_ACK + createColor(Preferences.minor_severity_text_color), // MINOR + createColor(Preferences.major_severity_text_color), // MAJOR + createColor(Preferences.invalid_severity_text_color), // INVALID + createColor(Preferences.undefined_severity_text_color), // UNDEFINED }; @@ -108,7 +147,14 @@ else if (rgb.length == 4) public static Color getColor(final SeverityLevel severity) { return severity_colors[severity.ordinal()]; + } + /** @param severity {@link SeverityLevel} + * @return Color + */ + public static Color getAlarmAreaPanelColor(final SeverityLevel severity) + { + return alarm_area_panel_severity_colors[severity.ordinal()]; } /** @param severity {@link SeverityLevel} @@ -119,6 +165,14 @@ public static Image getIcon(final SeverityLevel severity) return severity_icons[severity.ordinal()]; } + /** @param severity {@link SeverityLevel} + * @return Color, may be null + */ + public static Color getBackgroundColor(final SeverityLevel severity) + { + return severity_background_colors[severity.ordinal()]; + } + /** @param severity {@link SeverityLevel} * @return Background, may be null */ @@ -127,17 +181,26 @@ public static Background getBackground(final SeverityLevel severity) return severity_backgrounds[severity.ordinal()]; } + /** @param severity {@link SeverityLevel} * @return Background, may be null */ - public static Background getLegacyTableBackground(final SeverityLevel severity) + public static Color getLegacyTableBackground(final SeverityLevel severity) { return legacy_table_severity_backgrounds[severity.ordinal()]; } + /** @param severity {@link SeverityLevel} + * @return Background, may be null + */ + public static Color getAlarmAreaPanelBackgroundColor(final SeverityLevel severity) + { + return alarm_area_panel_severity_backgrounds[severity.ordinal()]; + } + /** Verify authorization, qualified by model's current config * @param model Alarm client model - * @param auto Authorization name + * @param authorization Authorization name * @return true if the user has authorization */ private static boolean haveQualifiedAuthorization(final AlarmClient model, final String authorization) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index 389fb7e00a..f36654a84d 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -15,6 +15,8 @@ public class Messages public static String acknowledgeFailed; public static String addComponentFailed; + public static String disableAlarmFailed; + public static String enableAlarmFailed; public static String moveItemFailed; public static String removeComponentFailed; public static String renameItemFailed; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java index f757130562..6bc83d1a10 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java @@ -17,12 +17,21 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.client.AlarmClientListener; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.model.SeverityLevel; import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.ui.javafx.JFXUtil; import org.phoebus.ui.javafx.UpdateThrottle; import javafx.application.Platform; @@ -32,16 +41,6 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; @@ -87,12 +86,16 @@ public class AlarmAreaView extends StackPane implements AlarmClientListener private final Font font = new Font(AlarmSystem.alarm_area_font_size); private final Border border = new Border(new BorderStroke(Color.BLACK, style, radii, new BorderWidths(2))); + private final String alarmConfigName; + /** @param model Model */ public AlarmAreaView(final AlarmClient model) { if (model.isRunning()) throw new IllegalStateException(); + alarmConfigName = model.getRoot().getName(); + grid.setHgap(AlarmSystem.alarm_area_gap); grid.setVgap(AlarmSystem.alarm_area_gap); grid.setPadding(new Insets(AlarmSystem.alarm_area_gap)); @@ -234,20 +237,15 @@ private void updateItem(final String item_name) logger.log(Level.WARNING, "Cannot update unknown alarm area item " + item_name); return; } - final SeverityLevel severity = areaFilter.getSeverity(item_name); - final Color color = AlarmUI.getColor(severity); - view_item.setBackground(new Background(new BackgroundFill(color, radii, Insets.EMPTY))); - if (color.getBrightness() >= 0.5) - view_item.setTextFill(Color.BLACK); - else - view_item.setTextFill(Color.WHITE); + SeverityLevel severityLevel = areaFilter.getSeverity(item_name); + view_item.setStyle("-fx-alignment: center; -fx-border-color: black; -fx-border-width: 2; -fx-border-radius: 10; -fx-background-insets: 1; -fx-background-radius: 10; -fx-text-fill: " + JFXUtil.webRGB(AlarmUI.getAlarmAreaPanelColor(severityLevel)) + "; -fx-background-color: " + JFXUtil.webRGB(AlarmUI.getAlarmAreaPanelBackgroundColor(severityLevel))); } private void createContextMenu() { final ObservableList menu_items = menu.getItems(); - menu_items.add(new OpenTreeViewAction()); + menu_items.add(new OpenTreeViewAction(alarmConfigName)); this.setOnContextMenuRequested(event -> menu.show(this.getScene().getWindow(), event.getScreenX(), event.getScreenY()) ); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/OpenTreeViewAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/OpenTreeViewAction.java index 11ccb0ad74..fd99a80747 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/OpenTreeViewAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/OpenTreeViewAction.java @@ -10,6 +10,7 @@ import java.util.logging.Level; import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.ui.AlarmURI; import org.phoebus.applications.alarm.ui.tree.AlarmTreeMenuEntry; import javafx.scene.control.MenuItem; @@ -21,10 +22,15 @@ @SuppressWarnings("nls") public class OpenTreeViewAction extends MenuItem { - /** Constructor */ - public OpenTreeViewAction() + + /** + * Constructor + * @param alarmConfigName The alarm configuration name + */ + public OpenTreeViewAction(String alarmConfigName) { final AlarmTreeMenuEntry entry = new AlarmTreeMenuEntry(); + entry.setResource(AlarmURI.createURI(AlarmSystem.server, alarmConfigName)); setText(entry.getName()); setGraphic(new ImageView(entry.getIcon())); setOnAction(event -> diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableInstance.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableInstance.java index f509c7aba9..cfb6cea7d4 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableInstance.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableInstance.java @@ -10,9 +10,11 @@ import static org.phoebus.applications.alarm.AlarmSystem.logger; import java.net.URI; +import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; +import org.apache.kafka.common.KafkaException; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.ui.AlarmConfigSelector; @@ -73,17 +75,19 @@ public AppDescriptor getAppDescriptor() private Node create(final URI input) throws Exception { final String[] parsed = AlarmURI.parseAlarmURI(input); - server = parsed[0]; - config_name = parsed[1]; + server = AlarmSystem.server; + config_name = Arrays.stream(AlarmSystem.config_names).anyMatch(config_name -> config_name.equals(parsed[1])) ? parsed[1] : AlarmSystem.config_name; try { - client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); - table = new AlarmTableUI(client); - mediator = new AlarmTableMediator(client, table); - client.addListener(mediator); - client.start(); + try { + client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); + } + catch (KafkaException e) { + logger.log(Level.SEVERE, "Failed to connect to Kafka server: " + e.getMessage()); + } + table = new AlarmTableUI(client); if (AlarmSystem.config_names.length > 0) { final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); @@ -91,6 +95,12 @@ private Node create(final URI input) throws Exception table.getToolbar().getItems().add(2, configs); } + if (client != null) { + mediator = new AlarmTableMediator(client, table); + client.addListener(mediator); + client.start(); + } + return table; } catch (Exception ex) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java index 4e045c1572..c07c48981b 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java @@ -17,6 +17,11 @@ import java.util.logging.Level; import java.util.regex.Pattern; +import javafx.application.Platform; +import javafx.scene.layout.Background; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; @@ -24,15 +29,18 @@ import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; import org.phoebus.applications.alarm.ui.AlarmUI; import org.phoebus.applications.alarm.ui.tree.ConfigureComponentAction; +import org.phoebus.framework.jobs.Job; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.persistence.Memento; import org.phoebus.framework.selection.Selection; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuService; import org.phoebus.ui.application.SaveSnapshotAction; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.Brightness; import org.phoebus.ui.javafx.ClearingTextField; import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.javafx.JFXUtil; import org.phoebus.ui.javafx.PrintAction; import org.phoebus.ui.javafx.Screenshot; import org.phoebus.ui.javafx.ToolbarHelper; @@ -68,10 +76,6 @@ import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; -import javafx.scene.layout.Background; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; @@ -195,34 +199,34 @@ public SeverityLevelCell() } @Override - protected void updateItem(final SeverityLevel item, final boolean empty) + protected void updateItem(final SeverityLevel severityLevel, final boolean empty) { - super.updateItem(item, empty); + super.updateItem(severityLevel, empty); - if (empty || item == null) + if (empty || severityLevel == null) { + setStyle("-fx-text-fill: black; -fx-background-color: transparent"); setText(""); - setBackground(null); - setTextFill(Color.BLACK); } else { - setText(item.toString()); + setText(severityLevel.toString()); if (AlarmSystem.alarm_table_color_legacy_background) { - final Background bg = AlarmUI.getLegacyTableBackground(item); - setBackground(bg); - final Paint p = bg.getFills().get(0).getFill(); - if (p instanceof Color && - Brightness.of((Color) p) < Brightness.BRIGHT_THRESHOLD) - setTextFill(Color.WHITE); - else - setTextFill(Color.BLACK); + Color legacyBackgroundColor = AlarmUI.getLegacyTableBackground(severityLevel); + Color legacyTextColor; + if (Brightness.of(legacyBackgroundColor) < Brightness.BRIGHT_THRESHOLD) { + legacyTextColor = Color.WHITE; + } + else { + legacyTextColor = Color.BLACK; + } + setStyle("-fx-alignment: center; -fx-border-color: transparent; -fx-border-width: 2 0 2 0; -fx-background-insets: 2 0 2 0; -fx-text-fill: " + JFXUtil.webRGB(legacyTextColor) + "; -fx-background-color: " + JFXUtil.webRGB(legacyBackgroundColor)); + } else { - setBackground(AlarmUI.getBackground(item)); - setTextFill(AlarmUI.getColor(item)); + setStyle("-fx-alignment: center; -fx-border-color: transparent; -fx-border-width: 2 0 2 0; -fx-background-insets: 2 0 2 0; -fx-text-fill: " + JFXUtil.webRGB(AlarmUI.getColor(severityLevel)) + "; -fx-background-color: " + JFXUtil.webRGB(AlarmUI.getBackgroundColor(severityLevel))); } } } @@ -288,7 +292,17 @@ ToolBar getToolbar() private ToolBar createToolbar() { setMaintenanceMode(false); - server_mode.setOnAction(event -> client.setMode(! client.isMaintenanceMode())); + server_mode.setOnAction(event -> { + JobManager.schedule(client.isMaintenanceMode() ? "Disable maintenance mode" : "Enable maintenance mode", + monitor -> { + try { + client.setMode(! client.isMaintenanceMode()); + } catch (Exception e) { + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(e.getMessage(), e)); + } + }); + + }); // Could 'bind', // server_mode.disableProperty().bind(new SimpleBooleanProperty(!AlarmUI.mayModifyMode(client))); @@ -298,7 +312,15 @@ private ToolBar createToolbar() server_mode.setDisable(true); setDisableNotify(false); - server_notify.setOnAction(event -> client.setNotify(! client.isDisableNotify())); + server_notify.setOnAction(event -> { + JobManager.schedule(client.isDisableNotify() ? "Enable alarm notification" : "Disable alarm notification", monitor -> { + try { + client.setNotify(! client.isDisableNotify()); + } catch (Exception e) { + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(e.getMessage(), e)); + } + }); + }); if (!AlarmUI.mayDisableNotify(client)) server_notify.setDisable(true); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeMenuEntry.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeMenuEntry.java index 1606b193ac..e5f348d488 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeMenuEntry.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeMenuEntry.java @@ -15,12 +15,19 @@ import javafx.scene.image.Image; +import java.net.URI; + /** Menu entry for alarm tree * @author Kay Kasemir */ @SuppressWarnings("nls") public class AlarmTreeMenuEntry implements MenuEntry { + /** + * Identifies alarm configuration. + */ + private URI resource; + @Override public String getName() { @@ -39,10 +46,19 @@ public String getMenuPath() return "Alarm"; } + public void setResource(URI resource){ + this.resource = resource; + } + @Override public Void call() throws Exception { - ApplicationService.createInstance(AlarmTreeApplication.NAME); + if(resource == null){ + ApplicationService.createInstance(AlarmTreeApplication.NAME); + } + else{ + ApplicationService.createInstance(AlarmTreeApplication.NAME, resource); + } return null; } } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index 647f165b82..e7c0c4a413 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2022 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -9,6 +9,7 @@ import static org.phoebus.applications.alarm.AlarmSystem.logger; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -41,7 +42,6 @@ import org.phoebus.ui.javafx.PrintAction; import org.phoebus.ui.javafx.Screenshot; import org.phoebus.ui.javafx.ToolbarHelper; -import org.phoebus.ui.javafx.TreeHelper; import org.phoebus.ui.javafx.UpdateThrottle; import org.phoebus.ui.selection.AppSelection; import org.phoebus.ui.spi.ContextMenuEntry; @@ -228,7 +228,12 @@ private ToolBar createToolbar() show_alarms.setTooltip(new Tooltip("Expand alarm tree to show active alarms")); show_alarms.setOnAction(event -> expandAlarms(tree_view.getRoot())); - return new ToolBar(no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms); + final Button show_disabled = new Button("", + ImageCache.getImageView(AlarmUI.class, "/icons/expand_disabled.png")); + show_disabled.setTooltip(new Tooltip("Expand alarm tree to show disabled PVs")); + show_disabled.setOnAction(event -> expandDisabledPVs(tree_view.getRoot())); + + return new ToolBar(no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); } ToolBar getToolbar() @@ -251,6 +256,29 @@ private void expandAlarms(final TreeItem> node) expandAlarms(sub); } + /** @param node Subtree node where to expand disabled PVs + * @return Does subtree contain disabled PVs? + */ + private boolean expandDisabledPVs(final TreeItem> node) + { + if (node != null && (node.getValue() instanceof AlarmClientLeaf)) + { + AlarmClientLeaf pv = (AlarmClientLeaf) node.getValue(); + if (! pv.isEnabled()) + return true; + } + + // Always expand the root, which itself is not visible, + // but this will show all the top-level elements. + // In addition, expand those items which contain disabled PV. + boolean expand = node == tree_view.getRoot(); + for (TreeItem> sub : node.getChildren()) + if (expandDisabledPVs(sub)) + expand = true; + node.setExpanded(expand); + return expand; + } + private TreeItem> createViewItem(final AlarmTreeItem model_item) { // Create view item for model item itself @@ -466,9 +494,10 @@ public void itemUpdated(final AlarmTreeItem item) } /** Called by throttle to perform accumulated updates */ + @SuppressWarnings("unchecked") private void performUpdates() { - final TreeItem[] view_items; + final TreeItem>[] view_items; synchronized (items_to_update) { // Creating a direct copy, i.e. another new LinkedHashSet<>(items_to_update), @@ -480,8 +509,38 @@ private void performUpdates() items_to_update.clear(); } - for (final TreeItem view_item : view_items) - TreeHelper.triggerTreeItemRefresh(view_item); + // How to update alarm tree cells when data changed? + // `setValue()` with a truly new value (not 'equal') should suffice, + // but there are two problems: + // Since we're currently using the alarm tree model item as a value, + // the value as seen by the TreeView remains the same. + // We could use a model item wrapper class as the cell value + // and replace it (while still holding the same model item!) + // for the TreeView to see a different wrapper value, but + // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, + // replacing a tree cell value fails to trigger refreshes + // for certain hidden items. + // Only replacing the TreeItem gives reliable refreshes. + for (final TreeItem> view_item : view_items) + // Top-level item has no parent, and is not visible, so we keep it + if (view_item.getParent() != null) + { + // Locate item in tree parent + final TreeItem> parent = view_item.getParent(); + final int index = parent.getChildren().indexOf(view_item); + + // Create new TreeItem for that value + final AlarmTreeItem value = view_item.getValue(); + final TreeItem> update = new TreeItem<>(value); + // Move child links to new item + final ArrayList>> children = new ArrayList<>(view_item.getChildren()); + view_item.getChildren().clear(); + update.getChildren().addAll(children); + update.setExpanded(view_item.isExpanded()); + + path2view.put(value.getPathName(), update); + parent.getChildren().set(index, update); + } } /** Context menu, details depend on selected items */ diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeViewCell.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeViewCell.java index a59b0b57b4..d9fe93624f 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeViewCell.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeViewCell.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,7 +7,16 @@ *******************************************************************************/ package org.phoebus.applications.alarm.ui.tree; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.TreeCell; +import javafx.scene.image.ImageView; import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; + import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.client.AlarmClientNode; import org.phoebus.applications.alarm.client.ClientState; @@ -15,28 +24,50 @@ import org.phoebus.applications.alarm.model.SeverityLevel; import org.phoebus.applications.alarm.ui.AlarmUI; -import javafx.scene.control.TreeCell; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.paint.Color; - /** TreeCell for AlarmTreeItem * @author Kay Kasemir */ @SuppressWarnings("nls") class AlarmTreeViewCell extends TreeCell> { + // Originally, the tree cell "graphics" were used for the icon, + // and the built-in label/text that can be controlled via + // setText and setBackground for the text. + // But using that built-in label/text background intermittently removes + // the "triangle" for expanding/collapsing subtrees + // as well as the "cursor" for selecting tree cells or navigating + // cells via cursor keys. + // So we add our own "graphics" to hold an icon and text + private final Label label = new Label(); + private final ImageView image = new ImageView(); + private final HBox content = new HBox(image, label); + + // TreeCell optimizes redraws by suppressing updates + // when old and new values match. + // Since we use the model item as a value, + // the cell sees no change, in fact an identical reference. + // In the fullness of time, a complete redesign might be useful + // to present changing values to the TreeCell, but also note + // the issue shown in org.phoebus.applications.alarm.TreeItemUpdateDemo + // + // So for now we simply force redraws by always pretending a change. + // This seems bad for performance, but profiling the alarm GUI for + // a large configuration like org.phoebus.applications.alarm.AlarmConfigProducerDemo + // with 1000 'sections' of 10 subsections of 10 PVs, + // the time spent in updateItem is negligible. + @Override + protected boolean isItemChanged(final AlarmTreeItem before, final AlarmTreeItem after) + { + return true; + } + @Override protected void updateItem(final AlarmTreeItem item, final boolean empty) { super.updateItem(item, empty); - // Note: Cannot use background because that's used by style sheet and selection/cursor + if (empty || item == null) - { - setText(null); setGraphic(null); - setBackground(Background.EMPTY); - } else { final SeverityLevel severity; @@ -46,7 +77,6 @@ protected void updateItem(final AlarmTreeItem item, final boolean empty) final ClientState state = leaf.getState(); final StringBuilder text = new StringBuilder(); - final Image icon; text.append("PV: ").append(leaf.getName()); if (leaf.isEnabled() && !state.isDynamicallyDisabled()) @@ -61,31 +91,32 @@ protected void updateItem(final AlarmTreeItem item, final boolean empty) .append(state.current_severity).append('/').append(state.current_message) .append(")"); } - setTextFill(AlarmUI.getColor(state.severity)); - setBackground(AlarmUI.getBackground(state.severity)); - icon = AlarmUI.getIcon(state.severity); + label.setTextFill(AlarmUI.getColor(state.severity)); + label.setBackground(AlarmUI.getBackground(state.severity)); + image.setImage(AlarmUI.getIcon(state.severity)); } else { text.append(" (disabled)"); - setTextFill(Color.GRAY); - setBackground(Background.EMPTY); - icon = AlarmUI.disabled_icon; + label.setTextFill(Color.GRAY); + label.setBackground(Background.EMPTY); + image.setImage(AlarmUI.disabled_icon); } - setText(text.toString()); - setGraphic(icon == null ? null : new ImageView(icon)); + label.setText(text.toString()); } else { final AlarmClientNode node = (AlarmClientNode) item; - setText(item.getName()); + label.setText(item.getName()); severity = node.getState().severity; - setTextFill(AlarmUI.getColor(severity)); - setBackground(AlarmUI.getBackground(severity)); - final Image icon = AlarmUI.getIcon(severity); - setGraphic(icon == null ? null : new ImageView(icon)); + label.setTextFill(AlarmUI.getColor(severity)); + label.setBackground(AlarmUI.getBackground(severity)); + image.setImage(AlarmUI.getIcon(severity)); } + // Profiler showed small advantage when skipping redundant 'setGraphic' call + if (getGraphic() != content) + setGraphic(content); } } } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DuplicatePVAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DuplicatePVAction.java index c447a0373b..5fff9d078a 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DuplicatePVAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DuplicatePVAction.java @@ -7,18 +7,23 @@ *******************************************************************************/ package org.phoebus.applications.alarm.ui.tree; +import javafx.application.Platform; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.model.AlarmTreePath; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; import javafx.scene.Node; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputDialog; +import java.util.logging.Level; +import java.util.logging.Logger; + /** Action that adds duplicate of PV to alarm tree configuration * @author Kay Kasemir */ @@ -27,7 +32,7 @@ class DuplicatePVAction extends MenuItem { /** @param node Node to position dialog * @param model Model where new component is added - * @param parent Parent item in alarm tree + * @param original Item subject to copy */ public DuplicatePVAction(final Node node, final AlarmClient model, final AlarmClientLeaf original) { @@ -59,7 +64,14 @@ public DuplicatePVAction(final Node node, final AlarmClient model, final AlarmCl // Request adding new PV final String new_path = AlarmTreePath.makePath(original.getParent().getPathName(), new_name); - JobManager.schedule(getText(), monitor -> model.sendItemConfigurationUpdate(new_path, template)); + JobManager.schedule(getText(), monitor -> { + try { + model.sendItemConfigurationUpdate(new_path, template); + } catch (Exception e) { + Logger.getLogger(DuplicatePVAction.class.getName()).log(Level.WARNING, "Failed to send item configuration", e); + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(e.getMessage(), e)); + } + }); }); } } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java index 308cbe0a6a..ea64bdc4c0 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java @@ -15,8 +15,10 @@ import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.applications.alarm.ui.Messages; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; import javafx.scene.Node; @@ -84,7 +86,14 @@ public EnableComponentAction(final Node node, final AlarmClient model, final Lis { final AlarmClientLeaf copy = pv.createDetachedCopy(); if (copy.setEnabled(doEnable())) - model.sendItemConfigurationUpdate(pv.getPathName(), copy); + try { + model.sendItemConfigurationUpdate(pv.getPathName(), copy); + } catch (Exception e) { + ExceptionDetailsErrorDialog.openError(Messages.error, + copy.isEnabled() ? Messages.enableAlarmFailed : Messages.disableAlarmFailed, + e); + throw e; + } } }); }); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java index 7793c653ca..1d824953a5 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java @@ -7,50 +7,40 @@ *******************************************************************************/ package org.phoebus.applications.alarm.ui.tree; -import java.time.LocalDateTime; -import java.time.temporal.TemporalAmount; - -import org.phoebus.applications.alarm.AlarmSystem; -import org.phoebus.applications.alarm.client.AlarmClient; -import org.phoebus.applications.alarm.client.AlarmClientLeaf; -import org.phoebus.applications.alarm.client.AlarmClientNode; -import org.phoebus.applications.alarm.model.AlarmTreeItem; -import org.phoebus.applications.alarm.ui.tree.datetimepicker.DateTimePicker; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; -import org.phoebus.util.time.SecondsParser; -import org.phoebus.util.time.TimeParser; - import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Dialog; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Spinner; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.stage.Modality; import javafx.util.Duration; +import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.tree.datetimepicker.DateTimePicker; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.util.time.SecondsParser; +import org.phoebus.util.time.TimeParser; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAmount; -/** Dialog for editing {@link AlarmTreeItem} + +/** + * Dialog for editing {@link AlarmTreeItem} * - *

When pressing "OK", dialog sends updated - * configuration. + *

When pressing "OK", dialog sends updated + * configuration. */ @SuppressWarnings("nls") -class ItemConfigDialog extends Dialog -{ +class ItemConfigDialog extends Dialog { private TextField description; private CheckBox enabled, latching, annunciating; private DateTimePicker enabled_date_picker; @@ -60,8 +50,7 @@ class ItemConfigDialog extends Dialog private final TitleDetailTable guidance, displays, commands; private final TitleDetailDelayTable actions; - public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) - { + public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { // Allow multiple instances initModality(Modality.NONE); setTitle("Configure " + item.getName()); @@ -87,8 +76,7 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) path.setEditable(false); layout.add(path, 1, row++); - if (item instanceof AlarmClientLeaf) - { + if (item instanceof AlarmClientLeaf) { final AlarmClientLeaf leaf = (AlarmClientLeaf) item; layout.add(new Label("Description:"), 0, row); @@ -126,7 +114,7 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) enabled_date_picker.setDateTimeValue(leaf.getEnabledDate()); enabled_date_picker.setPrefSize(280, 25); - relative_date = new ComboBox(); + relative_date = new ComboBox<>(); relative_date.setTooltip(new Tooltip("Select a predefined duration for disabling the alarm")); relative_date.getItems().addAll(AlarmSystem.shelving_options); relative_date.setPrefSize(200, 25); @@ -142,15 +130,14 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) // setOnAction for relative date must be set to null as to not trigger event when setting value enabled_date_picker.setOnAction((ActionEvent e) -> { - if (enabled_date_picker.getDateTimeValue() != null) - { + if (enabled_date_picker.getDateTimeValue() != null) { relative_date.setOnAction(null); enabled.setSelected(false); enabled_date_picker.getEditor().commitValue(); relative_date.getSelectionModel().clearSelection(); relative_date.setValue(null); relative_date.setOnAction(relative_event_handler); - }; + } }); final HBox until_box = new HBox(10, enabled_date_picker, relative_date); @@ -168,8 +155,7 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) final String detail; if (seconds <= 0) detail = "With the current delay of 0 seconds, alarms trigger immediately"; - else - { + else { final String hhmmss = SecondsParser.formatSeconds(seconds); detail = "With the current delay of " + seconds + " seconds, alarms trigger after " + hhmmss + " hours:minutes:seconds"; } @@ -247,7 +233,7 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) // Scroll pane stops the content from resizing, // so tell content to use the widths of the scroll pane // minus 40 to provide space for the scroll bar, and suggest minimum width - scroll.widthProperty().addListener((p, old, width) -> layout.setPrefWidth(Math.max(width.doubleValue()-40, 450))); + scroll.widthProperty().addListener((p, old, width) -> layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); getDialogPane().setContent(scroll); setResizable(true); @@ -256,25 +242,22 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) final Button ok = (Button) getDialogPane().lookupButton(ButtonType.OK); ok.addEventFilter(ActionEvent.ACTION, event -> - { - if (!validateAndStore(model, item)) - event.consume(); - }); + validateAndStore(model, item, event)); setResultConverter(button -> button == ButtonType.OK); } - /** Send requested configuration - * @param model {@link AlarmClient} - * @param item Original item - * @return true on success + /** + * Send requested configuration + * + * @param model {@link AlarmClient} + * @param item Original item + * @param event Button click event, consumed if save action fails (e.g. Kafka not reachable) */ - private boolean validateAndStore(final AlarmClient model, final AlarmTreeItem item) - { + private void validateAndStore(final AlarmClient model, final AlarmTreeItem item, ActionEvent event) { final AlarmTreeItem config; - if (item instanceof AlarmClientLeaf) - { + if (item instanceof AlarmClientLeaf) { final AlarmClientLeaf pv = new AlarmClientLeaf(null, item.getName()); pv.setDescription(description.getText().trim()); pv.setEnabled(enabled.isSelected()); @@ -294,31 +277,25 @@ private boolean validateAndStore(final AlarmClient model, final AlarmTreeItem else pv.setEnabled(true); - if (relative_enable_date != null) - { + if (relative_enable_date != null) { final TemporalAmount amount = TimeParser.parseTemporalAmount(relative_enable_date); final LocalDateTime update_date = LocalDateTime.now().plus(amount); pv.setEnabledDate(update_date); - }; + } + ; config = pv; - } - else + } else config = new AlarmClientNode(null, item.getName()); config.setGuidance(guidance.getItems()); config.setDisplays(displays.getItems()); config.setCommands(commands.getItems()); config.setActions(actions.getItems()); - try - { + try { model.sendItemConfigurationUpdate(item.getPathName(), config); - } - catch (Exception ex) - { + } catch (Exception ex) { ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); - return false; + event.consume(); } - - return true; } } \ No newline at end of file diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java index 21d381fe64..6e06d30a0f 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -92,7 +92,7 @@ public List getItems() class DelayTableCell extends TableCell { private final Spinner spinner; - + public DelayTableCell() { this.spinner = new Spinner<>(0, 10000, 1); @@ -124,7 +124,7 @@ public void updateItem(Integer item, boolean empty) spinner.getEditor().setStyle("-fx-text-inner-color: lightgray;"); //spinner.getEditor().setTextFill(Color.LIGHTGRAY); } - + this.spinner.getValueFactory().setValue(item); setGraphic(spinner); } @@ -215,36 +215,46 @@ private void createTable() } /** - * This function extract the command option from detail "option:info" - * + * This function extracts the option from detail "option:info" + * * @param titleDetailDelay - * @return enum Option_d either mailto or cmd + * @return enum Option_d (mailto, cmd, sevrpv) */ - private Option_d getOptionFromDetail(TitleDetailDelay titleDetailDelay) { - Option_d option = null; - String detail = titleDetailDelay != null ? titleDetailDelay.detail : null; - String[] split = detail != null ? detail.split(":") : null; - String optionString = split != null && split.length > 0 ? split[0] : null; - try { - option = optionString != null ? Option_d.valueOf(optionString) : Option_d.mailto; - } catch (Exception e) { - option = Option_d.mailto; + private Option_d getOptionFromDetail(final TitleDetailDelay titleDetailDelay) + { + if (titleDetailDelay == null) + return null; + + final int sep = titleDetailDelay.detail.indexOf(':'); + if (sep < 0) + return Option_d.mailto; + + try + { + return Option_d.valueOf(titleDetailDelay.detail.substring(0, sep)); + } + catch (Exception e) + { + return Option_d.mailto; } - return option; } /** - * This function extract the info from detail "option:info" - * + * This function extracts the info from detail "option:info" + * * @param titleDetailDelay - * @return information eg : mail or command + * @return information eg : mail, command, PV */ - private String getInfoFromDetail(TitleDetailDelay titleDetailDelay) { - String info = ""; - String detail = titleDetailDelay != null ? titleDetailDelay.detail : null; - String[] split = detail != null ? detail.split(":") : null; - info = split != null && split.length > 1 ? split[1] : ""; - return info; + private String getInfoFromDetail(final TitleDetailDelay titleDetailDelay) + { + if (titleDetailDelay == null) + return ""; + + final int sep = titleDetailDelay.detail.indexOf(':'); + if (sep < 0) + return ""; + + return titleDetailDelay.detail.substring(sep+1); } /** diff --git a/app/alarm/ui/src/main/resources/icons/expand_disabled.png b/app/alarm/ui/src/main/resources/icons/expand_disabled.png new file mode 100644 index 0000000000..c5f800b0a5 Binary files /dev/null and b/app/alarm/ui/src/main/resources/icons/expand_disabled.png differ diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index 998f2ba791..2ed7ab7472 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -20,6 +20,8 @@ error=Error acknowledgeFailed=Failed to acknowledge alarm(s) addComponentFailed=Failed to add component +disableAlarmFailed=Failed to disable alarm +enableAlarmFailed=Failed to enable alarm moveItemFailed=Failed to move item removeComponentFailed=Failed to remove component renameItemFailed=Failed to rename item diff --git a/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/TreeItemUpdateDemo.java b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/TreeItemUpdateDemo.java new file mode 100644 index 0000000000..a38b38b98e --- /dev/null +++ b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/TreeItemUpdateDemo.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2023 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.phoebus.applications.alarm; + +import org.phoebus.ui.javafx.ApplicationWrapper; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +/** Demo of alarm tree cell update + * + * Demonstrates refresh issue for tree cells that are + * not visible because they scrolled below the window bottom. + * + * Only replacing the TreeItem results in reliable tree updates. + * + * 1) Start this program, click the "On" and "Off" buttons, and observe how item "Two" is updated + * 2) Click "Off", reduce the window height so that the items under "Top" are hidden, + * click "On", increase window height to again show all items. + * Would expect to see "On", but tree still shows "Off" + * until the tree is collapsed and again expanded. + * 3) Click "On", reduce the window height so that the items under "Top" are hidden, + * click "Off", increase window height to again show all items. + * Tree should indeed show "Off". + * + * Conclusion is that TreeItem.setValue() does not trigger updates of certain hidden items. + * Basic google search suggests that the value must change, but "On" and "Off" + * are different object references and they are not 'equal'. + * + * When instead replacing the complete TreeItem, the tree is always updated. + * + * Tested with JavaFX 15, 19, 20 + * + * @author Kay Kasemir + */ +public class TreeItemUpdateDemo extends ApplicationWrapper +{ + private TreeView tree_view = new TreeView<>(); + + @Override + public void start(final Stage stage) + { + final Button on = new Button("On"); + on.setOnAction(event -> + { // Just updating the value of TreeItem is ignored for hidden items? + tree_view.getRoot().getChildren().get(1).setValue("On"); + }); + final Button off = new Button("Off"); + off.setOnAction(event -> + { // Replacing the TreeItem always "works"? + // (Note this would be more work for intermediate items + // that have child nodes, since child nodes need to be moved/copied) + tree_view.getRoot().getChildren().set(1, new TreeItem<>("Off")); + }); + final HBox bottom = new HBox(on, off); + on.setMaxWidth(1000); + off.setMaxWidth(1000); + HBox.setHgrow(on, Priority.ALWAYS); + HBox.setHgrow(off, Priority.ALWAYS); + + final VBox root = new VBox(tree_view, bottom); + VBox.setVgrow(tree_view, Priority.ALWAYS); + + TreeItem top = new TreeItem<>("Top"); + tree_view.setRoot(top); + + top.getChildren().add(new TreeItem<>("One")); + top.getChildren().add(new TreeItem<>("Two")); + top.getChildren().add(new TreeItem<>("Three")); + top.setExpanded(true); + + final Scene scene = new Scene(root, 300, 300); + stage.setScene(scene); + stage.show(); + } + + public static void main(final String[] args) + { + launch(TreeItemUpdateDemo.class, args); + } +} diff --git a/app/channel/channelfinder/pom.xml b/app/channel/channelfinder/pom.xml index 3760c6d727..03ab9ec901 100644 --- a/app/channel/channelfinder/pom.xml +++ b/app/channel/channelfinder/pom.xml @@ -3,19 +3,19 @@ org.phoebus app-channel - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-channel-channelfinder org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT com.fasterxml.jackson.core diff --git a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClient.java b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClient.java index aa6f38a3fa..77a1c7eb9a 100644 --- a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClient.java +++ b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClient.java @@ -55,17 +55,17 @@ public interface ChannelFinderClient { /** * Returns a channel that exactly matches the channelName - * channelName. + * channelName. * * @param channelName * - name of the required channel. - * @return {@link Channel} with name channelName or null + * @return {@link Channel} with name channelName or null * @throws ChannelFinderException - channelfinder exception */ public Channel getChannel(String channelName) throws ChannelFinderException; /** - * Destructively set a single channel channel, if the channel + * Destructively set a single channel channel, if the channel * already exists it will be replaced with the given channel. * * @param channel @@ -76,7 +76,7 @@ public interface ChannelFinderClient { /** - * Destructively set a Tag tag with no associated channels to the + * Destructively set a Tag tag with no associated channels to the * database. * * @param tag @@ -85,7 +85,7 @@ public interface ChannelFinderClient { public void set(Tag.Builder tag); /** - * Destructively set tag tag to channel channelName and + * Destructively set tag tag to channel channelName and * remove the tag from all other channels. * * @param tag @@ -98,7 +98,7 @@ public void set(Tag.Builder tag, String channelName) throws ChannelFinderException; /** - * Set tag tag on the set of channels {channels} and remove it from + * Set tag tag on the set of channels {channels} and remove it from * all others. * * @param tag @@ -112,7 +112,7 @@ public void set(Tag.Builder tag, Collection channelNames) throws ChannelFinderException; /** - * Destructively set a new property property. + * Destructively set a new property property. * * @param prop * - the property to be set. @@ -120,8 +120,8 @@ public void set(Tag.Builder tag, Collection channelNames) public void set(Property.Builder prop) throws ChannelFinderException; /** - * Destructively set property prop and add it to the channel - * channelName and remove it from all others. + * Destructively set property prop and add it to the channel + * channelName and remove it from all others. * * @param prop * - property to be set. @@ -131,10 +131,10 @@ public void set(Tag.Builder tag, Collection channelNames) public void set(Property.Builder prop, String channelName); /** - * Destructively set property prop and add it to the channels - * channelNames removing it from all other channels. By default all + * Destructively set property prop and add it to the channels + * channelNames removing it from all other channels. By default all * channels will contain the property with the same value specified in the - * prop.
+ * prop.
* to individually set the value for each channel use channelPropertyMap. * * @param prop @@ -146,8 +146,8 @@ public void set(Tag.Builder tag, Collection channelNames) public void set(Property.Builder prop, Collection channelNames); /** - * Destructively set the property prop and add it to the channels - * specified in the channelPropertyMap, where the map key is the + * Destructively set the property prop and add it to the channels + * specified in the channelPropertyMap, where the map key is the * channel name and the associated value is the property value to be used * for that channel. * @@ -160,7 +160,7 @@ public void set(Property.Builder prop, Map channelPropertyMap); /** - * Update existing channel with channel. + * Update existing channel with channel. * * @param channel - channel builder * @throws ChannelFinderException - channelfinder exception @@ -168,8 +168,8 @@ public void set(Property.Builder prop, public void update(Channel.Builder channel) throws ChannelFinderException; /** - * Update Tag tag by adding it to Channel with name - * channelName, without affecting the other instances of this tag. + * Update Tag tag by adding it to Channel with name + * channelName, without affecting the other instances of this tag. * * @param tag * the tag to be added @@ -182,8 +182,8 @@ public void update(Tag.Builder tag, String channelName) /** * - * Update the Tag tag by adding it to the set of the channels with - * names channelNames, without affecting the other instances of + * Update the Tag tag by adding it to the set of the channels with + * names channelNames, without affecting the other instances of * this tag. * * @param tag @@ -196,8 +196,8 @@ public void update(Tag.Builder tag, Collection channelNames) throws ChannelFinderException; /** - * Update Property property by adding it to the channel - * channelName, without affecting the other channels. + * Update Property property by adding it to the channel + * channelName, without affecting the other channels. * * @param property * - the property to be updated @@ -210,8 +210,8 @@ public void update(Property.Builder property, String channelName) throws ChannelFinderException; /** - * Update the channels identified with channelNames with the - * property property + * Update the channels identified with channelNames with the + * property property * * @param property - property builder * @param channelNames - list of channel names @@ -221,7 +221,7 @@ public void update(Property.Builder property, Collection channelNames) throws ChannelFinderException; /** - * Update the property property on all channels specified in the + * Update the property property on all channels specified in the * channelPropValueMap, where the key in the map is the channel name and the * value is the value for that property * @@ -234,13 +234,13 @@ public void update(Property.Builder property, throws ChannelFinderException; /** - * Search for channels who's name match the pattern pattern.
+ * Search for channels who's name match the pattern pattern.
* The pattern can contain wildcard char * or ?.
* * @param pattern * - the search pattern for the channel names * @return A Collection of channels who's name match the pattern - * pattern + * pattern * @throws ChannelFinderException - channelfinder exception */ public Collection findByName(String pattern) @@ -248,13 +248,13 @@ public Collection findByName(String pattern) /** * Search for channels with tags who's name match the pattern - * pattern.
+ * pattern.
* The pattern can contain wildcard char * or ?.
* * @param pattern * - the search pattern for the tag names * @return A Collection of channels which contain tags who's name match the - * pattern pattern + * pattern pattern * @throws ChannelFinderException - channelfinder exception */ public Collection findByTag(String pattern) @@ -262,7 +262,7 @@ public Collection findByTag(String pattern) /** * Search for channels with properties who's Value match the pattern - * pattern.
+ * pattern.
* The pattern can contain wildcard char * or ?.
* * @param property @@ -270,8 +270,8 @@ public Collection findByTag(String pattern) * @param pattern * - the seatch pattern for the property value. * @return A collection of channels containing the property with name - * propertyName who's value matches the pattern - * pattern. + * propertyName who's value matches the pattern + * pattern. * @throws ChannelFinderException - channelfinder exception */ public Collection findByProperty(String property, @@ -283,7 +283,7 @@ public Collection findByProperty(String property, * Tags=tagNamePattern Each criteria is logically ANDed, || seperated values * are logically ORed * - * Query for channels based on the Query string query example: + * Query for channels based on the Query string query example: * find("SR* Cell=1,2 Tags=GolderOrbit,myTag)
* * this will return all channels with names starting with SR AND have @@ -356,7 +356,7 @@ public void deleteProperty(String propertyName) throws ChannelFinderException; /** - * Delete the channel identified by channel + * Delete the channel identified by channel * * @param channelName * channel to be removed @@ -365,7 +365,7 @@ public void deleteProperty(String propertyName) public void deleteChannel(String channelName) throws ChannelFinderException; /** - * Delete the set of channels identified by channels + * Delete the set of channels identified by channels * * @param channels - list of channel builders * @throws ChannelFinderException - channelfinder exception @@ -375,53 +375,53 @@ public void delete(Collection channels) throws ChannelFinderException; /** - * Delete tag tag from the channel with the name - * channelName + * Delete tag tag from the channel with the name + * channelName * * @param tag * - the tag to be deleted. * @param channelName - * - the channel from which to delete the tag tag + * - the channel from which to delete the tag tag * @throws ChannelFinderException - channelfinder exception */ public void delete(Tag.Builder tag, String channelName) throws ChannelFinderException; /** - * Remove the tag tag from all the channels channelNames + * Remove the tag tag from all the channels channelNames * * @param tag * - the tag to be deleted. * @param channelNames - * - the channels from which to delete the tag tag + * - the channels from which to delete the tag tag * @throws ChannelFinderException - channelfinder exception */ public void delete(Tag.Builder tag, Collection channelNames) throws ChannelFinderException; /** - * Remove property property from the channel with name - * channelName + * Remove property property from the channel with name + * channelName * * @param property * - the property to be deleted. * @param channelName * - the channel from which to delete the property - * property + * property * @throws ChannelFinderException - channelfinder exception */ public void delete(Property.Builder property, String channelName) throws ChannelFinderException; /** - * Remove the property property from the set of channels - * channelNames + * Remove the property property from the set of channels + * channelNames * * @param property * - the property to be deleted. * @param channelNames * - the channels from which to delete the property - * property + * property * @throws ChannelFinderException - channelfinder exception */ public void delete(Property.Builder property, diff --git a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClientImpl.java b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClientImpl.java index 3d53bd8f86..b833bd9bca 100644 --- a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClientImpl.java +++ b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelFinderClientImpl.java @@ -128,7 +128,7 @@ public static CFCBuilder serviceURL() } /** - * Creates a {@link CFCBuilder} for a CF client to URI uri. + * Creates a {@link CFCBuilder} for a CF client to URI uri. * * @param uri - service uri * @return {@link CFCBuilder} @@ -140,7 +140,7 @@ public static CFCBuilder serviceURL(String uri) /** * Creates a {@link CFCBuilder} for a CF client to {@link URI} - * uri. + * uri. * * @param uri - service uri * @return {@link CFCBuilder} @@ -423,10 +423,10 @@ public static void resetPreferences() { /** * Returns a channel that exactly matches the channelName - * channelName. + * channelName. * * @param channelName - name of the required channel. - * @return {@link Channel} with name channelName or null + * @return {@link Channel} with name channelName or null * @throws ChannelFinderException - channelfinder exception */ public Channel getChannel(String channelName) throws ChannelFinderException { @@ -474,7 +474,7 @@ public Channel call() throws UniformInterfaceException } /** - * Destructively set a single channel channel, if the channel + * Destructively set a single channel channel, if the channel * already exists it will be replaced with the given channel. * * @param channel the channel to be added @@ -546,7 +546,7 @@ public void run() { } /** - * Destructively set a Tag tag with no associated channels to the + * Destructively set a Tag tag with no associated channels to the * database. * * @param tag - the tag to be set. @@ -556,7 +556,7 @@ public void set(Tag.Builder tag) { } /** - * Destructively set tag tag to channel channelName and + * Destructively set tag tag to channel channelName and * remove the tag from all other channels. * * @param tag - the tag to be set. @@ -570,7 +570,7 @@ public void set(Tag.Builder tag, String channelName) throws ChannelFinderExcepti } /** - * Set tag tag on the set of channels {channels} and remove it from + * Set tag tag on the set of channels {channels} and remove it from * all others. * * @param tag - the tag to be set. @@ -636,7 +636,7 @@ public void run() { } /** - * Destructively set a new property property. + * Destructively set a new property property. * * @param prop - the property to be set. */ @@ -645,8 +645,8 @@ public void set(Property.Builder prop) throws ChannelFinderException { } /** - * Destructively set property prop and add it to the channel - * channelName and remove it from all others. + * Destructively set property prop and add it to the channel + * channelName and remove it from all others. * * @param prop - property to be set. * @param channelName - the channel to which this property must be added. @@ -658,10 +658,10 @@ public void set(Property.Builder prop, String channelName) { } /** - * Destructively set property prop and add it to the channels - * channelNames removing it from all other channels. By default all + * Destructively set property prop and add it to the channels + * channelNames removing it from all other channels. By default all * channels will contain the property with the same value specified in the - * prop.
+ * prop.
* to individually set the value for each channel use channelPropertyMap. * * @param prop - the property to be set. @@ -673,8 +673,8 @@ public void set(Property.Builder prop, Collection channelNames) { } /** - * Destructively set the property prop and add it to the channels - * specified in the channelPropertyMap, where the map key is the + * Destructively set the property prop and add it to the channels + * specified in the channelPropertyMap, where the map key is the * channel name and the associated value is the property value to be used * for that channel. * @@ -733,7 +733,7 @@ public void run() { } /** - * Update existing channel with channel. + * Update existing channel with channel. * * @param channel - channel builder * @throws ChannelFinderException - channelfinder exception @@ -768,8 +768,8 @@ public void run() { } /** - * Update Tag tag by adding it to Channel with name - * channelName, without affecting the other instances of this tag. + * Update Tag tag by adding it to Channel with name + * channelName, without affecting the other instances of this tag. * * @param tag the tag to be added * @param channelName Name of the channel to which the tag is to be added @@ -780,8 +780,8 @@ public void update(Tag.Builder tag, String channelName) throws ChannelFinderExce } /** - * Update the Tag tag by adding it to the set of the channels with - * names channelNames, without affecting the other instances of + * Update the Tag tag by adding it to the set of the channels with + * names channelNames, without affecting the other instances of * this tag. * * @param tag - the tag that needs to be updated. @@ -836,8 +836,8 @@ public void run() { } /** - * Update Property property by adding it to the channel - * channelName, without affecting the other channels. + * Update Property property by adding it to the channel + * channelName, without affecting the other channels. * * @param property - the property to be updated * @param channelName - the channel to which this property should be added or @@ -952,12 +952,12 @@ public void run() { } /** - * Search for channels who's name match the pattern pattern.
+ * Search for channels who's name match the pattern pattern.
* The pattern can contain wildcard char * or ?.
* * @param pattern - the search pattern for the channel names * @return A Collection of channels who's name match the pattern - * pattern + * pattern * @throws ChannelFinderException - channelfinder exception */ public Collection findByName(String pattern) throws ChannelFinderException { @@ -969,12 +969,12 @@ public Collection findByName(String pattern) throws ChannelFinderExcept /** * Search for channels with tags who's name match the pattern - * pattern.
+ * pattern.
* The pattern can contain wildcard char * or ?.
* * @param pattern - the search pattern for the tag names * @return A Collection of channels which contain tags who's name match the - * pattern pattern + * pattern pattern * @throws ChannelFinderException - channelfinder exception */ public Collection findByTag(String pattern) throws ChannelFinderException { @@ -989,14 +989,14 @@ public Collection findByTag(String pattern) throws ChannelFinderExcepti /** * Search for channels with properties who's Value match the pattern - * pattern.
+ * pattern.
* The pattern can contain wildcard char * or ?.
* * @param property - the name of the property. * @param pattern - the seatch pattern for the property value. * @return A collection of channels containing the property with name - * propertyName who's value matches the pattern - * pattern. + * propertyName who's value matches the pattern + * pattern. * @throws ChannelFinderException - channelfinder exception */ public Collection findByProperty(String property, String... pattern) throws ChannelFinderException { @@ -1010,7 +1010,7 @@ public Collection findByProperty(String property, String... pattern) th } /** - * Query for channels based on the Query string query example: + * Query for channels based on the Query string query example: * find("SR* Cell=1,2 Tags=GolderOrbit,myTag)
*

* this will return all channels with names starting with SR AND have @@ -1097,12 +1097,12 @@ public Collection call() throws Exception { } catch (Exception e) { log.log(Level.WARNING, "Error creating channels:", e); } - log.log(Level.FINE, "Finished mapping to xml : " + String.valueOf(System.currentTimeMillis()-start)); + log.log(Level.FINE, "Finished mapping to xml. (Time: " + String.valueOf(System.currentTimeMillis()-start) + " ms)"); start = System.currentTimeMillis(); for (XmlChannel xmlchannel : xmlchannels) { channels.add(new Channel(xmlchannel)); } - log.log(Level.FINE, "Finished creating new channels : " + String.valueOf(System.currentTimeMillis()-start)); + log.log(Level.FINE, "Finished creating new channels. (Time: " + String.valueOf(System.currentTimeMillis()-start) + " ms)"); return Collections.unmodifiableCollection(channels); } } @@ -1175,7 +1175,7 @@ public void deleteProperty(String propertyName) throws ChannelFinderException } /** - * Delete the channel identified by channel + * Delete the channel identified by channel * * @param channelName - channel to be removed * @throws ChannelFinderException - channelfinder exception @@ -1204,7 +1204,7 @@ public void run() { } /** - * Delete the set of channels identified by channels + * Delete the set of channels identified by channels * * @param channels - channels to be deleted * @throws ChannelFinderException - throws exception @@ -1218,11 +1218,11 @@ public void delete(Collection channels) throws ChannelFinderExc } /** - * Delete tag tag from the channel with the name - * channelName + * Delete tag tag from the channel with the name + * channelName * * @param tag - the tag to be deleted. - * @param channelName - the channel from which to delete the tag tag + * @param channelName - the channel from which to delete the tag tag * @throws ChannelFinderException - throws exception */ public void delete(Tag.Builder tag, String channelName) throws ChannelFinderException @@ -1232,10 +1232,10 @@ public void delete(Tag.Builder tag, String channelName) throws ChannelFinderExce } /** - * Remove the tag tag from all the channels channelNames + * Remove the tag tag from all the channels channelNames * * @param tag - the tag to be deleted. - * @param channelNames - the channels from which to delete the tag tag + * @param channelNames - the channels from which to delete the tag tag * @throws ChannelFinderException - throws exception */ public void delete(Tag.Builder tag, Collection channelNames) throws ChannelFinderException @@ -1247,12 +1247,12 @@ public void delete(Tag.Builder tag, Collection channelNames) throws Chan } /** - * Remove property property from the channel with name - * channelName + * Remove property property from the channel with name + * channelName * * @param property - the property to be deleted. * @param channelName - the channel from which to delete the property - * property + * property * @throws ChannelFinderException - throws exception */ public void delete(Property.Builder property, String channelName) throws ChannelFinderException @@ -1261,12 +1261,12 @@ public void delete(Property.Builder property, String channelName) throws Channel } /** - * Remove the property property from the set of channels - * channelNames + * Remove the property property from the set of channels + * channelNames * * @param property - the property to be deleted. * @param channelNames - the channels from which to delete the property - * property + * property * @throws ChannelFinderException - throws exception */ public void delete(Property.Builder property, Collection channelNames) throws ChannelFinderException diff --git a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelUtil.java b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelUtil.java index 308d89c39b..d1b8f8bdd1 100644 --- a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelUtil.java +++ b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/ChannelUtil.java @@ -117,7 +117,7 @@ public static Collection getChannelNames(Collection channels) { /** * Given a Collection of channels returns a new collection of channels * containing only those channels which have all the properties in the - * propNames + * propNames * * @param channels * - the input list of channels @@ -139,7 +139,7 @@ public static Collection filterbyProperties(Collection channel /** * Given a Collection of channels returns a new collection of channels * containing only those channels which have all the tags in the - * tagNames + * tagNames * * @param channels * - the input list of channels @@ -161,7 +161,7 @@ public static Collection filterbyTags(Collection channels, Col /** * Given a Collection of channels returns a new collection of channels * containing only those channels which have all the tags in the - * tagNames + * tagNames * * @param channels * - the input list of channels diff --git a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Property.java b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Property.java index 2e246d3f43..001bceb07d 100644 --- a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Property.java +++ b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Property.java @@ -37,7 +37,7 @@ public static Builder property(String name) { /** * Returns a {@link Property.Builder} to create a property with given. - * name and value + * name and value * * @param name - property name * @param value - property value @@ -52,10 +52,10 @@ public static Builder property(String name, String value) { /** - * Returns a {@link Property.Builder} to for a property which is a copy of property. + * Returns a {@link Property.Builder} to for a property which is a copy of property. * * @param property - the property to be copied - * @return {@link Property.Builder} with attributes initialized to the same as property + * @return {@link Property.Builder} with attributes initialized to the same as property */ public static Builder property(Property property) { Builder propertyBuilder = new Builder(); @@ -69,7 +69,7 @@ public static Builder property(Property property) { * Set the owner for the property to be built. * * @param owner - owner id - * @return property {@link Builder} with owner set to owner + * @return property {@link Builder} with owner set to owner */ public Builder owner(String owner) { this.owner = owner; @@ -80,7 +80,7 @@ public Builder owner(String owner) { * Set the value for the property to be built. * * @param value - property value - * @return property {@link Builder} with value set to value + * @return property {@link Builder} with value set to value */ public Builder value(String value) { this.value = value; diff --git a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Tag.java b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Tag.java index 00ce2abcae..682b7a9445 100644 --- a/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Tag.java +++ b/app/channel/channelfinder/src/main/java/org/phoebus/channelfinder/Tag.java @@ -30,12 +30,12 @@ public static class Builder { /** * Returns a {@link Builder} to build a {@link Tag} which is a copy of - * the given tag. + * the given tag. * * @param tag * - the tag to be copied * @return tag {@link Builder} with attributes initialized to the same - * as tag + * as tag */ public static Builder tag(Tag tag) { Builder builder = new Builder(); @@ -46,11 +46,11 @@ public static Builder tag(Tag tag) { /** * Returns a tag {@link Builder} to build a {@link Tag} with the name - * name + * name * * @param name * - tag name - * @return tag {@link Builder} with name name + * @return tag {@link Builder} with name name */ public static Builder tag(String name) { Builder builder = new Builder(); @@ -60,14 +60,14 @@ public static Builder tag(String name) { /** * Returns a tag {@link Builder} to build a {@link Tag} with the name - * name and owner owner + * name and owner owner * * @param name * - the tag name * @param owner * - the tag owner id - * @return A {@link Builder} with name set to name and owner id - * set to owner + * @return A {@link Builder} with name set to name and owner id + * set to owner */ public static Builder tag(String name, String owner) { Builder builder = new Builder(); @@ -80,7 +80,7 @@ public static Builder tag(String name, String owner) { * Set the owner for the tag to be built. * * @param owner - owner id - * @return tag {@link Builder} with owner set to owner + * @return tag {@link Builder} with owner set to owner */ public Builder owner(String owner) { this.owner = owner; diff --git a/app/channel/pom.xml b/app/channel/pom.xml index 40035bccc7..ece695b119 100644 --- a/app/channel/pom.xml +++ b/app/channel/pom.xml @@ -3,7 +3,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-channel pom diff --git a/app/channel/utility/pom.xml b/app/channel/utility/pom.xml index 9d0e43de11..2438e9d699 100644 --- a/app/channel/utility/pom.xml +++ b/app/channel/utility/pom.xml @@ -3,34 +3,34 @@ org.phoebus app-channel - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-channel-utility org.phoebus app-channel-channelfinder - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/channel/views/pom.xml b/app/channel/views/pom.xml index ececd2651a..42f2da7fdf 100644 --- a/app/channel/views/pom.xml +++ b/app/channel/views/pom.xml @@ -3,39 +3,39 @@ org.phoebus app-channel - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-channel-views org.phoebus app-channel-channelfinder - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-channel-utility - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelInfo.java b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelInfo.java index 842941e42a..8275180642 100644 --- a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelInfo.java +++ b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelInfo.java @@ -99,7 +99,7 @@ public void call(Selection selection) throws Exception result -> Platform.runLater(() -> { controller.addChannels(result); }), - (url, ex) -> ExceptionDetailsErrorDialog.openError("ChannelFinder Query Error", ex.getMessage(), ex)); + (url, ex) -> ExceptionDetailsErrorDialog.openError("ChannelFinder Query Error", ex)); }); diff --git a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTableApp.java b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTableApp.java index c5a9ced1f8..fbb10943f0 100644 --- a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTableApp.java +++ b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTableApp.java @@ -48,7 +48,7 @@ public boolean canOpenResource(String resource) { } /** - * Support the launching of channeltable using resource cf://? + * Support the launching of channeltable using resource {@literal cf://?} * e.g. * -resource cf://?query=SR* */ diff --git a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTreeApp.java b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTreeApp.java index eb6d058016..fac447db80 100644 --- a/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTreeApp.java +++ b/app/channel/views/src/main/java/org/phoebus/channel/views/ChannelTreeApp.java @@ -49,7 +49,7 @@ public boolean canOpenResource(String resource) { } /** - * Support the launching of channeltable using resource cf://? + * Support the launching of channeltable using resource {@literal cf://?} * e.g. * -resource cf://?query=SR* */ diff --git a/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelFinderController.java b/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelFinderController.java index 16520f7489..67d056b6c0 100644 --- a/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelFinderController.java +++ b/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelFinderController.java @@ -41,7 +41,7 @@ public void search(String searchString) { } channelSearchJob = ChannelSearchJob.submit(this.client, searchString, channels -> Platform.runLater(() -> setChannels(channels)), - (url, ex) -> ExceptionDetailsErrorDialog.openError("ChannelFinder Query Error", ex.getMessage(), ex)); + (url, ex) -> ExceptionDetailsErrorDialog.openError("ChannelFinder Query Error", ex)); } diff --git a/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelTreeByPropertyNode.java b/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelTreeByPropertyNode.java index 6276ca4fae..8c61f88fa2 100644 --- a/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelTreeByPropertyNode.java +++ b/app/channel/views/src/main/java/org/phoebus/channel/views/ui/ChannelTreeByPropertyNode.java @@ -52,7 +52,6 @@ public class ChannelTreeByPropertyNode { * @param model * @param parentNode * @param displayName - * @param connect */ public ChannelTreeByPropertyNode(ChannelTreeByPropertyModel model, ChannelTreeByPropertyNode parentNode, String displayName) { this.model = model; diff --git a/app/console/pom.xml b/app/console/pom.xml index eba4033d37..3efbfee464 100644 --- a/app/console/pom.xml +++ b/app/console/pom.xml @@ -4,18 +4,18 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/credentials-management/pom.xml b/app/credentials-management/pom.xml index aab2db40a2..2527e1b099 100644 --- a/app/credentials-management/pom.xml +++ b/app/credentials-management/pom.xml @@ -21,7 +21,7 @@ app org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -31,12 +31,12 @@ org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT \ No newline at end of file diff --git a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementApp.java b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementApp.java index 55700d2d86..4f55f13e5f 100644 --- a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementApp.java +++ b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementApp.java @@ -60,7 +60,8 @@ public String getDisplayName() { @Override public AppInstance create() { List authenticationProviders = - ServiceLoader.load(ServiceAuthenticationProvider.class).stream().map(Provider::get).collect(Collectors.toList()); + ServiceLoader.load(ServiceAuthenticationProvider.class).stream().map(Provider::get) + .collect(Collectors.toList()); try { SecureStore secureStore = new SecureStore(); new CredentialsManagementStage(authenticationProviders, secureStore).show(); diff --git a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementController.java b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementController.java index 69a07c0a5c..1aa73f5786 100644 --- a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementController.java +++ b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementController.java @@ -31,10 +31,13 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.stage.Stage; import javafx.util.Callback; import org.phoebus.framework.jobs.JobManager; import org.phoebus.security.authorization.ServiceAuthenticationProvider; import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -64,6 +67,9 @@ public class CredentialsManagementController { @FXML private Button clearAllCredentialsButton; + @FXML + private TableColumn scopeColumn; + private final SimpleBooleanProperty listEmpty = new SimpleBooleanProperty(true); private final ObservableList serviceItems = FXCollections.observableArrayList(); @@ -71,6 +77,8 @@ public class CredentialsManagementController { private static final Logger LOGGER = Logger.getLogger(CredentialsManagementController.class.getName()); private final List authenticationProviders; + private Stage stage; + public CredentialsManagementController(List authenticationProviders, SecureStore secureStore) { this.authenticationProviders = authenticationProviders; this.secureStore = secureStore; @@ -95,7 +103,7 @@ public TableCell call(final TableColumn pa login(serviceItem); } else{ - logOut(serviceItem.getScope()); + logOut(serviceItem.getAuthenticationScope()); } }); } @@ -135,24 +143,29 @@ public void logOutFromAll() { } } + /** + * Attempts to sign in user based on provided credentials. If sign-in succeeds, this method will close the + * associated UI. + * @param serviceItem The {@link ServiceItem} defining the scope, and implicitly the authentication service. + */ private void login(ServiceItem serviceItem){ try { serviceItem.getServiceAuthenticationProvider().authenticate(serviceItem.getUsername(), serviceItem.getPassword()); try { - secureStore.setScopedAuthentication(new ScopedAuthenticationToken(serviceItem.getScope(), + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(serviceItem.getAuthenticationScope(), serviceItem.getUsername(), serviceItem.getPassword())); + stage.close(); } catch (Exception exception) { LOGGER.log(Level.WARNING, "Failed to store credentials", exception); } - updateTable(); } catch (Exception exception) { LOGGER.log(Level.WARNING, "Failed to login to service", exception); ExceptionDetailsErrorDialog.openError(parent, "Login Failure", "Failed to login to service", exception); } } - private void logOut(String scope) { + private void logOut(AuthenticationScope scope) { try { secureStore.deleteScopedAuthenticationToken(scope); updateTable(); @@ -168,19 +181,19 @@ private void updateTable() { // Match saved tokens with an authentication provider, where applicable List serviceItems = savedTokens.stream().map(token -> { ServiceAuthenticationProvider provider = - authenticationProviders.stream().filter(p-> p.getServiceName().equals(token.getScope())).findFirst().orElse(null); + authenticationProviders.stream().filter(p-> p.getAuthenticationScope().equals(token.getAuthenticationScope())).findFirst().orElse(null); return new ServiceItem(provider, token.getUsername(), token.getPassword()); }).collect(Collectors.toList()); // Also need to add ServiceItems for providers not matched with a saved token, i.e. for logged-out services - authenticationProviders.stream().forEach(p -> { + authenticationProviders.forEach(p -> { Optional serviceItem = serviceItems.stream().filter(si -> - p.getServiceName().equals(si.getScope())).findFirst(); + p.getAuthenticationScope().equals(si.getAuthenticationScope())).findFirst(); if(serviceItem.isEmpty()){ serviceItems.add(new ServiceItem(p)); } }); - serviceItems.sort(Comparator.comparing(ServiceItem::getScope)); + serviceItems.sort(Comparator.comparing(ServiceItem::getAuthenticationScope)); Platform.runLater(() -> { this.serviceItems.setAll(serviceItems); listEmpty.set(savedTokens.isEmpty()); @@ -193,7 +206,7 @@ private void updateTable() { * Model class for the table view */ public static class ServiceItem { - private ServiceAuthenticationProvider serviceAuthenticationProvider; + private final ServiceAuthenticationProvider serviceAuthenticationProvider; private String username; private String password; private boolean loginAction = false; @@ -217,9 +230,18 @@ public void setUsername(String username){ this.username = username; } - public String getScope() { + public AuthenticationScope getAuthenticationScope() { return serviceAuthenticationProvider != null ? - serviceAuthenticationProvider.getServiceName() : ""; + serviceAuthenticationProvider.getAuthenticationScope() : null; + } + + /** + * @return String representation of the authentication scope. + */ + @SuppressWarnings("unused") + public String getScope(){ + return serviceAuthenticationProvider != null ? + serviceAuthenticationProvider.getAuthenticationScope().getName() : ""; } public String getPassword(){ @@ -239,7 +261,7 @@ public boolean isLoginAction(){ } } private class UsernameTableCell extends TableCell{ - private TextField textField = new TextField(); + private final TextField textField = new TextField(); public UsernameTableCell(){ textField.getStyleClass().add("text-field-styling"); @@ -266,7 +288,7 @@ protected void updateItem(String item, final boolean empty) } private class PasswordTableCell extends TableCell{ - private PasswordField passwordField = new PasswordField(); + private final PasswordField passwordField = new PasswordField(); public PasswordTableCell(){ passwordField.getStyleClass().add("text-field-styling"); @@ -282,13 +304,22 @@ protected void updateItem(String item, final boolean empty) setGraphic(null); } else{ - passwordField.setText(item == null ? item : "dummypass"); // Hack to no reveal password length + passwordField.setText(item == null ? item : "dummypass"); // Hack to not reveal password length if(getTableRow() != null && getTableRow().getItem() != null) { // Disable field if user is logged in. passwordField.disableProperty().set(!getTableRow().getItem().loginAction); } + passwordField.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + CredentialsManagementController.this.login(getTableRow().getItem()); + } + }); setGraphic(passwordField); } } } + + public void setStage(Stage stage){ + this.stage = stage; + } } diff --git a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementStage.java b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementStage.java index 6749e1175e..ef51b00f04 100644 --- a/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementStage.java +++ b/app/credentials-management/src/main/java/org/phoebus/applications/credentialsmanagement/CredentialsManagementStage.java @@ -50,6 +50,7 @@ public CredentialsManagementStage(List authentica CredentialsManagementController controller = (CredentialsManagementController) clazz.getConstructor(List.class, SecureStore.class) .newInstance(authenticationProviders, secureStore); + controller.setStage(this); return controller; } catch (Exception e) { diff --git a/app/databrowser-timescale/pom.xml b/app/databrowser-timescale/pom.xml index 8de360ed45..7372b70b29 100644 --- a/app/databrowser-timescale/pom.xml +++ b/app/databrowser-timescale/pom.xml @@ -3,14 +3,14 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-databrowser-timescale org.phoebus app-databrowser - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/databrowser/pom.xml b/app/databrowser/pom.xml index f087cfe048..dfe4a0bffd 100644 --- a/app/databrowser/pom.xml +++ b/app/databrowser/pom.xml @@ -3,7 +3,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-databrowser @@ -27,37 +27,37 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-formula - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-rtplot - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT com.google.protobuf diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java index 4b67f8c688..8fb3365b46 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java @@ -95,7 +95,12 @@ public void changedTimeAxisConfig() @Override public void changedAxis(final Optional axis) - { setDirty(true); } + { + if (!axis.isPresent() || !axis.get().autoScaleUpdateInProgress) + { + setDirty(true); + } + } @Override public void itemAdded(final ModelItem item) diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/export/ValueWithInfoFormatter.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/export/ValueWithInfoFormatter.java index 516208697a..b552955bc1 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/export/ValueWithInfoFormatter.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/export/ValueWithInfoFormatter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010-2018 Oak Ridge National Laboratory. + * Copyright (c) 2010-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,8 +8,6 @@ package org.csstudio.trends.databrowser3.export; import org.csstudio.trends.databrowser3.Messages; -import org.epics.vtype.VString; -import org.epics.vtype.VStringArray; import org.epics.vtype.VType; import org.phoebus.archive.vtype.Style; import org.phoebus.archive.vtype.VTypeHelper; @@ -40,12 +38,6 @@ public String getHeader() @Override public String format(final VType value) { - if (value instanceof VString - || value instanceof VStringArray - || Double.isNaN(VTypeHelper.toDouble(value))) - return super.format(value) + - Messages.Export_Delimiter + Messages.Export_NoValueMarker + - Messages.Export_Delimiter + Messages.Export_NoValueMarker; return super.format(value) + Messages.Export_Delimiter + org.phoebus.core.vtypes.VTypeHelper.getSeverity(value) + Messages.Export_Delimiter + VTypeHelper.getMessage(value); diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/AxisConfig.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/AxisConfig.java index efbad8f4dc..991f4a20d9 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/AxisConfig.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/AxisConfig.java @@ -58,6 +58,8 @@ public class AxisConfig /** Logarithmic scale? */ private boolean log_scale; + public boolean autoScaleUpdateInProgress = false; + /** Initialize with defaults * @param name Axis name */ @@ -227,7 +229,11 @@ public void setRange(final double min, final double max) this.min = max; this.max = min; } + + autoScaleUpdateInProgress = true; fireAxisChangeEvent(); + autoScaleUpdateInProgress = false; + } /** @return true if grid lines are drawn */ diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/HistoricSamples.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/HistoricSamples.java index 0c5e58f84b..03c9d5c6d7 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/HistoricSamples.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/HistoricSamples.java @@ -158,5 +158,6 @@ public void clear() { visible_size = 0; samples = new PlotSample[0]; + border_time = Optional.empty(); } } diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/PVItem.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/PVItem.java index 5dec2e83cc..7dd0cfbd56 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/PVItem.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/model/PVItem.java @@ -242,9 +242,11 @@ public void addArchiveDataSource(final ArchiveDataSource archs[]) /** @param archive Archive to remove as a source from this item. */ public void removeArchiveDataSource(final ArchiveDataSource archive) { - // Archive removed -> (Probably) no need to get new data - if (archives.remove(archive)) + boolean change = archives.remove(archive); + if (!Preferences.use_default_archives && change) { + // Archive removed -> (Probably) no need to get new data fireItemDataConfigChanged(false); + } } /** @param archs Archives to remove as a source from this item. Ignored when not used. */ @@ -254,7 +256,7 @@ public void removeArchiveDataSource(final List archs) for (ArchiveDataSource archive : archs) if (archives.remove(archive)) change = true; - if (change) + if (!Preferences.use_default_archives && change) fireItemDataConfigChanged(false); } diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/selection/DatabrowserSelection.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/selection/DatabrowserSelection.java index 80ec6992b9..383b8cf5a2 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/selection/DatabrowserSelection.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/ui/selection/DatabrowserSelection.java @@ -12,6 +12,7 @@ import org.csstudio.trends.databrowser3.model.ModelItem; import org.csstudio.trends.databrowser3.persistence.XMLPersistence; import org.csstudio.trends.databrowser3.ui.plot.ModelBasedPlot; +import org.phoebus.util.time.TimeInterval; import org.phoebus.util.time.TimeRelativeInterval; import javafx.scene.image.Image; @@ -53,6 +54,13 @@ public TimeRelativeInterval getPlotTime() return model.getTimerange(); } + public void makeTimeRangeAbsolute() + { + TimeInterval absoluteInterval = model.getTimerange().toAbsoluteInterval(); + TimeRelativeInterval absoluteTimeInterval = TimeRelativeInterval.of(absoluteInterval.getStart(), absoluteInterval.getEnd()); + model.setTimerange(absoluteTimeInterval); + } + /** * Get the title of the selected databrowser plot * @return plot title diff --git a/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/AbstractRDBValueIterator.java b/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/AbstractRDBValueIterator.java index 2bc1fb2a4d..0c714c4a31 100644 --- a/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/AbstractRDBValueIterator.java +++ b/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/AbstractRDBValueIterator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2018 Oak Ridge National Laboratory. + * Copyright (c) 2017-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,6 +15,7 @@ import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.epics.util.array.ArrayDouble; import org.epics.util.stats.Range; @@ -224,7 +225,7 @@ protected VType decodeSampleTableValue(final ResultSet result, final boolean han // Default to string final String txt = result.getString(6); - return VString.of(txt, alarm, time); + return VString.of(Objects.toString(txt), alarm, time); } /** @param severity Original severity diff --git a/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/StoredProcedureValueIterator.java b/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/StoredProcedureValueIterator.java index d7c0a16a9f..e9f151334b 100644 --- a/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/StoredProcedureValueIterator.java +++ b/app/databrowser/src/main/java/org/phoebus/archive/reader/rdb/StoredProcedureValueIterator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Oak Ridge National Laboratory. + * Copyright (c) 2017-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,6 +18,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; @@ -219,7 +220,7 @@ private List decodeOptimizedTable(final ResultSet result) throws Exceptio // WB==-1 indicates a String sample final VType value; if (result.getInt(1) < 0) - value = VString.of(result.getString(8), alarm, time); + value = VString.of(Objects.toString(result.getString(8)), alarm, time); else { // Only one value within averaging bucket? final int cnt = result.getInt(9); diff --git a/app/diag/pom.xml b/app/diag/pom.xml index 5124217ec8..6238c80ea8 100644 --- a/app/diag/pom.xml +++ b/app/diag/pom.xml @@ -4,18 +4,18 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/app/display/adapters/pom.xml b/app/display/adapters/pom.xml index 2f060b3ab4..b364fe6c26 100644 --- a/app/display/adapters/pom.xml +++ b/app/display/adapters/pom.xml @@ -3,7 +3,7 @@ app-display org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -12,17 +12,17 @@ org.phoebus app-email-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-runtime - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile diff --git a/app/display/adapters/src/main/java/org/csstudio/display/builder/util/DisplayBuilderAdapterFactory.java b/app/display/adapters/src/main/java/org/csstudio/display/builder/util/DisplayBuilderAdapterFactory.java index 4536fdc39e..c0cb178614 100644 --- a/app/display/adapters/src/main/java/org/csstudio/display/builder/util/DisplayBuilderAdapterFactory.java +++ b/app/display/adapters/src/main/java/org/csstudio/display/builder/util/DisplayBuilderAdapterFactory.java @@ -58,7 +58,7 @@ else if (adapterType.isAssignableFrom(LogEntry.class)) { LogEntryBuilder log = log() .title(LogbookPreferences.auto_title ? "Display Screenshot for : " + selectionInfo.getName() : "") - .appendDescription(getBody(selectionInfo)); + .appendDescription(LogbookPreferences.auto_body ? getBody(selectionInfo) : ""); try { final File image_file = selectionInfo.getImage() == null ? null : new Screenshot(selectionInfo.getImage()).writeToTempfile("image"); diff --git a/app/display/convert-edm/pom.xml b/app/display/convert-edm/pom.xml index 54c74f1e71..23f60c9c9e 100644 --- a/app/display/convert-edm/pom.xml +++ b/app/display/convert-edm/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-convert-edm @@ -22,22 +22,22 @@ org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-formula - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-editor - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeButtonClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeButtonClass.java index 887b189f7d..64f991c6a9 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeButtonClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeButtonClass.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -27,8 +27,8 @@ public Convert_activeButtonClass(final EdmConverter converter, final Widget pare if (t.getControlPv() != null) widget.propPVName().setValue(convertPVName(t.getControlPv())); - if (t.getAttribute("controlBitsPos").isExistInEDL()) - widget.propBit().setValue(t.getControlBitsPos()); + if (t.getAttribute("controlBitPos").isExistInEDL()) + widget.propBit().setValue(t.getControlBitPos()); // EDM widget has no LED widget.propShowLED().setValue(false); diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeCircleClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeCircleClass.java index e61e9e710f..b11ae94399 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeCircleClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeCircleClass.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,6 +8,7 @@ package org.csstudio.display.converter.edm.widgets; import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.widgets.BaseLEDWidget; import org.csstudio.display.builder.model.widgets.EllipseWidget; import org.csstudio.display.converter.edm.EdmConverter; import org.csstudio.opibuilder.converter.model.EdmWidget; @@ -18,33 +19,40 @@ * @author Matevz, Lei Hu, Xihui Chen et al - Original logic in Opi_.. converter */ @SuppressWarnings("nls") -public class Convert_activeCircleClass extends ConverterBase +public class Convert_activeCircleClass extends ConverterBase { public Convert_activeCircleClass(final EdmConverter converter, final Widget parent, final Edm_activeCircleClass r) { super(converter, parent, r); + EllipseWidget ell = (EllipseWidget) widget; + // No 'dash' support //if (r.getLineStyle().isExistInEDL() && // r.getLineStyle().get() == EdmLineStyle.DASH) if (r.getAttribute("lineWidth").isExistInEDL()) - widget.propLineWidth().setValue(r.getLineWidth()); + ell.propLineWidth().setValue(r.getLineWidth()); else - widget.propLineWidth().setValue(1); - widget.propTransparent().setValue(! r.isFill()); + ell.propLineWidth().setValue(1); + ell.propTransparent().setValue(! r.isFill()); if (r.isLineAlarm()) - createAlarmColor(r.getAlarmPv(), widget.propLineColor()); + createAlarmColor(r.getAlarmPv(), ell.propLineColor()); else - convertColor(r.getLineColor(), r.getAlarmPv(), widget.propLineColor()); + convertColor(r.getLineColor(), r.getAlarmPv(), ell.propLineColor()); if (r.isFillAlarm() && r.getAlarmPv() != null) - createAlarmColor(r.getAlarmPv(), widget.propBackgroundColor()); + createAlarmColor(r.getAlarmPv(), ell.propBackgroundColor()); else - convertColor(r.getFillColor(), r.getAlarmPv(), widget.propBackgroundColor()); + convertColor(r.getFillColor(), r.getAlarmPv(), ell.propBackgroundColor()); + + ell.propVisible().setValue(!r.isInvisible()); + + widget = Convert_activeRectangleClass.convertShapeToLED(widget, r, r.isFillAlarm(), r.getFillColor(), r.getAlarmPv()); - widget.propVisible().setValue(!r.isInvisible()); + if (widget instanceof BaseLEDWidget) + ((BaseLEDWidget)widget).propSquare().setValue(false); } @Override diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java index b9a4b73894..c0fdb02853 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeMessageButtonClass.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -132,7 +132,9 @@ else if (have_release_value) logger.log(Level.WARNING, "EDM message 'push' button '" + desc + "' lacks both 'press' and 'release'; writing empty string"); } - b.propActions().setValue(new ActionInfos(List.of(new WritePVActionInfo(desc, pv, value)))); + // Set the button's $(pv_name) macro to the PV name, and use that within the write-PV action + b.propPVName().setValue(pv); + b.propActions().setValue(new ActionInfos(List.of(new WritePVActionInfo(desc, "$(pv_name)", value)))); } if (mb.getPassword() != null) diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeRectangleClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeRectangleClass.java index 35757f925a..2373cd9a5e 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeRectangleClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeRectangleClass.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,9 +7,27 @@ *******************************************************************************/ package org.csstudio.display.converter.edm.widgets; +import static org.csstudio.display.converter.edm.Converter.logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.csstudio.display.builder.model.ChildrenProperty; import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.NamedWidgetColors; +import org.csstudio.display.builder.model.persist.WidgetColorService; +import org.csstudio.display.builder.model.properties.WidgetColor; +import org.csstudio.display.builder.model.widgets.BaseLEDWidget; +import org.csstudio.display.builder.model.widgets.LEDWidget; +import org.csstudio.display.builder.model.widgets.MultiStateLEDWidget; import org.csstudio.display.builder.model.widgets.RectangleWidget; import org.csstudio.display.converter.edm.EdmConverter; +import org.csstudio.opibuilder.converter.model.EdmColor; +import org.csstudio.opibuilder.converter.model.EdmModel; import org.csstudio.opibuilder.converter.model.EdmWidget; import org.csstudio.opibuilder.converter.model.Edm_activeRectangleClass; @@ -18,7 +36,7 @@ * @author Matevz, Lei Hu, Xihui Chen et al - Original logic in Opi_.. converter */ @SuppressWarnings("nls") -public class Convert_activeRectangleClass extends ConverterBase +public class Convert_activeRectangleClass extends ConverterBase { public Convert_activeRectangleClass(final EdmConverter converter, final Widget parent, final Edm_activeRectangleClass r) { @@ -28,7 +46,8 @@ public Convert_activeRectangleClass(final EdmConverter converter, final Widget p final int linewidth = Math.max(1, r.getLineWidth()); // EDM applies linewidth inside and outside of widget - widget.propLineWidth().setValue(linewidth); + final RectangleWidget rect = (RectangleWidget) widget; + rect.propLineWidth().setValue(linewidth); widget.propX().setValue(r.getX() - converter.getOffsetX() - linewidth/2); widget.propY().setValue(r.getY() - converter.getOffsetY() - linewidth/2); widget.propWidth().setValue(r.getW()+linewidth); @@ -38,22 +57,199 @@ public Convert_activeRectangleClass(final EdmConverter converter, final Widget p //if (r.getLineStyle().isExistInEDL() && // r.getLineStyle().get() == EdmLineStyle.DASH) - widget.propTransparent().setValue(! r.isFill()); + rect.propTransparent().setValue(! r.isFill()); - widget.propVisible().setValue(!r.isInvisible()); + rect.propVisible().setValue(!r.isInvisible()); if (r.isLineAlarm() && r.getAlarmPv() != null) - createAlarmColor(r.getAlarmPv(), widget.propLineColor()); + createAlarmColor(r.getAlarmPv(), rect.propLineColor()); else - convertColor(r.getLineColor(), r.getAlarmPv(), widget.propLineColor()); + convertColor(r.getLineColor(), r.getAlarmPv(), rect.propLineColor()); if (r.isFillAlarm() && r.getAlarmPv() != null) - createAlarmColor(r.getAlarmPv(), widget.propBackgroundColor()); + createAlarmColor(r.getAlarmPv(), rect.propBackgroundColor()); + else + convertColor(r.getFillColor(), r.getAlarmPv(), rect.propBackgroundColor()); + + // Convert to LED? + widget = convertShapeToLED(widget, r, r.isFillAlarm(), r.getFillColor(), r.getAlarmPv()); + } + + /** Can original widget be replaced with LED because shape uses dynamic color? */ + protected static Widget convertShapeToLED(final Widget widget, final EdmWidget edm, final boolean fill_alarm, final EdmColor fillColor, final String pv_spec) + { + if (!(fill_alarm || fillColor.isDynamic()) || pv_spec == null || pv_spec.isBlank()) + return widget; + + logger.log(Level.FINE, "Checking " + edm.getType() + " with dynamic color for LED conversion"); + + String pv = convertPVName(pv_spec); + + // EDM dynamic colors are defined as ranges like this: + // >=-0.5 && <0.5: "color0" + // >=0.5 && <1.5: "color1" + // >=1.5 && <2.5: "color2" + // >=2.5 && <3.5: "color3" + // >=3.5 && <4.5: "color4" + // >=4.5 && <=5.5: "color5" + // + // In most cases, they are then used with enumerated or integer PVs + // so values 0, 1, .., 5 map to color0..5. + // We assume that case and try to create an LED or MultiStateLED + // + // If the EDM display was meant to be used with double values, + // this will fail. + // 1.9 that was resulting in color2 for EDM will now show color1... + final List colors = new ArrayList<>(); + + + if (fill_alarm) + { // Use alarm severity of the PV and alarm colors + pv = "=highestSeverity(`" + pv + "`)"; + colors.add(WidgetColorService.getColor(NamedWidgetColors.ALARM_OK)); + colors.add(WidgetColorService.getColor(NamedWidgetColors.ALARM_MINOR)); + colors.add(WidgetColorService.getColor(NamedWidgetColors.ALARM_MAJOR)); + colors.add(WidgetColorService.getColor(NamedWidgetColors.ALARM_INVALID)); + colors.add(WidgetColorService.getColor(NamedWidgetColors.ALARM_DISCONNECTED)); + } else - convertColor(r.getFillColor(), r.getAlarmPv(), widget.propBackgroundColor()); + { + int index = 0; + for (Entry entry : fillColor.getRuleMap().entrySet()) + { + final double[] start_end = parseColorRange(entry.getKey()); + if (// start...end surrounds index +- 1 + ((index-1) < start_end[0] && start_end[0] <= index && + index <= start_end[1] && start_end[1] < (index+1)) + + // Or just start and open end + || + ((index-1) < start_end[0] && start_end[0] <= index && + Double.isNaN(start_end[1])) + ) + { + final EdmColor edm_color = EdmModel.getColorsList().getColor(entry.getValue()); + if (edm_color == null) + { + logger.log(Level.WARNING, "Dynamic color uses unknown color " + entry.getValue()); + return widget; + } + final WidgetColor color = convertStaticColor(edm_color); + logger.log(Level.FINE, String.format("State %d (%6.3f to %6.3f) --> %s", + index, + start_end[0], start_end[1], + color.toString())); + colors.add(color); + } + else + { + logger.log(Level.FINE, "Colors don't map to integer state ranges"); + return widget; + } + + ++index; + } + } + + BaseLEDWidget replacement = null; + if (colors.size() == 2) + { + logger.log(Level.INFO, "Creating LED for " + pv); + logger.log(Level.INFO, "Off color " + colors.get(0)); + logger.log(Level.INFO, "On color " + colors.get(1)); + + // Create LED in place of original shape + final LEDWidget led = new LEDWidget(); + led.propName().setValue(widget.propName().getValue()); + led.propX().setValue(widget.propX().getValue()); + led.propY().setValue(widget.propY().getValue()); + led.propWidth().setValue(widget.propWidth().getValue()); + led.propHeight().setValue(widget.propHeight().getValue()); + led.propSquare().setValue(true); + + // Instead of script, use plain PV and LED behavior + led.propPVName().setValue(pv); + led.propOffColor().setValue(colors.get(0)); + led.propOnColor().setValue(colors.get(1)); + + // LED should ideally be alarm sensitive, but the shape wasn't, so keep it similar + led.propBorderAlarmSensitive().setValue(false); + + replacement = led; + } + else if (colors.size() > 2) + { + logger.log(Level.INFO, "Creating MultiStateLED for " + colors.size() + " states of " + pv); + for (int i=0; i colors.size()) + led.propStates().removeElement(); + while (led.propStates().size() < colors.size()) + led.propStates().addElement(); + for (int i=0; i=\\s*([-+]?[0-9]*\\.?[0-9]*)\\s*&&\\s*<=?\\s*([-+]?[0-9]*\\.?[0-9]*)\\s*"); + private static Pattern start = Pattern.compile("\\s*>=?\\s*([-+]?[0-9]*\\.?[0-9]*)\\s*"); + + /** Parse start and end from ">=-0.5 && <0.5" or ">=4.5 && <=5.5" or just start from ">2.5" + * @param range Range of EDM dynamic color + * @return [start, end], either start or end may be NaN + */ + protected static double[] parseColorRange(final String range) + { + Matcher matcher = start_end.matcher(range); + if (matcher.matches()) + return new double[] + { + Double.parseDouble(matcher.group(1)), + Double.parseDouble(matcher.group(2)) + }; + + matcher = start.matcher(range); + if (matcher.matches()) + return new double[] + { + Double.parseDouble(matcher.group(1)), + Double.NaN + }; + + return new double[] { Double.NaN, Double.NaN }; } @Override - protected RectangleWidget createWidget(final EdmWidget edm) + protected Widget createWidget(final EdmWidget edm) { return new RectangleWidget(); } diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/ConverterBase.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/ConverterBase.java index 127098891e..a1c24b7eef 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/ConverterBase.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/ConverterBase.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -56,7 +56,7 @@ @SuppressWarnings("nls") public abstract class ConverterBase { - protected final W widget; + protected W widget; public ConverterBase(final EdmConverter converter, final Widget parent, final EdmWidget t) { @@ -200,8 +200,11 @@ public static WidgetColor evaluateDynamicColor(final EdmColor edm, final double } - /** Find a '=' that is neither preceded by '<', '>', '=' nor followed by '=' */ - private static final Pattern expand_equal = Pattern.compile("(?=])=(?!=)"); + /** Find a single '=' that is neither preceded by '!', '<', '>', '=' nor followed by '=' + * + * Used to turn '=' into '==' but keep '!=', '<=', '>=', '==' unchanged + */ + private static final Pattern expand_equal = Pattern.compile("(?=!])=(?!=)"); /** @param expression EDM color rule expression like ">=5 && <10" * @return Display Builder rule expression like "pv0>=5 && pv0<10" @@ -245,7 +248,7 @@ public static void createAlarmColor(final String alarm_pv, final WidgetProperty< /** @param edm Static EDM color * @return {@link WidgetColor} */ - private static WidgetColor convertStaticColor(final EdmColor edm) + protected static WidgetColor convertStaticColor(final EdmColor edm) { // EDM uses 16 bit color values final int red = edm.getRed() >> 8, diff --git a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/StringSplitter.java b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/StringSplitter.java index 92f79bc082..78e5c6d496 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/StringSplitter.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/StringSplitter.java @@ -55,7 +55,7 @@ private StringSplitter() /** Split source string into an array of elements separated by the splitting character, * but ignoring split characters enclosed in quotes. * - * @param trimmedSource String to be split + * @param source String to be split * @param splitChar Character used to split the source string, e.g. ',' or ' ' * @param deleteHeadTailQuotes Delete quotes in the head and tail of individual elements * if true diff --git a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/EdmPointsList.java b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/EdmPointsList.java index 0702245efb..8bb71afef9 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/EdmPointsList.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/EdmPointsList.java @@ -78,9 +78,9 @@ public int[] get() { /** * Add a point to the end of the points list. - * @param Point to be added. + * @param point to be added. */ - public void addPoint(int e) { - val.add(e); + public void addPoint(int point) { + val.add(point); } } diff --git a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/Edm_activeButtonClass.java b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/Edm_activeButtonClass.java index af9562865f..9ce3fc9c2f 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/Edm_activeButtonClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/opibuilder/converter/model/Edm_activeButtonClass.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013 Oak Ridge National Laboratory. + * Copyright (c) 2013-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -24,7 +24,7 @@ public class Edm_activeButtonClass extends EdmWidget { @EdmAttributeAn @EdmOptionalAn private String onLabel; @EdmAttributeAn @EdmOptionalAn private String offLabel; @EdmAttributeAn @EdmOptionalAn private String buttonType; - @EdmAttributeAn @EdmOptionalAn private int controlBitsPos; + @EdmAttributeAn @EdmOptionalAn private int controlBitPos; @EdmAttributeAn @EdmOptionalAn private String labelType; @@ -37,8 +37,8 @@ public Edm_activeButtonClass(EdmEntity genericEntity) throws EdmException { public String getButtonType() { return buttonType; } - public int getControlBitsPos() { - return controlBitsPos; + public int getControlBitPos() { + return controlBitPos; } public EdmColor getOnColor() { diff --git a/app/display/convert-edm/src/test/java/org/csstudio/display/converter/edm/CalcPvConverterTest.java b/app/display/convert-edm/src/test/java/org/csstudio/display/converter/edm/CalcPvConverterTest.java index 6b999bcd32..bb5d7e9421 100644 --- a/app/display/convert-edm/src/test/java/org/csstudio/display/converter/edm/CalcPvConverterTest.java +++ b/app/display/convert-edm/src/test/java/org/csstudio/display/converter/edm/CalcPvConverterTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -31,6 +31,9 @@ public void testCalcPv() formula = ConverterBase.convertPVName("CALC\\{A+B+C=0?0:1}(HEBA, HUBA, CUBA)"); assertThat(formula, equalTo("=`HEBA`+`HUBA`+`CUBA`==0?0:1")); + formula = ConverterBase.convertPVName("CALC\\{A=1&&B!=2}(HEBA, HUBA)"); + assertThat(formula, equalTo("=`HEBA`==1&&`HUBA`!=2")); + formula = ConverterBase.convertPVName("CALC\\{A+B+C+D+E+F+G+H+I+J+K+L=0?0:1}(SCL_Diag:PS_LW01:Off,SCL_Diag:PS_LW02:Off,SCL_Diag:PS_LW03:Off,SCL_Diag:PS_LW04:Off,SCL_Diag:PS_LW12:Off,SCL_Diag:PS_LW13:Off,SCL_Diag:PS_LW14:Off,SCL_Diag:PS_LW15:Off,SCL_Diag:PS_LW32:Off,LDmp_Diag:PS_LW01:Off,LDmp_Diag:PS_LW02:Off,LDmp_Diag:PS_LW03:Off)"); assertThat(formula, equalTo("=`SCL_Diag:PS_LW01:Off`+`SCL_Diag:PS_LW02:Off`+`SCL_Diag:PS_LW03:Off`+`SCL_Diag:PS_LW04:Off`+`SCL_Diag:PS_LW12:Off`+`SCL_Diag:PS_LW13:Off`+`SCL_Diag:PS_LW14:Off`+`SCL_Diag:PS_LW15:Off`+`SCL_Diag:PS_LW32:Off`+`LDmp_Diag:PS_LW01:Off`+`LDmp_Diag:PS_LW02:Off`+`LDmp_Diag:PS_LW03:Off`==0?0:1")); diff --git a/app/display/convert-medm/pom.xml b/app/display/convert-medm/pom.xml index 7f4612b79c..da116c3361 100644 --- a/app/display/convert-medm/pom.xml +++ b/app/display/convert-medm/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-convert-medm @@ -22,12 +22,12 @@ org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/AbstractADL2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/AbstractADL2Model.java index 428d65d964..6c29658a73 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/AbstractADL2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/AbstractADL2Model.java @@ -177,7 +177,7 @@ else if (dynAttr.get_vis().equals("calc")) if (dynAttr.getClrMode().equals("alarm") && dynAttr.get_chan() != null) { // Attach script that sets background color based on alarm severity - final String embedded_script = + String embedded_script = "from org.csstudio.display.builder.runtime.script import PVUtil\n" + "from org.csstudio.display.builder.model.persist import WidgetColorService\n" + "sevr = PVUtil.getSeverity(pvs[0])\n" + @@ -193,6 +193,9 @@ else if (dynAttr.get_vis().equals("calc")) " name = 'OK'\n" + "color = WidgetColorService.getColor(name)\n" + "widget.setPropertyValue('background_color', color)\n"; + // Also set optional line color + if (widgetModel.checkProperty("line_color").isPresent()) + embedded_script += "widget.setPropertyValue('line_color', color)\n"; final ScriptInfo script = new ScriptInfo(ScriptInfo.EMBEDDED_PYTHON, embedded_script, true, diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/CartesianPlot2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/CartesianPlot2Model.java index 0722af2270..e90dbacb78 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/CartesianPlot2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/CartesianPlot2Model.java @@ -31,7 +31,6 @@ public CartesianPlot2Model(ADLWidget adlWidget, WidgetColor[] colorMap, Widget p /** * @param adlWidget - * @param colorMap */ @Override public void processWidget(ADLWidget adlWidget) { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Oval2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Oval2Model.java index b1c5134255..0596dee047 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Oval2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Oval2Model.java @@ -8,13 +8,18 @@ import org.csstudio.display.builder.model.ChildrenProperty; import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.NamedWidgetColors; +import org.csstudio.display.builder.model.persist.WidgetColorService; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.widgets.EllipseWidget; +import org.csstudio.display.builder.model.widgets.MultiStateLEDWidget; +import org.csstudio.display.builder.model.widgets.RectangleWidget; import org.csstudio.utility.adlparser.fileParser.ADLWidget; +import org.csstudio.utility.adlparser.fileParser.widgetParts.ADLDynamicAttribute; import org.csstudio.utility.adlparser.fileParser.widgets.ADLAbstractWidget; import org.csstudio.utility.adlparser.fileParser.widgets.Oval; -public class Oval2Model extends AbstractADL2Model { +public class Oval2Model extends AbstractADL2Model { public Oval2Model(ADLWidget adlWidget, WidgetColor[] colorMap, Widget parentModel) throws Exception { super(adlWidget, colorMap, parentModel); @@ -32,6 +37,8 @@ public void processWidget(ADLWidget adlWidget) throws Exception { if ( ovalWidget.hasADLBasicAttribute() ) { setShapesColorFillLine(ovalWidget); } + + widgetModel = updateAlarmShapeToLED(ovalWidget, widgetModel); } @Override @@ -40,4 +47,58 @@ public void makeModel(ADLWidget adlWidget, widgetModel = new EllipseWidget(); ChildrenProperty.getChildren(parentModel).addChild(widgetModel); } + + /** Convert alarm-colored shape into LED + * @param shape MEDM oval or rectangle that might use a "dynamic" alarm color + * @param initial Ellipse or RectangleWidget + * @return Initial widget or LED + */ + public static Widget updateAlarmShapeToLED(ADLAbstractWidget shape, Widget initial) + { + if (!shape.hasADLBasicAttribute() || !shape.hasADLDynamicAttribute()) + return initial; + + // Does this filled (not just outline) shape change color based on alarm? + final ADLDynamicAttribute dynAttr = shape.getAdlDynamicAttribute(); + if ("outline".equals(shape.getAdlBasicAttribute().getFill()) || + !dynAttr.getClrMode().equals("alarm") || + dynAttr.get_chan() == null) + return initial; + + // Replace with LED + final MultiStateLEDWidget led = new MultiStateLEDWidget(); + TranslatorUtils.copyPosAndSize(initial, led); + led.propName().setValue(initial.getName()); + led.propSquare().setValue(initial instanceof RectangleWidget); + + // Read alarm severity of PV + led.propPVName().setValue("=highestSeverity(`" + dynAttr.get_chan() + "`)"); + led.propBorderAlarmSensitive().setValue(false); + + // Alarm-type colors for states + while (led.propStates().size() < 5) + led.propStates().addElement(); + led.propStates().getElement(0).label().setValue(""); + led.propStates().getElement(0).color().setValue(WidgetColorService.getColor(NamedWidgetColors.ALARM_OK)); + led.propStates().getElement(1).label().setValue(""); + led.propStates().getElement(1).color().setValue(WidgetColorService.getColor(NamedWidgetColors.ALARM_MINOR)); + led.propStates().getElement(2).label().setValue(""); + led.propStates().getElement(2).color().setValue(WidgetColorService.getColor(NamedWidgetColors.ALARM_MAJOR)); + led.propStates().getElement(3).label().setValue(""); + led.propStates().getElement(3).color().setValue(WidgetColorService.getColor(NamedWidgetColors.ALARM_INVALID)); + led.propStates().getElement(4).label().setValue(""); + led.propStates().getElement(4).color().setValue(WidgetColorService.getColor(NamedWidgetColors.ALARM_DISCONNECTED)); + + // LED has a fixed outline, the 'filled' oval doesn't. + // Could add a script to color the outline the same as the LED body, but that's a lot of overhead. + // Leaving the default gray border? To make it look closer to MEDM, turn outline transparent. + led.propLineColor().setValue(NamedWidgetColors.TRANSPARENT); + + // Replace initial widget with led + final Widget parent = initial.getParent().get(); + ChildrenProperty.getChildren(parent).removeChild(initial); + ChildrenProperty.getChildren(parent).addChild(led); + + return led; + } } diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Rectangle2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Rectangle2Model.java index 921e051bad..1321343350 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Rectangle2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/Rectangle2Model.java @@ -14,7 +14,7 @@ import org.csstudio.utility.adlparser.fileParser.widgets.ADLAbstractWidget; import org.csstudio.utility.adlparser.fileParser.widgets.Rectangle; -public class Rectangle2Model extends AbstractADL2Model { +public class Rectangle2Model extends AbstractADL2Model { public Rectangle2Model(ADLWidget adlWidget, WidgetColor[] colorMap, Widget parentModel) throws Exception { super(adlWidget, colorMap, parentModel); @@ -31,6 +31,8 @@ public void processWidget(ADLWidget adlWidget) throws Exception { //check fill parameters if ( rectWidget.hasADLBasicAttribute() ) setShapesColorFillLine(rectWidget); + + widgetModel = Oval2Model.updateAlarmShapeToLED(rectWidget, widgetModel); } @Override diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java index 8af79110ec..5ddb0f613d 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/RelatedDisplay2Model.java @@ -84,10 +84,9 @@ public void processWidget(ADLWidget adlWidget) throws Exception { } /** + * @param rdDisplay * @param target - * @param rdDisplays - * @param ii - * @return + * @return ActionInfo */ public ActionInfo createOpenDisplayAction(final RelatedDisplayItem rdDisplay, Target target) { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/TranslatorUtils.java b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/TranslatorUtils.java index b5a89394db..16fdb4c2a5 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/TranslatorUtils.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/opibuilder/adl2boy/translator/TranslatorUtils.java @@ -5,6 +5,7 @@ /*************************************************************************/ package org.csstudio.opibuilder.adl2boy.translator; +import org.csstudio.display.builder.model.Widget; import org.csstudio.utility.adlparser.fileParser.ADLWidget; import org.csstudio.utility.adlparser.fileParser.WrongADLFormatException; import org.csstudio.utility.adlparser.fileParser.widgetParts.ADLBasicAttribute; @@ -38,4 +39,12 @@ public static void setDefaultDynamicAttribute(ADLWidget child) throws WrongADLFo defaultDynamicAttribute = new ADLDynamicAttribute(child); } } + + public static void copyPosAndSize(Widget source, Widget dest) + { + dest.propX().setValue(source.propX().getValue()); + dest.propY().setValue(source.propY().getValue()); + dest.propWidth().setValue(source.propWidth().getValue()); + dest.propHeight().setValue(source.propHeight().getValue()); + } } diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLBasicAttribute.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLBasicAttribute.java index 5fd1fe6104..b2759023bd 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLBasicAttribute.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLBasicAttribute.java @@ -47,7 +47,6 @@ public class ADLBasicAttribute extends WidgetPart{ * The default constructor. * * @param adlBasicAttribute An ADLWidget that correspond a ADL Basic Attribute. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLBasicAttribute(final ADLWidget adlBasicAttribute) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLControl.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLControl.java index 0e243a65fb..fb48fc6296 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLControl.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLControl.java @@ -39,12 +39,8 @@ public class ADLControl extends ADLConnected { /** * The default constructor. * - * @param adlWidget - * An ADLWidget that correspond a ADL Control. - * @param parentWidgetModel - * The Widget that set the parameter from ADLWidget. - * @throws WrongADLFormatException - * Wrong ADL format or untreated parameter found. + * @param adlWidget An ADLWidget that correspond a ADL Control. + * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLControl(final ADLWidget adlWidget) throws WrongADLFormatException { super(adlWidget); diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLDynamicAttribute.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLDynamicAttribute.java index d6a88cd2af..8347d59b23 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLDynamicAttribute.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLDynamicAttribute.java @@ -36,7 +36,7 @@ * @version $Revision$ * @since 11.09.2007 */ -@SuppressWarnings("nls") + public class ADLDynamicAttribute extends WidgetPart{ //TODO Strip out old code lines that refer to SDS implementations @@ -74,7 +74,6 @@ public class ADLDynamicAttribute extends WidgetPart{ * The default constructor. * * @param adlDynamicAttribute An ADLWidget that correspond a ADL Dynamic Attribute. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLDynamicAttribute(final ADLWidget adlDynamicAttribute) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMenuItem.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMenuItem.java index 09059a3cc8..e1d5e34115 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMenuItem.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMenuItem.java @@ -66,12 +66,8 @@ public class ADLMenuItem extends WidgetPart { /** * The default constructor. * - * @param menuItem - * An ADLWidget that correspond a ADL Menu Item. - * @param parentWidgetModel - * The Widget that set the parameter from ADLWidget. - * @throws WrongADLFormatException - * Wrong ADL format or untreated parameter found. + * @param menuItem An ADLWidget that correspond a ADL Menu Item. + * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLMenuItem(final ADLWidget menuItem) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMonitor.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMonitor.java index a36572ebff..f498f1a8fb 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMonitor.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLMonitor.java @@ -39,12 +39,8 @@ public class ADLMonitor extends ADLConnected { /** * The default constructor. * - * @param adlWidget - * An ADLWidget that correspond a ADL Monitor. - * @param parentWidgetModel - * The Widget that set the parameter from ADLWidget. - * @throws WrongADLFormatException - * Wrong ADL format or untreated parameter found. + * @param adlWidget An ADLWidget that correspond a ADL Monitor. + * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLMonitor(final ADLWidget adlWidget) throws WrongADLFormatException { super(adlWidget); diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLObject.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLObject.java index 99e2dc45bc..d7e90ea1eb 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLObject.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLObject.java @@ -56,7 +56,6 @@ public class ADLObject extends WidgetPart{ * The default constructor. * * @param adlObject An ADLWidget that correspond a ADL Object. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLObject(final ADLWidget adlObject) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLPoints.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLPoints.java index 7c20551934..6fdac1c969 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLPoints.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLPoints.java @@ -48,7 +48,6 @@ public class ADLPoints extends WidgetPart{ * The default constructor. * * @param adlPoints An ADLWidget that correspond a ADL Points. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLPoints (final ADLWidget adlPoints ) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLSensitive.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLSensitive.java index 0b12e20dc2..846fec8634 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLSensitive.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/ADLSensitive.java @@ -58,7 +58,6 @@ public class ADLSensitive extends WidgetPart { * The default constructor. * * @param sensitive An ADLWidget that correspond a ADL Sensitive Item. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public ADLSensitive(final ADLWidget sensitive) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/RelatedDisplayItem.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/RelatedDisplayItem.java index 6a9e29e6ba..0d4ebd914b 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/RelatedDisplayItem.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/RelatedDisplayItem.java @@ -60,12 +60,8 @@ public class RelatedDisplayItem extends WidgetPart { /** * The default constructor. * - * @param display - * An ADLWidget that correspond a ADL Related Display Item. - * @param parentWidgetModel - * The Widget that set the parameter from ADLWidget. - * @throws WrongADLFormatException - * Wrong ADL format or untreated parameter found. + * @param display An ADLWidget that correspond a ADL Related Display Item. + * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public RelatedDisplayItem(final ADLWidget display) throws WrongADLFormatException { diff --git a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/WidgetPart.java b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/WidgetPart.java index 2a2620fd8b..27d7e14531 100644 --- a/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/WidgetPart.java +++ b/app/display/convert-medm/src/main/java/org/csstudio/utility/adlparser/fileParser/widgetParts/WidgetPart.java @@ -49,7 +49,6 @@ public abstract class WidgetPart { * The default constructor. * * @param widgetPart An ADLWidget that correspond to the Child Widget Part. - * @param parentWidgetModel The Widget that set the parameter from ADLWidget. * @throws WrongADLFormatException Wrong ADL format or untreated parameter found. */ public WidgetPart(final ADLWidget widgetPart) throws WrongADLFormatException { diff --git a/app/display/editor/doc/formula_functions.rst b/app/display/editor/doc/formula_functions.rst deleted file mode 100644 index 310e454160..0000000000 --- a/app/display/editor/doc/formula_functions.rst +++ /dev/null @@ -1,3 +0,0 @@ -================= -Formula Functions -================= diff --git a/app/display/editor/doc/rules.rst b/app/display/editor/doc/rules.rst index 1a646bc290..3f8406d36f 100644 --- a/app/display/editor/doc/rules.rst +++ b/app/display/editor/doc/rules.rst @@ -44,14 +44,16 @@ of the rule are accessible in the expression. - A PV severity value is referenced using the syntax pvSev{index}, e.g. pvSev0 == 1. Severity values are: - - -1 - Invalid - - 0 - OK - 1 - Minor - 2 - Major + - 3 - Invalid + + - 4 - Undefined + Value as Expression ------------------- diff --git a/app/display/editor/doc/widgets.rst b/app/display/editor/doc/widgets.rst index 6a399b2449..f62acfdb99 100644 --- a/app/display/editor/doc/widgets.rst +++ b/app/display/editor/doc/widgets.rst @@ -3,12 +3,42 @@ Widgets ======= The Display Builder application supports a set of widgets which can be used to create control GUIs. The widgets are -organized based on functionality as shown below +organized according to the categories *Graphics*, *Monitors*, *Controls*, *Plots*, *Structure*, and *Miscellaneous*: .. image:: images/widgets.png + :align: center + +Each category corresponds to an intended use case of the widgets belonging to the category: + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + :align: center + + * - Widget Category + - Intended Use Case + + * - `Graphics` + - Static graphical elements + + * - `Monitors` + - Displaying values from PVs + + * - `Controls` + - Writing values to PVs + + * - `Plots` + - Plotting PV values + + * - `Structure` + - Grouping widgets and embedding displays + + * - `Miscellaneous` + - 3D Viewer, Web Browser .. toctree:: :maxdepth: 1 widgets_properties - widgets_classes \ No newline at end of file + widgets_classes + widgets_order \ No newline at end of file diff --git a/app/display/editor/doc/widgets_order.rst b/app/display/editor/doc/widgets_order.rst new file mode 100644 index 0000000000..41c4909def --- /dev/null +++ b/app/display/editor/doc/widgets_order.rst @@ -0,0 +1,34 @@ +============ +Widget Order +============ + +The order of widgets, that is, the order in which they are listed in the display +file, matters in a few different ways. + +1. Widgets are drawn in this order on the display. + If widgets overlap, those listed later will appear on top of widgets + that are drawn earlier. + +2. When using the keyboard to navigate between widgets in an running display, + the "TAB" key will navigate to the next widget in this order. + +3. The editor "Widget Tree" that can sometimes be + useful to locate widgets will list them in this order. + +By default, the widget order is based on how widgets were added to the display. +New widgets are simply added to the end of the display. + +There are several options to change the widget order: + +* Use the context menu or editor toolbar to move selected widgets "backwards" or "forward", + which moves them one level in the order, or "back" respectively "front" to move them to the + start or end of the order. + +* Use the "Widget Tree" to drag/drop widgets witin the order + +* Select either the display background or a group widget, then invoke the "Sort Widgets" option + from the context menu. This will sort the widgets of the display or of a group by position, + ordering them left to right and top to bottom. + If widgets share the same position, they are ordered by name. + This can be a quick way to establish a useful "TAB" order, but it may not result in the desired order + for overlapping widgets, which requires either manual adjustment or setting widget names that aid in the sort. diff --git a/app/display/editor/doc/widgets_properties.rst b/app/display/editor/doc/widgets_properties.rst index d047b05d5d..ab5c268207 100644 --- a/app/display/editor/doc/widgets_properties.rst +++ b/app/display/editor/doc/widgets_properties.rst @@ -26,4 +26,14 @@ is ``$(pv_name)\n$(pv_value)``. In case a PV is not defined by the user for such anything useful. In such cases user should consider to change the tool tip property value, or set it to an empty value. An empty tool tip property will suppress rendering of a widget tool tip, even if the value for the tool tip text is -set by a rule. \ No newline at end of file +set by a rule. + +File Property +------------- + +Some widget use a File property to identify a resource, local or remote. If such a reference is a local file, then +user is advised to use relative a path as an absolute +path may not be portable between hosts, in particular not between Windows and Linux/Mac hosts. + +File paths may be specified using both forward slash (/) or backslash (\\) as path separator as both will work interchangeably +between Windows and Linux/Mac. \ No newline at end of file diff --git a/app/display/editor/pom.xml b/app/display/editor/pom.xml index b82ac17fad..bc915f569f 100644 --- a/app/display/editor/pom.xml +++ b/app/display/editor/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-editor @@ -22,22 +22,22 @@ org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-representation-javafx - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/DisplayEditor.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/DisplayEditor.java index 65165f2ff5..cb4b464f8e 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/DisplayEditor.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/DisplayEditor.java @@ -9,6 +9,7 @@ import static org.csstudio.display.builder.editor.Plugin.logger; +import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -20,6 +21,7 @@ import java.util.prefs.Preferences; import org.csstudio.display.builder.editor.actions.ActionDescription; +import org.csstudio.display.builder.editor.app.DisplayEditorApplication; import org.csstudio.display.builder.editor.app.DisplayEditorInstance; import org.csstudio.display.builder.editor.palette.Palette; import org.csstudio.display.builder.editor.poly.PointsBinding; @@ -34,10 +36,7 @@ import org.csstudio.display.builder.editor.util.Rubberband; import org.csstudio.display.builder.editor.util.WidgetNaming; import org.csstudio.display.builder.editor.util.WidgetTransfer; -import org.csstudio.display.builder.model.ChildrenProperty; -import org.csstudio.display.builder.model.DisplayModel; -import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.WidgetDescriptor; +import org.csstudio.display.builder.model.*; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.ModelWriter; import org.csstudio.display.builder.model.util.ModelThreadPool; @@ -46,7 +45,11 @@ import org.csstudio.display.builder.model.widgets.TabsWidget.TabItemProperty; import org.csstudio.display.builder.representation.ToolkitListener; import org.csstudio.display.builder.representation.javafx.JFXRepresentation; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.preferences.PhoebusPreferenceService; +import org.phoebus.framework.util.ResourceParser; +import org.phoebus.framework.workbench.ApplicationService; +import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.undo.UndoButtons; import org.phoebus.ui.undo.UndoableActionManager; @@ -70,6 +73,8 @@ import javafx.scene.control.ToggleButton; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.KeyCode; @@ -159,6 +164,7 @@ public class DisplayEditor private ToggleButton snap; private ToggleButton coords; private ToggleButton crosshair; + private DisplayEditorInstance instance; /** Snap and info options */ public static final String @@ -366,6 +372,52 @@ private ToggleButton createToggleButton(final ActionDescription action, boolean return button; } + public void reloadDisplay (final EditorGUI editor_gui) + { + DisplayEditorApplication application = new DisplayEditorApplication(); + URI file = editor_gui.getFile().toURI(); + instance = application.create(file); + // Warn if editor is dirty + if (instance.isDirty()) + { + final Alert prompt = new Alert(Alert.AlertType.CONFIRMATION); + prompt.setTitle(Messages.ReloadDisplay); + prompt.setHeaderText(Messages.ReloadWarning); + DialogHelper.positionDialog(prompt, editor_gui.getParentNode(), -200, -200); + if (prompt.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) + return; + } + instance.reloadDisplay(); + } + + public void runDisplay (final EditorGUI editor_gui) + { + JobManager.schedule(Messages.Run, monitor -> + { + // Save if there's something to save + if (editor_gui.getDisplayEditor().getUndoableActionManager().canUndo()) { + DisplayEditorApplication application = new DisplayEditorApplication(); + URI file = editor_gui.getFile().toURI(); + Runnable runnable = new Runnable() { + @Override + public void run() { + instance = application.create(file); + try { + instance.doSave(monitor); + } catch (Exception e) { + ModelPlugin.logger.log(Level.SEVERE, "Cannot save editor instance", e); + } + + } + }; + Platform.runLater(runnable); + } + + // Open in runtime, on UI thread + Platform.runLater(() -> ApplicationService.createInstance("display_runtime", ResourceParser.getURI(editor_gui.getFile()))); + }); + } + /** @return ToolBar */ public ToolBar getToolBar() { diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/EditorGUI.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/EditorGUI.java index bf1faf6b2a..504ba339c5 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/EditorGUI.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/EditorGUI.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2020 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -22,6 +22,7 @@ import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import javafx.scene.control.*; import org.csstudio.display.builder.editor.actions.ActionDescription; import org.csstudio.display.builder.editor.app.CreateGroupAction; import org.csstudio.display.builder.editor.app.DisplayEditorInstance; @@ -49,12 +50,6 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Parent; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane.Divider; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -143,10 +138,14 @@ private class ActionWapper extends MenuItem // Use Ctrl-C .. except on Mac, where it's Command-C .. final boolean meta = event.isShortcutDown(); - if (meta && code == KeyCode.Z) + if(code == KeyCode.F5) + editor.reloadDisplay(this); + else if (meta && code == KeyCode.Z) editor.getUndoableActionManager().undoLast(); else if (meta && code == KeyCode.Y) editor.getUndoableActionManager().redoLast(); + else if (meta && code == KeyCode.G) + editor.runDisplay(this); else if (in_editor && meta && code == KeyCode.C) editor.copyToClipboard(); else if (in_editor && !meta && code == KeyCode.C) @@ -461,6 +460,7 @@ public void loadModel(final File file) { canon_path = file.getCanonicalPath(); model = ModelLoader.loadModel(new FileInputStream(file), canon_path); + model.expandMacros(org.csstudio.display.builder.model.Preferences.getMacros()); this.file = file; } catch (final Exception ex) @@ -502,8 +502,8 @@ public void loadModel(final File file) "saving the display in this state might lead to losing those widgets or some of their properties." + "\nPlease check the log for details.", null); } - }); + property_panel.setFile(file); } /** Save model to file diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/Messages.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/Messages.java index fc7465844f..c73eb7dbd9 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/Messages.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/Messages.java @@ -47,8 +47,10 @@ public class Messages DownloadTitle, EditEmbededDisplay, ExpandTree, + FileBrowserToolTip, FileChangedHdr, FileChangedDlg, + FilePath, FindWidget, Grid, LoadDisplay, @@ -95,6 +97,7 @@ public class Messages ShowWidgetTree, Size, Snap, + SortWidgets, UpdateWidgetLocation, UpdateWidgetOrder, UseWidgetClass_TT, diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/actions/ActionDescription.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/actions/ActionDescription.java index f5a627fcdc..24ea44716c 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/actions/ActionDescription.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/actions/ActionDescription.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2020 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -24,6 +24,7 @@ import org.csstudio.display.builder.editor.Messages; import org.csstudio.display.builder.editor.app.DisplayEditorInstance; import org.csstudio.display.builder.editor.undo.SetWidgetPropertyAction; +import org.csstudio.display.builder.editor.undo.SortWidgetsAction; import org.csstudio.display.builder.editor.undo.UpdateWidgetOrderAction; import org.csstudio.display.builder.model.ChildrenProperty; import org.csstudio.display.builder.model.DisplayModel; @@ -840,6 +841,17 @@ else if ( w1y >= w2y && w1y + w1h >= w2y + w2h ) } }; + /** Sort widgets */ + public static final ActionDescription SORT_WIDGETS = + new ActionDescription("icons/sort.png", Messages.SortWidgets) + { + @Override + public void run(final DisplayEditor editor, final boolean selected) + { + editor.getUndoableActionManager().execute(new SortWidgetsAction(editor)); + } + }; + /** Open in external Editor */ public static final ActionDescription OPEN_EXTERNAL = new ActionDescription("icons/file.png", Messages.OpenInExternalEditor) diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java index 4d368df3a8..d2e93bb229 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2022 Oak Ridge National Laboratory. + * Copyright (c) 2017-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -24,6 +24,7 @@ import org.csstudio.display.builder.editor.Messages; import org.csstudio.display.builder.editor.WidgetSelectionHandler; import org.csstudio.display.builder.editor.actions.ActionDescription; +import org.csstudio.display.builder.editor.undo.SortWidgetsAction; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.ModelPlugin; import org.csstudio.display.builder.model.Widget; @@ -120,12 +121,11 @@ public class DisplayEditorInstance implements AppInstance menu_node.setOnContextMenuRequested(event -> handleContextMenu(menu, event)); menu_node.setContextMenu(menu); - dock_item.addCloseCheck(this::okToClose); dock_item.addClosedNotification(this::dispose); } /** @return Current 'dirty' state */ - boolean isDirty() + public boolean isDirty() { return dock_item.isDirty(); } @@ -163,6 +163,10 @@ private void handleContextMenu(final ContextMenu menu, ContextMenuEvent contextM final MenuItem morph = new MorphWidgetsMenu(editor_gui.getDisplayEditor()); final MenuItem back = new ActionWapper(ActionDescription.TO_BACK); final MenuItem front = new ActionWapper(ActionDescription.TO_FRONT); + + final ActionWapper sort_widgets = new ActionWapper(ActionDescription.SORT_WIDGETS); + sort_widgets.setDisable(SortWidgetsAction.getWidgetToSort(editor_gui.getDisplayEditor()) == null); + final ActionWapper open_external = new ActionWapper(ActionDescription.OPEN_EXTERNAL); if (selection.size() <= 0) { @@ -245,6 +249,7 @@ private void handleContextMenu(final ContextMenu menu, ContextMenuEvent contextM morph, back, front, + sort_widgets, new SetDisplaySize(editor_gui.getDisplayEditor()), new SeparatorMenuItem(), ExecuteDisplayAction.asMenuItem(this), @@ -374,7 +379,7 @@ private void handleNewModel(final DisplayModel model) model_name_listener.propertyChanged(model.propName(), null, null); } - void reloadDisplay() + public void reloadDisplay() { loadDisplay(dock_item.getInput()); } @@ -404,7 +409,7 @@ void loadWidgetClasses() }); } - void doSave(final JobMonitor monitor) throws Exception + public void doSave(final JobMonitor monitor) throws Exception { save_job = monitor; @@ -459,7 +464,7 @@ void doSave(final JobMonitor monitor) throws Exception else { // Save-As with proper file name dock_item.setInput(proper.toURI()); - if (! dock_item.save_as(monitor)) + if (! dock_item.save_as(monitor, dock_item.getTabPane().getScene().getWindow())) dock_item.setInput(orig_input); } } diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ArraySizePropertyBinding.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ArraySizePropertyBinding.java index 0a967ded67..c8b0fffe98 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ArraySizePropertyBinding.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/ArraySizePropertyBinding.java @@ -33,6 +33,21 @@ public class ArraySizePropertyBinding extends WidgetPropertyBinding, ArrayWidgetProperty>> { private PropertyPanelSection panel_section; + private int min_value, max_value; + + /** Check text element from array property in response to property UI */ + private final ChangeListener text_listener = (observable, oldValue, newValue) -> + { + if(!newValue.isEmpty()){ + if(!newValue.matches("\\d*")){ + jfx_node.getEditor().setText(oldValue); + } else if(Integer.parseInt(newValue) < min_value){ + jfx_node.getEditor().setText(String.valueOf(min_value)); + } else if(Integer.parseInt(newValue) > max_value){ + jfx_node.getEditor().setText(String.valueOf(max_value)); + } + } + }; /** Add/remove elements from array property in response to property UI */ @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -47,8 +62,10 @@ public class ArraySizePropertyBinding extends WidgetPropertyBinding(other_prop)); + undo.execute(new RemoveArrayElementAction<>(other_prop)); } } }; /** Update property sub-panel as array elements are added/removed */ - private WidgetPropertyListener>> prop_listener = (prop, removed, added) -> + private final WidgetPropertyListener>> prop_listener = (prop, removed, added) -> { // Re-populate the complete property panel. // Combined with the un-focus call above when changing the array size, this "works": @@ -90,21 +107,26 @@ public class ArraySizePropertyBinding extends WidgetPropertyBinding node, - final ArrayWidgetProperty> widget_property, - final List other) + final UndoableActionManager undo, + final Spinner node, + final ArrayWidgetProperty> widget_property, + final List other, + final int min_value, + final int max_value) { super(undo, node, widget_property, other); + this.min_value = min_value; + this.max_value = max_value; this.panel_section = panel_section; } @Override public void bind() { + jfx_node.getEditor().textProperty().addListener(text_listener); + jfx_node.setEditable(true); jfx_node.valueProperty().addListener(ui_listener); jfx_node.getValueFactory().setValue(widget_property.size()); - widget_property.addPropertyListener(prop_listener); } diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanel.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanel.java index caac164498..9f1171d135 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanel.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanel.java @@ -8,27 +8,28 @@ package org.csstudio.display.builder.editor.properties; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - +import javafx.beans.property.SimpleStringProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import org.csstudio.display.builder.editor.DisplayEditor; import org.csstudio.display.builder.editor.Messages; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetProperty; +import org.phoebus.framework.workbench.ApplicationService; import org.phoebus.ui.javafx.ClearingTextField; +import org.phoebus.ui.javafx.PlatformInfo; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; +import java.io.File; +import java.net.URI; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; /** Property UI * @@ -42,6 +43,10 @@ public class PropertyPanel extends BorderPane private final PropertyPanelSection section; private final TextField searchField = new ClearingTextField(); + private File file; + + private final SimpleStringProperty filePathProperty = new SimpleStringProperty(); + /** @param editor {@link DisplayEditor} */ public PropertyPanel (final DisplayEditor editor) @@ -60,12 +65,49 @@ public PropertyPanel (final DisplayEditor editor) toolsPane.setPadding(new Insets(6)); toolsPane.getChildren().add(searchField); + Label filePathCategoryLabel = new Label(Messages.FilePath); + filePathCategoryLabel.getStyleClass().add("property_category"); + filePathCategoryLabel.setMaxWidth(Double.MAX_VALUE); + + final HBox filePathPane = new HBox(); + filePathPane.getStyleClass().add("file_path_pane"); + Label filePathLabel = new Label(); + filePathLabel.setMaxWidth(Double.MAX_VALUE); + filePathLabel.getStyleClass().add("file_path"); + filePathLabel.textProperty().bind(filePathProperty); + filePathLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + HBox.setHgrow(filePathLabel, Priority.ALWAYS); + + Button openFileBrowser = new Button("..."); + openFileBrowser.setOnAction(e -> { + try { + String absolutePath = this.file.getParentFile().getAbsolutePath(); + String uriString; + if(PlatformInfo.isWindows){ + uriString = "file:/" + absolutePath.replace('\\', '/'); + } + else { + uriString = "file:" + absolutePath; + } + ApplicationService.createInstance("file_browser", new URI(uriString)); + } catch (Exception ex) { + Logger.getLogger(PropertyPanel.class.getName()) + .log(Level.WARNING, "Unable to launch file browser app", ex); + } + }); + openFileBrowser.setTooltip(new Tooltip(Messages.FileBrowserToolTip)); + filePathPane.getChildren().addAll(filePathLabel, openFileBrowser); + + final VBox topPane = new VBox(); + topPane.setAlignment(Pos.CENTER_LEFT); + topPane.getChildren().addAll(toolsPane, filePathCategoryLabel, filePathPane); + final ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); scrollPane.setContent(section); scrollPane.setMinHeight(0); - setTop(toolsPane); + setTop(topPane); setCenter(scrollPane); // Track currently selected widgets @@ -147,4 +189,14 @@ private void updatePropertiesView(final Set> properties, final section.setClassMode(model != null && model.isClassModel()); section.fill(editor.getUndoableActionManager(), filtered, other); } + + /** + * Sets the absolute file path text + * + * @param file OPI file being edited + */ + public void setFile(File file) { + this.file = file; + filePathProperty.set(file.getAbsolutePath()); + } } diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java index e8633986f0..d5fde2634c 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java @@ -13,7 +13,9 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.logging.Level; +import java.util.function.Consumer; import org.csstudio.display.builder.editor.DisplayEditor; import org.csstudio.display.builder.editor.Messages; @@ -43,6 +45,10 @@ import org.csstudio.display.builder.model.properties.RulesWidgetProperty; import org.csstudio.display.builder.model.properties.ScriptsWidgetProperty; import org.csstudio.display.builder.model.properties.WidgetClassProperty; +import org.csstudio.display.builder.model.properties.WidgetColor; +import org.csstudio.display.builder.model.properties.NamedWidgetColor; +import org.csstudio.display.builder.model.properties.ColorMap; +import org.csstudio.display.builder.model.properties.PredefinedColorMaps; import org.csstudio.display.builder.representation.javafx.FilenameSupport; import org.phoebus.framework.macros.MacroHandler; import org.phoebus.ui.autocomplete.PVAutocompleteMenu; @@ -207,7 +213,10 @@ public static Node bindSimplePropertyField ( { if (widget instanceof DisplayModel) { // DisplayModel is not registered as a Widget that users can add - field = new Label(Messages.Display, ImageCache.getImageView(ModelPlugin.class, "/icons/display.png")); + String text = Messages.Display; + Label label = new Label(text, ImageCache.getImageView(ModelPlugin.class, "/icons/display.png")); + Tooltip.install(label, new Tooltip(text)); + field = label; } else { @@ -216,13 +225,18 @@ public static Node bindSimplePropertyField ( { final ImageView icon = new ImageView(WidgetIcons.getIcon(type)); final String name = WidgetFactory.getInstance().getWidgetDescriptor(type).getName(); - field = new Label(name, icon); + Label label = new Label(name, icon); + Tooltip.install(label, new Tooltip(name)); + field = label; } catch (Exception ex) { // Even 'unknown' widgets should have an icon, // but fall back to just showing the type name - field = new Label(String.valueOf(property.getValue())); + String text = String.valueOf(property.getValue()); + Label label = new Label(text); + Tooltip.install(label, new Tooltip(text)); + field = label; } } } @@ -231,6 +245,7 @@ public static Node bindSimplePropertyField ( final TextField text = new TextField(); text.setText(String.valueOf(property.getValue())); text.setDisable(true); + Tooltip.install(text, new Tooltip(text.getText())); field = text; } } @@ -239,6 +254,25 @@ else if (property instanceof ColorWidgetProperty) final ColorWidgetProperty color_prop = (ColorWidgetProperty) property; final WidgetColorPropertyField color_field = new WidgetColorPropertyField(); final WidgetColorPropertyBinding binding = new WidgetColorPropertyBinding(undo, color_field, color_prop, other); + + Function widgetColorToTooltip = (widgetColor) -> + { + if (widgetColor instanceof NamedWidgetColor) { + return new Tooltip(((NamedWidgetColor) widgetColor).getName()); + } + else { + if (widgetColor.getAlpha() == 255) { + return new Tooltip("RGB(" + widgetColor.getRed() + "," + widgetColor.getGreen() + "," + widgetColor.getBlue() + ")"); + } + else { + return new Tooltip("RGB(" + widgetColor.getRed() + "," + widgetColor.getGreen() + "," + widgetColor.getBlue() + "," + widgetColor.getAlpha() + ")"); + } + } + }; + + Tooltip.install(color_field, widgetColorToTooltip.apply(color_prop.getValue())); + color_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(color_field, widgetColorToTooltip.apply(new_value))); + bindings.add(binding); binding.bind(); field = color_field; @@ -249,6 +283,10 @@ else if (property instanceof FontWidgetProperty) final Button font_field = new Button(); font_field.setMnemonicParsing(false); font_field.setMaxWidth(Double.MAX_VALUE); + + Tooltip.install(font_field, new Tooltip(font_prop.getValue().toString())); + font_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(font_field, new Tooltip(new_value.toString()))); + final WidgetFontPropertyBinding binding = new WidgetFontPropertyBinding(undo, font_field, font_prop, other); bindings.add(binding); binding.bind(); @@ -263,6 +301,9 @@ else if (property instanceof EnumWidgetProperty) combo.setMaxWidth(Double.MAX_VALUE); combo.setMaxHeight(Double.MAX_VALUE); + Tooltip.install(combo, new Tooltip(enum_prop.getValue().toString())); + enum_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(combo, new Tooltip(new_value.toString()))); + final ToggleButton macroButton = new ToggleButton("", ImageCache.getImageView(DisplayEditor.class, "/icons/macro-edit.png")); macroButton.getStyleClass().add("macro_button"); macroButton.setTooltip(new Tooltip(Messages.MacroEditButton)); @@ -276,7 +317,7 @@ else if (property instanceof EnumWidgetProperty) final EventHandler macro_handler = event -> { final boolean use_macro = macroButton.isSelected() || - MacroHandler.containsMacros(enum_prop.getSpecification()); + MacroHandler.containsMacros(enum_prop.getSpecification()); combo.setEditable(use_macro); // Combo's text field has been set to the current value // while the combo was non-editable. @@ -322,6 +363,25 @@ else if (property instanceof BooleanWidgetProperty) BorderPane.setMargin(macroButton, new Insets(0, 0, 0, 3)); BorderPane.setAlignment(macroButton, Pos.CENTER); + { + Tooltip tooltipWhenSetToTrue = new Tooltip("True"); + Tooltip tooltipWhenSetToFalse = new Tooltip("False"); + + Consumer setToolTip = (bool) -> { + if (bool) { + Tooltip.install(combo, tooltipWhenSetToTrue); + Tooltip.install(check, tooltipWhenSetToTrue); + } + else { + Tooltip.install(combo, tooltipWhenSetToTrue); + Tooltip.install(check, tooltipWhenSetToFalse); + } + }; + + setToolTip.accept(bool_prop.getValue()); + bool_prop.addPropertyListener((listener, old_value, new_value) -> setToolTip.accept(new_value)); + } + final BooleanWidgetPropertyBinding binding = new BooleanWidgetPropertyBinding(undo, check, combo, macroButton, bool_prop, other); bindings.add(binding); binding.bind(); @@ -351,6 +411,21 @@ else if (property instanceof ColorMapWidgetProperty) final ColorMapPropertyBinding binding = new ColorMapPropertyBinding(undo, map_button, colormap_prop, other); bindings.add(binding); binding.bind(); + + Function colorMapToTooltip = (colorMap) -> + { + if (colorMap instanceof PredefinedColorMaps.Predefined) { + PredefinedColorMaps.Predefined predefinedColorMap = (PredefinedColorMaps.Predefined) colorMap; + return new Tooltip(predefinedColorMap.getDescription()); + } + else { + return new Tooltip("Color Map"); + } + }; + + Tooltip.install(map_button, colorMapToTooltip.apply(colormap_prop.getValue())); + colormap_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(map_button, colorMapToTooltip.apply(new_value))); + field = map_button; } else if (property instanceof WidgetClassProperty) @@ -367,6 +442,10 @@ else if (property instanceof WidgetClassProperty) final WidgetClassBinding binding = new WidgetClassBinding(undo, combo, widget_class_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(combo, new Tooltip(widget_class_prop.getValue())); + widget_class_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(combo, new Tooltip(new_value))); + field = combo; } else if (property instanceof FilenameWidgetProperty) @@ -392,6 +471,11 @@ else if (property instanceof FilenameWidgetProperty) final MacroizedWidgetPropertyBinding binding = new MacroizedWidgetPropertyBinding(undo, text, file_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(select_file, new Tooltip("Select File")); + Tooltip.install(text, new Tooltip(file_prop.getValue())); + file_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(text, new Tooltip(new_value))); + field = new HBox(text, select_file); HBox.setHgrow(text, Priority.ALWAYS); // For RulesDialog, see above @@ -435,6 +519,11 @@ public void bind() } }); + + Tooltip.install(open_editor, new Tooltip("Open Editor")); + Tooltip.install(text, new Tooltip(pv_prop.getValue())); + pv_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(text, new Tooltip(new_value))); + field = new HBox(text, open_editor); HBox.setHgrow(text, Priority.ALWAYS); // For RulesDialog, see above @@ -449,13 +538,18 @@ else if (property instanceof MacroizedWidgetProperty) final MacroizedWidgetProperty macro_prop = (MacroizedWidgetProperty)property; final TextField text = new TextField(); text.setPromptText(macro_prop.getDefaultValue().toString()); + text.setOnKeyReleased((event) -> Tooltip.install(text, new Tooltip(text.getText()))); final MacroizedWidgetPropertyBinding binding = new MacroizedWidgetPropertyBinding(undo, text, macro_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(text, new Tooltip(text.getText())); + if (CommonWidgetProperties.propText.getName().equals(property.getName()) || - CommonWidgetProperties.propTooltip.getName().equals(property.getName())) + CommonWidgetProperties.propTooltip.getName().equals(property.getName())) { // Allow editing multi-line text in dialog final Button open_editor = new Button("..."); + Tooltip.install(open_editor, new Tooltip("Open Editor")); open_editor.setOnAction(event -> { final MultiLineInputDialog dialog = new MultiLineInputDialog(open_editor, macro_prop.getSpecification()); @@ -469,6 +563,8 @@ else if (property instanceof MacroizedWidgetProperty) final MacroizedWidgetProperty other_prop = (MacroizedWidgetProperty) w.getProperty(macro_prop.getName()); undo.execute(new SetMacroizedWidgetPropertyAction(other_prop, result.get())); } + text.setText(result.get().replaceAll("\n", "\\\\n")); + Tooltip.install(text, new Tooltip(result.get())); }); field = new HBox(text, open_editor); HBox.setHgrow(text, Priority.ALWAYS); @@ -490,6 +586,10 @@ else if (property instanceof PointsWidgetProperty) final PointsPropertyBinding binding = new PointsPropertyBinding(undo, points_field, points_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(points_field, new Tooltip(points_prop.getValue().size() + " Points")); + points_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(points_field, new Tooltip(new_value.size() + " Points"))); + field = points_field; } return field; @@ -503,11 +603,11 @@ else if (property instanceof PointsWidgetProperty) * @param indentationLevel Indentation level */ private void createPropertyUI( - final UndoableActionManager undo, - final WidgetProperty property, - final List other, - final int structureIndex, - final int indentationLevel) + final UndoableActionManager undo, + final WidgetProperty property, + final List other, + final int structureIndex, + final int indentationLevel) { // Skip runtime properties if (property.getCategory() == WidgetPropertyCategory.RUNTIME) @@ -535,6 +635,10 @@ else if (property instanceof MacrosWidgetProperty) final MacrosPropertyBinding binding = new MacrosPropertyBinding(undo, macros_field, macros_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(macros_field, new Tooltip(macros_field.getText())); + macros_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(macros_field, new Tooltip(macros_field.getText()))); + field = macros_field; } else if (property instanceof ActionsWidgetProperty) @@ -546,6 +650,10 @@ else if (property instanceof ActionsWidgetProperty) final ActionsPropertyBinding binding = new ActionsPropertyBinding(undo, actions_field, actions_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(actions_field, new Tooltip(actions_field.getText())); + actions_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(actions_field, new Tooltip(actions_field.getText()))); + field = actions_field; } else if (property instanceof ScriptsWidgetProperty) @@ -557,6 +665,10 @@ else if (property instanceof ScriptsWidgetProperty) final ScriptsPropertyBinding binding = new ScriptsPropertyBinding(undo, scripts_field, scripts_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(scripts_field, new Tooltip(scripts_field.getText())); + scripts_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(scripts_field, new Tooltip(scripts_field.getText()))); + field = scripts_field; } else if (property instanceof RulesWidgetProperty) @@ -567,6 +679,10 @@ else if (property instanceof RulesWidgetProperty) final RulesPropertyBinding binding = new RulesPropertyBinding(undo, rules_field, rules_prop, other); bindings.add(binding); binding.bind(); + + Tooltip.install(rules_field, new Tooltip(rules_field.getText())); + rules_prop.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(rules_field, new Tooltip(rules_field.getText()))); + field = rules_field; } else if (property instanceof StructuredWidgetProperty) @@ -595,8 +711,10 @@ else if (property instanceof ArrayWidgetProperty) final ArrayWidgetProperty> array = (ArrayWidgetProperty>) property; // UI for changing array size - final Spinner spinner = new Spinner<>(array.getMinimumSize(), 100, 0); - final ArraySizePropertyBinding count_binding = new ArraySizePropertyBinding(this, undo, spinner, array, other); + final int min_value = array.getMinimumSize(); + final int max_value = 100; + final Spinner spinner = new Spinner<>(min_value, max_value, 0); + final ArraySizePropertyBinding count_binding = new ArraySizePropertyBinding(this, undo, spinner, array, other, min_value, max_value); bindings.add(count_binding); count_binding.bind(); @@ -670,6 +788,10 @@ else if (property instanceof ArrayWidgetProperty) final TextField text = new TextField(); text.setText(String.valueOf(property.getValue())); text.setEditable(false); + + Tooltip.install(text, new Tooltip(text.getText())); + property.addPropertyListener((listener, old_value, new_value) -> Tooltip.install(text, new Tooltip(text.getText()))); + field = text; } @@ -700,7 +822,7 @@ else if (property instanceof ArrayWidgetProperty) final Widget widget = property.getWidget(); if (! (property == widget.getProperty("type") || - property == widget.getProperty("name"))) + property == widget.getProperty("name"))) { if (class_mode) { // Class definition mode: diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tracker/SelectedWidgetUITracker.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tracker/SelectedWidgetUITracker.java index 58081eef51..7010fa1682 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tracker/SelectedWidgetUITracker.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tracker/SelectedWidgetUITracker.java @@ -358,6 +358,11 @@ private void closeInlineEditor() inline_editor = null; } + private boolean requestFocusIsDisabled = false; + public void setDisableRequestFocus(boolean requestFocusIsDisabled) { + this.requestFocusIsDisabled = requestFocusIsDisabled; + } + /** Locate widgets that would be 'clicked' by a mouse event's location */ private class ClickWidgets extends RecursiveTask { @@ -635,9 +640,10 @@ public void setSelectedWidgets(final List widgets) bindToWidgets(); - // Get focus to allow use of arrow keys - Platform.runLater(() -> tracker.requestFocus()); - + if (!requestFocusIsDisabled) { + // Get focus to allow use of arrow keys + Platform.runLater(() -> tracker.requestFocus()); + } } @Override diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTree.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTree.java index 5112c65739..5a8b5017f7 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTree.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTree.java @@ -331,9 +331,11 @@ private void bindSelections() : wot.getTab().getWidget(); if (! widgets.contains(widget)) widgets.add(widget); - }; + } logger.log(Level.FINE, "Selected in tree: {0}", widgets); + editor.getSelectedWidgetUITracker().setDisableRequestFocus(true); editor.getWidgetSelectionHandler().setSelection(widgets); + editor.getSelectedWidgetUITracker().setDisableRequestFocus(false); } finally { diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTreeCell.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTreeCell.java index 432eb8097b..c5aea5ef2c 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTreeCell.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/tree/WidgetTreeCell.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2017 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -136,7 +136,9 @@ else if (item.isWidget()) // Extra icon for widgets with macros defined Optional> optMacros = widget.checkProperty(propMacros); final String iconName = "/icons/macro_hint.png"; - if (optMacros.isPresent() && optMacros.get().getValue().getNames().size() > 0) + + + if (optMacros.isPresent() && !optMacros.get().getValue().isEmpty()) { ImageView macroIcon = ImageCache.getImageView(DisplayEditor.class.getResource(iconName)); graphic = new HBox(graphic, macroIcon); @@ -146,7 +148,7 @@ else if (item.isWidget()) NavigationTabsWidget tabs = (NavigationTabsWidget)widget; for(NavigationTabsWidget.TabProperty tab : tabs.propTabs().getValue()) { - if (tab.macros().getValue().getNames().size() > 0) + if (! tab.macros().getValue().isEmpty()) { ImageView macroIcon = ImageCache.getImageView(DisplayEditor.class.getResource(iconName)); graphic = new HBox(graphic, macroIcon); diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/undo/SortWidgetsAction.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/undo/SortWidgetsAction.java new file mode 100644 index 0000000000..316898c4e5 --- /dev/null +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/undo/SortWidgetsAction.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2023 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.csstudio.display.builder.editor.undo; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.csstudio.display.builder.editor.DisplayEditor; +import org.csstudio.display.builder.editor.Messages; +import org.csstudio.display.builder.model.ChildrenProperty; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.widgets.GroupWidget; +import org.csstudio.display.builder.model.widgets.TabsWidget; +import org.csstudio.display.builder.model.widgets.TabsWidget.TabItemProperty; +import org.phoebus.ui.undo.UndoableAction; + +/** Action to sort child widgets of display or group + * @author Kay Kasemir + */ +public class SortWidgetsAction extends UndoableAction +{ + /** @param editor {@link DisplayEditor} + * @return {@link Widget} for model or selected group, or null + */ + public static Widget getWidgetToSort(final DisplayEditor editor) + { + final List selection = editor.getWidgetSelectionHandler().getSelection(); + if (selection.isEmpty()) + return editor.getModel(); + if (selection.size() == 1 && selection.get(0) instanceof GroupWidget) + return selection.get(0); + return null; + } + + /** Children of model or a selected group */ + private final ChildrenProperty children; + + /** Original widget order for 'children' and sub-widgets */ + private final List> originals = new LinkedList<>(); + + /** @param editor {@link DisplayEditor} */ + public SortWidgetsAction(final DisplayEditor editor) + { + super(Messages.SortWidgets); + final Widget widget = getWidgetToSort(editor); + children = ChildrenProperty.getChildren(widget); + } + + @Override + public void run() + { + originals.clear(); + sort(children); + } + + private void sort(final ChildrenProperty children) + { + // Get original order + final List original = new ArrayList<>(children.getValue()); + originals.add(original); + + // Recurse.. + for (Widget child : original) + if (child instanceof TabsWidget) + for (TabItemProperty tab : ((TabsWidget)child).propTabs().getValue()) + sort(tab.children()); + else + { + final ChildrenProperty sub = ChildrenProperty.getChildren(child); + if (sub != null) + sort(sub); + } + + // .. then sort on this level + final List sorted = new ArrayList<>(original); + sorted.sort((a, b) -> + { + // Sort top..bottom, fall back to left..right and finally sort by name + int order = a.propY().getValue() - b.propY().getValue(); + if (order == 0) + order = a.propX().getValue() - b.propX().getValue(); + if (order == 0) + order = a.getName().compareTo(b.getName()); + return order; + }); + + // Delete from end of list, if only to avoid 'moving' remaining items + for (int i=original.size()-1; i>=0; --i) + children.removeChild(original.get(i)); + for (Widget w : sorted) + children.addChild(w); + } + + @Override + public void undo() + { + restore(children); + } + + private void restore(final ChildrenProperty children) + { + // Restore order for this item ... + final List original = originals.remove(0); + final List sorted = new ArrayList<>(children.getValue()); + for (int i=sorted.size()-1; i>=0; --i) + children.removeChild(sorted.get(i)); + for (Widget w : original) + children.addChild(w); + + // .. then recurse + for (Widget child : original) + if (child instanceof TabsWidget) + for (TabItemProperty tab : ((TabsWidget)child).propTabs().getValue()) + restore(tab.children()); + else + { + final ChildrenProperty sub = ChildrenProperty.getChildren(child); + if (sub != null) + restore(sub); + } + } +} diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/util/ParentHandler.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/util/ParentHandler.java index 12252b2344..0dcbd577cf 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/util/ParentHandler.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/util/ParentHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,6 +19,7 @@ import org.csstudio.display.builder.model.widgets.ArrayWidget; import org.csstudio.display.builder.model.widgets.GroupWidget; import org.csstudio.display.builder.model.widgets.TabsWidget; +import org.csstudio.display.builder.model.widgets.TabsWidget.TabItemProperty; import org.csstudio.display.builder.model.widgets.VisibleWidget; import javafx.geometry.Rectangle2D; @@ -137,8 +138,9 @@ private ParentSearchResult findParent(final List children) else if (widget instanceof TabsWidget) { // Check children of _selected_ Tab final TabsWidget tabwid = (TabsWidget) widget; - final int selected = tabwid.propActiveTab().getValue(); - child_prop = tabwid.propTabs().getValue().get(selected).children(); + final List tabs = tabwid.propTabs().getValue(); + int selected = Math.min(tabwid.propActiveTab().getValue(), tabs.size()-1); + child_prop = tabs.get(selected).children(); } else if (widget instanceof ArrayWidget) { diff --git a/app/display/editor/src/main/resources/icons/sort.png b/app/display/editor/src/main/resources/icons/sort.png new file mode 100644 index 0000000000..0a54b9aad0 Binary files /dev/null and b/app/display/editor/src/main/resources/icons/sort.png differ diff --git a/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/messages.properties b/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/messages.properties index 31d1fc6b58..aba46f9d24 100644 --- a/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/messages.properties +++ b/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/messages.properties @@ -29,8 +29,10 @@ DownloadTitle=Download remote file Duplicate=Duplicate Widgets EditEmbededDisplay=Edit Embedded Display ExpandTree=Expand All Tree Items +FileBrowserToolTip=Open parent dir in File Browser FileChangedHdr=File has changed FileChangedDlg=The file\n {0}\nhas been changed while you were editing it.\n\n'OK' to save and thus overwrite what somebody else has written,\nor\n'Cancel' and then re-load the file or save it under a different name. +FilePath=File Path FindWidget=Find Widget... Grid=Align widgets to Grid LoadDisplay=Load display @@ -77,6 +79,7 @@ ShowCoordinates=Show Coordinates ShowCrosshair=Show Crosshair Cursor Size=Size Snap=Snap to widgets +SortWidgets=Sort Widgets UpdateWidgetLocation=Update Widget Location UpdateWidgetOrder=Update Widget Order UseWidgetClass_TT=Include this property in definition of widget class? diff --git a/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/opieditor.css b/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/opieditor.css index 7aa8799e43..de31739148 100644 --- a/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/opieditor.css +++ b/app/display/editor/src/main/resources/org/csstudio/display/builder/editor/opieditor.css @@ -82,6 +82,16 @@ -fx-background-radius: 0; } +.file_path +{ + -fx-padding: 3 5 0 5; +} + +.file_path_pane +{ + -fx-padding: 2 0 1 0; +} + .structure_property_name { -fx-font-weight: bold; diff --git a/app/display/fonts/pom.xml b/app/display/fonts/pom.xml index ec918199ca..c72e46a094 100644 --- a/app/display/fonts/pom.xml +++ b/app/display/fonts/pom.xml @@ -3,14 +3,14 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-fonts org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/linearmeter/build.xml b/app/display/linearmeter/build.xml new file mode 100644 index 0000000000..797ad339a2 --- /dev/null +++ b/app/display/linearmeter/build.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/display/linearmeter/pom.xml b/app/display/linearmeter/pom.xml new file mode 100644 index 0000000000..9bdaa4209b --- /dev/null +++ b/app/display/linearmeter/pom.xml @@ -0,0 +1,39 @@ + + + + + app-display + org.phoebus + 4.7.4-SNAPSHOT + + + 4.0.0 + + app-display-linearmeter + + + 11 + 11 + + + + + org.phoebus + app-display-model + 4.7.4-SNAPSHOT + compile + + + org.phoebus + app-display-representation + 4.7.4-SNAPSHOT + compile + + + org.phoebus + app-display-representation-javafx + 4.7.4-SNAPSHOT + compile + + + diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterRepresentationService.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterRepresentationService.java new file mode 100644 index 0000000000..fe416614e8 --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterRepresentationService.java @@ -0,0 +1,21 @@ +package org.csstudio.display.extra.widgets; + +import org.csstudio.display.builder.model.WidgetDescriptor; +import org.csstudio.display.builder.representation.WidgetRepresentation; +import org.csstudio.display.builder.representation.WidgetRepresentationFactory; +import org.csstudio.display.builder.representation.spi.WidgetRepresentationsService; +import org.csstudio.display.extra.widgets.linearmeter.LinearMeterRepresentation; +import org.csstudio.display.extra.widgets.linearmeter.LinearMeterWidget; + +import java.util.Map; + +import static java.util.Map.entry; + +public class LinearMeterRepresentationService implements WidgetRepresentationsService { + @SuppressWarnings({"unchecked", "rawtypes", "nls"}) + @Override + public Map> getWidgetRepresentationFactories() { + return Map.ofEntries( + entry(LinearMeterWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new LinearMeterRepresentation())); + } +} diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterWidgetService.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterWidgetService.java new file mode 100644 index 0000000000..15fd208cb3 --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/LinearMeterWidgetService.java @@ -0,0 +1,22 @@ +package org.csstudio.display.extra.widgets; + +import org.csstudio.display.builder.model.WidgetDescriptor; +import org.csstudio.display.builder.model.spi.WidgetsService; +import org.csstudio.display.extra.widgets.linearmeter.LinearMeterWidget; + +import java.util.Collection; +import java.util.List; + +/** + * A widget service for which provided additional widgets + */ +public class LinearMeterWidgetService implements WidgetsService +{ + @Override + public Collection getWidgetDescriptors() + { + return List.of( + LinearMeterWidget.WIDGET_DESCRIPTOR + ); + } +} diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterRepresentation.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterRepresentation.java new file mode 100644 index 0000000000..a7e9aca9ed --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterRepresentation.java @@ -0,0 +1,391 @@ +package org.csstudio.display.extra.widgets.linearmeter; + +import java.util.ArrayDeque; +import java.util.Deque; + +import javafx.util.Pair; + +import org.csstudio.display.builder.model.BaseWidgetPropertyListener; +import org.csstudio.display.builder.model.DirtyFlag; +import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyListener; +import org.csstudio.display.builder.model.properties.PropertyChangeHandler; +import org.csstudio.display.builder.model.properties.WidgetColor; +import org.csstudio.display.builder.representation.javafx.JFXUtil; +import org.csstudio.display.builder.representation.javafx.widgets.RegionBaseRepresentation; +import org.epics.util.stats.Range; + +import javafx.scene.layout.Pane; +import org.epics.vtype.Display; +import org.epics.vtype.VDouble; + +@SuppressWarnings("nls") +public class LinearMeterRepresentation extends RegionBaseRepresentation +{ + private DirtyFlag dirty_look = new DirtyFlag(); + + private UntypedWidgetPropertyListener layoutChangedListener = this::layoutChanged; + private WidgetPropertyListener orientationChangedListener = this::orientationChanged; + private UntypedWidgetPropertyListener valueListener = this::valueChanged; + private WidgetPropertyListener widthChangedListener = this::widthChanged; + private WidgetPropertyListener heightChangedListener = this::heightChanged; + private RTLinearMeter meter; + + private java.awt.Color widgetColorToAWTColor(WidgetColor widgetColor) { + return new java.awt.Color(widgetColor.getRed(), widgetColor.getGreen(), widgetColor.getBlue(), widgetColor.getAlpha()); + } + + @Override + public Pane createJFXNode() + { + double initialValue = toolkit.isEditMode() ? (model_widget.propMinimum().getValue() + model_widget.propMaximum().getValue()) / 2.0 + : Double.NaN; + + double minimum, maximum; + double loLo, low, high, hiHi; + if (model_widget.propLimitsFromPV().getValue() && !toolkit.isEditMode()) { + minimum = Double.NaN; + maximum = Double.NaN; + loLo = Double.NaN; + low = Double.NaN; + high = Double.NaN; + hiHi = Double.NaN; + } + else { + minimum = model_widget.propMinimum().getValue(); + maximum = model_widget.propMaximum().getValue(); + loLo = model_widget.propLevelLoLo().getValue(); + low = model_widget.propLevelLow().getValue(); + high = model_widget.propLevelHigh().getValue(); + hiHi = model_widget.propLevelHiHi().getValue(); + } + + meter = new RTLinearMeter(initialValue, + model_widget.propWidth().getValue(), + model_widget.propHeight().getValue(), + minimum, + maximum, + loLo, + low, + high, + hiHi, + model_widget.propShowUnits().getValue(), + model_widget.propShowLimits().getValue(), + model_widget.propDisplayHorizontal().getValue(), + model_widget.propIsGradientEnabled().getValue(), + model_widget.propIsHighlightActiveRegionEnabled().getValue(), + widgetColorToAWTColor(model_widget.propNormalStatusColor().getValue()), + widgetColorToAWTColor(model_widget.propMinorWarningColor().getValue()), + widgetColorToAWTColor(model_widget.propMajorWarningColor().getValue()), + model_widget.propNeedleWidth().getValue(), + widgetColorToAWTColor(model_widget.propNeedleColor().getValue()), + model_widget.propKnobSize().getValue(), + widgetColorToAWTColor(model_widget.propKnobColor().getValue())); + meter.setSize(model_widget.propWidth().getValue(),model_widget.propHeight().getValue()); + meter.setHorizontal(model_widget.propDisplayHorizontal().getValue()); + meter.setManaged(false); + return new Pane(meter); + } + + Deque> widgetPropertiesWithWidgetPropertyListeners = new ArrayDeque<>(); + private void addWidgetPropertyListener(WidgetProperty widgetProperty, WidgetPropertyListener widgetPropertyListener) { + widgetProperty.addPropertyListener(widgetPropertyListener); + widgetPropertiesWithWidgetPropertyListeners.push(new Pair(widgetProperty, widgetPropertyListener)); + } + private void addUntypedWidgetPropertyListener(WidgetProperty widgetProperty, UntypedWidgetPropertyListener widgetPropertyListener) { + widgetProperty.addUntypedPropertyListener(widgetPropertyListener); + widgetPropertiesWithWidgetPropertyListeners.push(new Pair(widgetProperty, widgetPropertyListener)); + } + + @Override + protected void registerListeners() + { + super.registerListeners(); + + addWidgetPropertyListener(model_widget.propWidth(), widthChangedListener); + addWidgetPropertyListener(model_widget.propHeight(), heightChangedListener); + addUntypedWidgetPropertyListener(model_widget.propForeground(), layoutChangedListener); + addUntypedWidgetPropertyListener(model_widget.propBackground(), layoutChangedListener); + addUntypedWidgetPropertyListener(model_widget.propScaleVisible(), layoutChangedListener); + addUntypedWidgetPropertyListener(model_widget.propFont(), layoutChangedListener); + addUntypedWidgetPropertyListener(model_widget.propNeedleColor(), layoutChangedListener); + + addWidgetPropertyListener(model_widget.propShowUnits(), (property, old_value, new_value) -> { + meter.setShowUnits(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propShowLimits(), (property, old_value, new_value) -> { + meter.setShowLimits(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propIsGradientEnabled(), (property, old_value, new_value) -> { + meter.setIsGradientEnabled(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propIsHighlightActiveRegionEnabled(), (property, old_value, new_value) -> { + meter.setIsHighlightActiveRegionEnabled(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propNormalStatusColor(), (property, old_value, new_value) -> { + meter.setNormalStatusColor(widgetColorToAWTColor(new_value)); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propMinorWarningColor(), (property, old_value, new_value) -> { + meter.setMinorAlarmColor(widgetColorToAWTColor(new_value)); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propMajorWarningColor(), (property, old_value, new_value) -> { + meter.setMajorAlarmColor(widgetColorToAWTColor(new_value)); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propNeedleWidth(), (property, old_value, new_value) -> { + meter.setNeedleWidth(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propNeedleColor(), (property, old_value, new_value) -> { + meter.setNeedleColor(widgetColorToAWTColor(new_value)); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propMinimum(), (property, old_value, new_value) -> { + + synchronized (meter) { + boolean validRange = Double.isFinite(new_value) && Double.isFinite(model_widget.propMaximum().getValue()); + meter.setRange(new_value, model_widget.propMaximum().getValue(), validRange); + if (toolkit.isEditMode() && validRange) { + meter.setCurrentValue((new_value + model_widget.propMaximum().getValue()) / 2.0); + } + } + + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propMaximum(), (property, old_value, new_value) -> { + + synchronized (meter) { + boolean validRange = Double.isFinite(new_value) && Double.isFinite(model_widget.propMinimum().getValue()); + meter.setRange(model_widget.propMinimum().getValue(), new_value, validRange); + if (toolkit.isEditMode() && validRange) { + meter.setCurrentValue((new_value + model_widget.propMinimum().getValue()) / 2.0); + } + } + + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propLevelLoLo(), (property, old_value, new_value) -> { + meter.setLoLo(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propLevelLow(), (property, old_value, new_value) -> { + meter.setLow(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propLevelHigh(), (property, old_value, new_value) -> { + meter.setHigh(new_value); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propLevelHiHi(), (property, old_value, new_value) -> { + meter.setHiHi(new_value); + layoutChanged(null, null, null); + }); + + addUntypedWidgetPropertyListener(model_widget.runtimePropValue(), valueListener); + + addWidgetPropertyListener(model_widget.propDisplayHorizontal(), orientationChangedListener); + + addWidgetPropertyListener(model_widget.propKnobColor(), (property, old_value, new_value) -> { + meter.setKnobColor(widgetColorToAWTColor(new_value)); + layoutChanged(null, null, null); + }); + + addWidgetPropertyListener(model_widget.propKnobSize(), (property, old_value, new_value) -> { + meter.setKnobSize(new_value); + layoutChanged(null, null, null); + }); + } + + @Override + protected void unregisterListeners() + { + for (var widgetPropertyWithWidgetPropertyListener : widgetPropertiesWithWidgetPropertyListeners) { + var widgetProperty = widgetPropertyWithWidgetPropertyListener.getKey(); + var widgetPropertyListener = widgetPropertyWithWidgetPropertyListener.getValue(); + widgetProperty.removePropertyListener(widgetPropertyListener); + } + widgetPropertiesWithWidgetPropertyListeners.clear(); + + super.unregisterListeners(); + } + + int minimumSize = 25; + private void widthChanged(WidgetProperty prop, Integer old_value, Integer new_value) + { + if (new_value < minimumSize) { + prop.setValue(minimumSize); + return; + } + layoutChanged(prop, old_value, new_value); + } + + private void heightChanged(WidgetProperty prop, Integer old_value, Integer new_value) + { + if (new_value < minimumSize) { + prop.setValue(minimumSize); + return; + } + layoutChanged(prop, old_value, new_value); + } + + private void orientationChanged(WidgetProperty prop, Boolean old, Boolean horizontal) + { + if (toolkit.isEditMode()) + { + synchronized(meter) { + int w = model_widget.propWidth().getValue(); + int h = model_widget.propHeight().getValue(); + model_widget.propWidth().setValue(h); + model_widget.propHeight().setValue(w); + meter.setHorizontal(horizontal); + } + layoutChanged(null, null, null); + } + } + + private void layoutChanged(WidgetProperty property, Object old_value, Object new_value) + { + dirty_look.mark(); + toolkit.scheduleUpdate(this); + } + + private double observedMin = Double.NaN; + private double observedMax = Double.NaN; + private void valueChanged(WidgetProperty property, Object old_value, Object new_value) + { + synchronized (meter) { // "synchronized (meter) { ... }" is due to the reading of values from "meter.linearMeter", + // which may otherwise be interleaved with writes to these values. + if (new_value instanceof VDouble ) { + VDouble vDouble = ((VDouble) new_value); + double newValue = vDouble.getValue(); + meter.setCurrentValue(newValue); + + if (!Double.isNaN(newValue)) + if (Double.isNaN(observedMin) || newValue < observedMin) { + observedMin = newValue; + } + + if (Double.isNaN(observedMax) || newValue > observedMax) { + observedMax = newValue; + } + + Display display = vDouble.getDisplay(); + + // Set the units: + if (model_widget.propShowUnits().getValue() ) { + meter.setUnits(display.getUnit()); + } + + if (model_widget.propLimitsFromPV().getValue()) { + Range displayRange = display.getDisplayRange(); + if ( displayRange != null + && Double.isFinite(displayRange.getMinimum()) + && Double.isFinite(displayRange.getMaximum()) + && displayRange.getMaximum() - displayRange.getMinimum() > 0.0) { + if (meter.linearMeterScale.getValueRange().getLow() != displayRange.getMinimum() || meter.linearMeterScale.getValueRange().getHigh() != displayRange.getMaximum() || !meter.getValidRange()) { + meter.setRange(displayRange.getMinimum(), displayRange.getMaximum(), true); + } + } + else { + Range controlRange = display.getControlRange(); + if ( controlRange != null + && Double.isFinite(controlRange.getMinimum()) + && Double.isFinite(controlRange.getMaximum()) + && controlRange.getMaximum() - controlRange.getMinimum() > 0.0) { + if (meter.linearMeterScale.getValueRange().getLow() != controlRange.getMinimum() || meter.linearMeterScale.getValueRange().getHigh() != controlRange.getMaximum() || !meter.getValidRange()) { + meter.setRange(controlRange.getMinimum(), controlRange.getMaximum(), true); + } + } + else if (!Double.isNaN(observedMin) && !Double.isNaN(observedMax)) { + meter.setRange(observedMin - 1, observedMax + 1, false); + } + else { + meter.setRange(0.0, 100.0, false); + } + } + + { + Range warningRange = display.getWarningRange(); + if (warningRange != null) { + if (!Double.isNaN(warningRange.getMinimum()) && meter.getLow() != warningRange.getMinimum()) { + meter.setLow(warningRange.getMinimum()); + } + + if (!Double.isNaN(warningRange.getMaximum()) && meter.getHigh() != warningRange.getMaximum()) { + meter.setHigh(warningRange.getMaximum()); + } + } + } + + { + Range alarmRange = display.getAlarmRange(); + if (alarmRange != null) { + if (!Double.isNaN(alarmRange.getMinimum()) && meter.getLoLo() != alarmRange.getMinimum()) { + meter.setLoLo(alarmRange.getMinimum()); + } + + if (!Double.isNaN(alarmRange.getMaximum()) && meter.getHiHi() != alarmRange.getMaximum()) { + meter.setHiHi(alarmRange.getMaximum()); + } + } + } + } + } + } + } + + @Override + public void updateChanges() + { + super.updateChanges(); + + if (dirty_look.checkAndClear()) + { + synchronized (meter) { + boolean horizontal = model_widget.propDisplayHorizontal().getValue(); + int width = model_widget.propWidth().getValue(); + int height = model_widget.propHeight().getValue(); + meter.linearMeterScale.setHorizontal(horizontal); + + meter.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); + meter.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); + + meter.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + + meter.setScaleVisible(model_widget.propScaleVisible().getValue()); + + jfx_node.setPrefSize(width, height); + + meter.setSize(width, height); + } + } + } + + @Override + public void dispose() + { + meter.dispose(); + super.dispose(); + } +} diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterScale.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterScale.java new file mode 100644 index 0000000000..f17d6db822 --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterScale.java @@ -0,0 +1,176 @@ +package org.csstudio.display.extra.widgets.linearmeter; + +import java.awt.*; +import java.awt.geom.AffineTransform; + +import org.csstudio.javafx.rtplot.internal.*; +import org.csstudio.javafx.rtplot.internal.util.GraphicsUtils; + + +public class LinearMeterScale extends NumericAxis +{ + private int p1x, p1y; + private double scale; + private double offset; + + public boolean isHorizontal() { + return isHorizontal; + } + + private boolean isHorizontal = true; + + public synchronized void setHorizontal(boolean v){ + this.isHorizontal = v; + } + + public void computeTicks(Graphics2D gc) { super.computeTicks(gc);} + + public Ticks getTicks() { + return ticks; + } + + public int getTickLength() { + return TICK_LENGTH; + } + + /** Create scale with label and listener. */ + public LinearMeterScale(PlotPartListener listener, + int width, + int height, + boolean horizontal, + double min, + double max) + { + super("", listener, horizontal, min, max); + super.setBounds(0, 0, width, height); + isHorizontal = horizontal; + } + + /** Configure scale layout + * @param p1x + * @param p1y + * @param s + */ + public void configure(int p1x, int p1y, double s) + { + this.p1x = p1x; + this.p1y = p1y; + this.scale = s; + this.offset = this.range.getLow(); + + dirty_ticks = true; + requestLayout(); + } + + @Override + public int getDesiredPixelSize(Rectangle region, Graphics2D gc) + { + // Not used + return 0; + } + + /** {@inheritDoc} */ + @Override + public void paint(Graphics2D gc, Rectangle plot_bounds) + { + + if (!isVisible()){ + return; + } + + Stroke old_width = gc.getStroke(); + Color old_fg = gc.getColor(); + Color foreground = GraphicsUtils.convert(getColor()); + gc.setFont(scale_font); + + super.paint(gc); + + computeTicks(gc); + + gc.setColor(foreground); + + //Draw axis + gc.setColor(foreground); + + // Major tick marks + gc.setStroke(TICK_STROKE); + int start_x = this.p1x; + int start_y = this.p1y; + + if (isHorizontal) + { + FontMetrics scale_font_fontMetrics = gc.getFontMetrics(scale_font); + + for (MajorTick tick : ticks.getMajorTicks()) { + gc.drawLine((int) ((start_x + this.scale * (tick.getValue() - offset) )), + (int) ((start_y - 0.5 * TICK_LENGTH)), + (int) ((start_x + this.scale * (tick.getValue() - offset) )), + (int) ((start_y + 0.5 * TICK_LENGTH))); + drawTickLabel(gc, + (int) ((start_x + this.scale * (tick.getValue() - offset) - scale_font_fontMetrics.stringWidth(tick.getLabel())/2)), + (int) (start_y + 0.5 * TICK_LENGTH + 2 + Math.round(Math.ceil((72.0 * (scale_font_fontMetrics.getAscent())) / 96.0))), + tick.getLabel()); + } + } else { + + for (MajorTick tick : ticks.getMajorTicks()) { + gc.drawLine((int) (start_x - 0.5 * TICK_LENGTH), + (int) (start_y - this.scale * (tick.getValue() - offset)), + (int) (start_x + 0.5 * TICK_LENGTH), + (int) (start_y - this.scale * (tick.getValue() - offset))); + drawTickLabel(gc, + (int) (start_x + 4), + (int) (start_y - this.scale * (tick.getValue() - offset) + Math.round((72.0 * scale_font.getSize()) / (96.0 * 2.0))), + tick.getLabel()); + } + } + + gc.setStroke(old_width); + + // Minor tick marks + if(isHorizontal) { + for (MinorTick tick : ticks.getMinorTicks()) { + gc.drawLine((int) ((start_x + this.scale * (tick.getValue() - offset) )), + (int) (start_y - 0.5 * TICK_LENGTH), + (int) ((start_x + this.scale * (tick.getValue() - offset) )), + (int) (start_y + 0.5 * TICK_LENGTH)); + } + } else { + for (MinorTick tick : ticks.getMinorTicks()) { + gc.drawLine((int) (start_x - 0.5 * TICK_LENGTH), + (int) (start_y - this.scale * (tick.getValue() - offset) ), + (int) (start_x + 0.5 * TICK_LENGTH), + (int) (start_y - this.scale * (tick.getValue() - offset) )); + } + } + + gc.setColor(old_fg); + + } + + private void drawTickLabel(Graphics2D gc, + int cx, + int cy, + String mark) + { + gc.setFont(scale_font); + + if(!isHorizontal){ + AffineTransform orig = gc.getTransform(); + gc.translate(cx, cy); + gc.drawString(mark, 6, 0); + gc.setTransform(orig); + } else { + AffineTransform orig = gc.getTransform(); + gc.translate(cx, cy); + gc.drawString(mark, 0, 0); + gc.setTransform(orig); + } + } + + @Override + public void drawTickLabel(Graphics2D gc, Double tick) + { + // NOP + } +} diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterWidget.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterWidget.java new file mode 100644 index 0000000000..cbac432785 --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/LinearMeterWidget.java @@ -0,0 +1,349 @@ + +package org.csstudio.display.extra.widgets.linearmeter; + +import java.util.Arrays; +import java.util.List; + +import org.csstudio.display.builder.model.*; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.NamedWidgetColors; +import org.csstudio.display.builder.model.persist.NamedWidgetFonts; +import org.csstudio.display.builder.model.persist.WidgetColorService; +import org.csstudio.display.builder.model.persist.WidgetFontService; +import org.csstudio.display.builder.model.properties.WidgetColor; +import org.csstudio.display.builder.model.properties.WidgetFont; +import org.csstudio.display.builder.model.widgets.PVWidget; +import org.phoebus.framework.persistence.XMLUtil; +import org.phoebus.ui.vtype.FormatOption; +import org.w3c.dom.Element; + +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.*; + +@SuppressWarnings("nls") +public class LinearMeterWidget extends PVWidget +{ + public LinearMeterWidget() + { + super(WIDGET_DESCRIPTOR.getType(), 240, 120); + } + + public static WidgetDescriptor WIDGET_DESCRIPTOR = + new WidgetDescriptor("linearmeter", WidgetCategory.MONITOR, + "LinearMeter", + "/icons/linear-meter.png", + "Compact monitor widget for the value of a PV.", + Arrays.asList("")) + { + @Override + public Widget createWidget() + { + return new LinearMeterWidget(); + } + }; + + /** + * 1.0.0: Linear meter by Claudio Rosatti based on 3rd party library + * 2.0.0: Simple linear meter, based on meter v 3.0.0, drawn in background + */ + public static Version METER_VERSION = new Version(2, 0, 0); + + /** Custom configurator to read legacy files */ + protected static class LinearMeterConfigurator extends WidgetConfigurator + { + //TODO: This has to be fixed for the Linear Meter. Current implementation is + // for the Meter widget and is not valid. No version 3. + + public LinearMeterConfigurator(Version xmlVersion) + { + super(xmlVersion); + } + + @Override + public boolean configureFromXML(ModelReader reader, Widget widget, + Element xml) throws Exception + { + if (!super.configureFromXML(reader, widget, xml)) + return false; + + LinearMeterWidget meter = (LinearMeterWidget) widget; + + if (xml_version.getMajor() < 2) + { // BOY + + Element e = XMLUtil.getChildElement(xml, "scale_font"); + if (e != null) + meter.propFont().readFromXML(reader, e); + + // Are any of the limits disabled, or 'Show Ramp' disabled? + if ((!XMLUtil.getChildBoolean(xml, "show_hihi").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_hi").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_lo").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_lolo").orElse(true) + ) + || + !XMLUtil.getChildBoolean(xml, "show_markers").orElse(true)) + meter.propShowLimits().setValue(false); + } + else if (xml_version.getMajor() < 3) + { // Display Builder meter based on 3rd party JFX lib + XMLUtil.getChildBoolean(xml, "unit_from_pv") + .ifPresent(meter.propShowUnits()::setValue); + + if (!XMLUtil.getChildBoolean(xml, "show_hihi").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_high").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_low").orElse(true) && + !XMLUtil.getChildBoolean(xml, "show_lolo").orElse(true)) + meter.propShowLimits().setValue(false); + } + + return true; + } + } + + public static WidgetPropertyDescriptor propShowLimits = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_limits", Messages.WidgetProperties_ShowLimits); + + public static WidgetPropertyDescriptor propDisplayHorizontal = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "displayHorizontal", Messages.WidgetProperties_Horizontal); + + public static WidgetPropertyDescriptor propScaleVisible = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "scale_visible", Messages.WidgetProperties_ScaleVisible); + + public static WidgetPropertyDescriptor propNeedleColor = + newColorPropertyDescriptor(WidgetPropertyCategory.MISC, "needle_color", Messages.WidgetProperties_NeedleColor); + + public static WidgetPropertyDescriptor knobColor_descriptor = + newColorPropertyDescriptor(WidgetPropertyCategory.MISC, "knob_color", Messages.WidgetProperties_KnobColor); + + public static WidgetPropertyDescriptor knobSize_descriptor = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "knob_size", "Knob Size"); + + public static WidgetPropertyDescriptor propLevelHiHi = + newDoublePropertyDescriptor (WidgetPropertyCategory.BEHAVIOR, "level_hihi", Messages.WidgetProperties_LevelHiHi); + + public static WidgetPropertyDescriptor propLevelHigh = + newDoublePropertyDescriptor (WidgetPropertyCategory.BEHAVIOR, "level_high", Messages.WidgetProperties_LevelHigh); + + public static WidgetPropertyDescriptor propLevelLoLo = + newDoublePropertyDescriptor (WidgetPropertyCategory.BEHAVIOR, "level_lolo", Messages.WidgetProperties_LevelLoLo); + + public static WidgetPropertyDescriptor propLevelLow = + newDoublePropertyDescriptor (WidgetPropertyCategory.BEHAVIOR, "level_low", Messages.WidgetProperties_LevelLow); + + public static StructuredWidgetProperty.Descriptor colorsStructuredWidget_descriptor = + new StructuredWidgetProperty.Descriptor(WidgetPropertyCategory.DISPLAY, "colors", "Colors"); + + public static WidgetPropertyDescriptor propNormalStatusColor_descriptor = + newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "normal_status_color", "Normal Status Color"); + + public static WidgetPropertyDescriptor minorWarningColor_descriptor = + newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "minor_warning_color", "Low & High Warning Color"); + + public static WidgetPropertyDescriptor majorWarningColor_descriptor = + newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "major_warning_color", "LoLo & HiHi Warning Color"); + + public static WidgetPropertyDescriptor isGradientEnabled_descriptor = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "is_gradient_enabled", "Enable Gradient"); + + public static WidgetPropertyDescriptor isHighlightingOfInactiveRegionsEnabled_descriptor = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "is_highlighting_of_active_regions_enabled", "Highlight Active Region"); + + public static WidgetPropertyDescriptor needleWidth_descriptor = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "needle_width", "Needle Width"); + + private WidgetProperty foreground; + private WidgetProperty background; + private WidgetProperty font; + private WidgetProperty format; + private WidgetProperty show_units; + private WidgetProperty show_limits; + private WidgetProperty needle_color; + private WidgetProperty scale_visible; + private WidgetProperty knob_color; + private WidgetProperty knobSize; + private WidgetProperty limits_from_pv; + private WidgetProperty minimum; + private WidgetProperty maximum; + private WidgetProperty level_high; + private WidgetProperty level_hihi; + private WidgetProperty level_lolo; + private WidgetProperty level_low; + private WidgetProperty displayHorizontal; + + private StructuredWidgetProperty colorsStructuredWidget; + private WidgetProperty isGradientEnabled; + private WidgetProperty isHighlightingOfInactiveRegionsEnabled; + private WidgetProperty needleWidth; + private WidgetProperty normalStatusColor; + private WidgetProperty minorWarningColor; + private WidgetProperty majorWarningColor; + + @Override + public Version getVersion() + { + return METER_VERSION; + } + + @Override + public WidgetConfigurator getConfigurator(Version persistedVersion) throws Exception + { + return new LinearMeterConfigurator(persistedVersion); + } + + @Override + protected void defineProperties(List> properties) + { + super.defineProperties(properties); + + properties.add(font = propFont.createProperty(this, WidgetFontService.get(NamedWidgetFonts.DEFAULT))); + properties.add(format = propFormat.createProperty(this, FormatOption.DEFAULT)); + properties.add(show_units = propShowUnits.createProperty(this, true)); + properties.add(scale_visible = propScaleVisible.createProperty(this, true)); + properties.add(show_limits = propShowLimits.createProperty(this, true)); + properties.add(limits_from_pv = propLimitsFromPV.createProperty(this, true)); + properties.add(minimum = propMinimum.createProperty(this, 0.0)); + properties.add(maximum = propMaximum.createProperty(this, 100.0)); + properties.add(displayHorizontal = propDisplayHorizontal.createProperty(this, true)); + properties.add(knobSize = knobSize_descriptor.createProperty(this, 8)); + foreground = propForegroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.TEXT)); + background = propBackgroundColor.createProperty(this, new WidgetColor(0, 0, 0, 0)); + needle_color = propNeedleColor.createProperty(this, new WidgetColor(0, 0, 0, 255)); + knob_color = knobColor_descriptor.createProperty(this, new WidgetColor(0, 0, 0, 255)); + normalStatusColor = propNormalStatusColor_descriptor.createProperty(this, new WidgetColor(194,198,195)); + minorWarningColor = minorWarningColor_descriptor.createProperty(this, new WidgetColor(242, 148, 141)); + majorWarningColor = majorWarningColor_descriptor.createProperty(this, new WidgetColor(240, 60, 46)); + isGradientEnabled = isGradientEnabled_descriptor.createProperty(this, false); + isHighlightingOfInactiveRegionsEnabled = isHighlightingOfInactiveRegionsEnabled_descriptor.createProperty(this, true); + properties.add(needleWidth = needleWidth_descriptor.createProperty(this, 1)); + List> colorSelectionWidgets = Arrays.asList(foreground, + background, + needle_color, + knob_color, + normalStatusColor, + minorWarningColor, + majorWarningColor, + isGradientEnabled, + isHighlightingOfInactiveRegionsEnabled); + properties.add(colorsStructuredWidget = colorsStructuredWidget_descriptor.createProperty(this, + colorSelectionWidgets)); + properties.add(level_lolo = propLevelLoLo.createProperty(this, 10.0)); + properties.add(level_low = propLevelLow.createProperty(this, 20.0)); + properties.add(level_high = propLevelHigh.createProperty(this, 80.0)); + properties.add(level_hihi = propLevelHiHi.createProperty(this, 90.0)); + } + + /** @return 'foreground_color' property */ + public WidgetProperty propForeground() + { + return foreground; + } + + /** @return 'background_color' property */ + public WidgetProperty propBackground() + { + return background; + } + + /** @return 'font' property */ + public WidgetProperty propFont() + { + return font; + } + + /** @return 'format' property */ + public WidgetProperty propFormat() + { + return format; + } + + /** @return 'show_units' property */ + public WidgetProperty propShowUnits() + { + return show_units; + } + + /** @return 'scale_visible' property */ + public WidgetProperty propScaleVisible() + { + return scale_visible; + } + + /** @return 'show_limits' property */ + public WidgetProperty propShowLimits() + { + return show_limits; + } + + /** @return 'needle_color' property */ + public WidgetProperty propNeedleColor() + { + return needle_color; + } + + /** @return 'knob_color' property */ + public WidgetProperty propKnobColor() + { + return knob_color; + } + + public WidgetProperty propKnobSize() { return knobSize; } + + /** @return 'limits_from_pv' property */ + public WidgetProperty propLimitsFromPV() + { + return limits_from_pv; + } + + /** @return 'minimum' property */ + public WidgetProperty propMinimum() + { + return minimum; + } + + /** @return 'maximum' property */ + public WidgetProperty propMaximum() + { + return maximum; + } + + public WidgetProperty propDisplayHorizontal() { return displayHorizontal; } + + public WidgetProperty propLevelHiHi ( ) { + return level_hihi; + } + + public WidgetProperty propLevelHigh ( ) { + return level_high; + } + + public WidgetProperty propLevelLoLo ( ) { + return level_lolo; + } + + public WidgetProperty propLevelLow ( ) { + return level_low; + } + + public WidgetProperty propIsGradientEnabled () { + return isGradientEnabled; + } + + public WidgetProperty propIsHighlightActiveRegionEnabled () { + return isHighlightingOfInactiveRegionsEnabled; + } + + public WidgetProperty propNormalStatusColor() { + return normalStatusColor; + } + + public WidgetProperty propMinorWarningColor() { + return minorWarningColor; + } + + public WidgetProperty propMajorWarningColor() { + return majorWarningColor; + } + + public WidgetProperty propNeedleWidth() { return needleWidth; } + +} diff --git a/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/RTLinearMeter.java b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/RTLinearMeter.java new file mode 100644 index 0000000000..8e6a35f1a6 --- /dev/null +++ b/app/display/linearmeter/src/main/java/org/csstudio/display/extra/widgets/linearmeter/RTLinearMeter.java @@ -0,0 +1,1100 @@ + +package org.csstudio.display.extra.widgets.linearmeter; + +import static org.csstudio.javafx.rtplot.Activator.logger; + +import java.awt.BasicStroke; +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.IllegalPathStateException; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.IOException; +import java.util.Arrays; +import java.util.logging.Level; + +import javafx.application.Platform; +import org.csstudio.javafx.rtplot.internal.AxisPart; +import org.csstudio.javafx.rtplot.internal.PlotPart; +import org.csstudio.javafx.rtplot.internal.PlotPartListener; +import org.csstudio.javafx.rtplot.internal.util.GraphicsUtils; + +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.WritableImage; + +import javax.imageio.ImageIO; + +/** + * @author European Spallation Source ERIC + * @version 1.1 + * + * Version 1.0 implemented by Fredrik Söderberg. + * Version 1.1 (some fixes and improvements) by Abraham Wolk. + * + * The Linear Meter graphics design is by Dirk Nordt and Fredrik Söderberg. + * + */ + +@SuppressWarnings("nls") +public class RTLinearMeter extends ImageView +{ + + // Note: To a first approximation, all methods that are called from different threads must be "synchronized". + + public RTLinearMeter(double initialValue, + int width, + int height, + double min, + double max, + double loLo, + double low, + double high, + double hiHi, + boolean showUnits, + boolean showLimits, + boolean isHorizontal, + boolean isGradientEnabled, + boolean isHighlightActiveRegionEnabled, + Color normalStatusColor, + Color minorAlarmColor, + Color majorAlarmColor, + int needleWidth, + Color needleColor, + int knobSize, + Color knobColor) + { + if (warningTriangle == null) { + try { + warningTriangle = ImageIO.read(getClass().getResource("/graphics/Warning_Triangle_Red.png")); + } catch (IOException ioException) { + logger.log(Level.WARNING, "Unable to load warning triangle icon!"); + } + } + + if (Double.isFinite(min) && Double.isFinite(max)) { + validRange = true; + } + else { + validRange = false; + min = 0.0; + max = 100.0; + } + + linearMeterScale = new LinearMeterScale(plot_part_listener, + width, + height, + isHorizontal, + min, + max); + + this.loLo = loLo; + this.low = low; + this.high = high; + this.hiHi = hiHi; + this.showUnits = showUnits; + this.showLimits = showLimits; + + layout(); + + this.currentValue = initialValue; + this.isGradientEnabled = isGradientEnabled; + this.isHighlightActiveRegionEnabled = isHighlightActiveRegionEnabled; + + this.needleWidth = needleWidth; + this.needleColor = needleColor; + this.knobSize = knobSize; + this.knobColor = knobColor; + + setNormalStatusColor(normalStatusColor); + setMinorAlarmColor(minorAlarmColor); + setMajorAlarmColor(majorAlarmColor); + + requestLayout(); + } + + private boolean showUnits; + private String units = ""; + private boolean showLimits; + + private double loLo; + private double low; + private double high; + private double hiHi; + + private static Image warningTriangle = null; + + public void redrawLinearMeterScale() { + boolean isHorizontal = linearMeterScale.isHorizontal(); + linearMeterScale = new LinearMeterScale(plot_part_listener, + linearMeterScale.getBounds().width, + linearMeterScale.getBounds().height, + linearMeterScale.isHorizontal(), + linearMeterScale.getValueRange().getLow(), + linearMeterScale.getValueRange().getHigh()); + linearMeterScale.setHorizontal(isHorizontal); + if (font != null) { + linearMeterScale.setScaleFont(GraphicsUtils.convert(font)); + } + } + + private enum WARNING { + NONE, + VALUE_LESS_THAN_MIN, + VALUE_GREATER_THAN_MAX, + MIN_AND_MAX_NOT_DEFINED, + LAG, + NO_UNIT + } + + private WARNING currentWarning = WARNING.NONE; + + /** Colors */ + private Color foreground = Color.BLACK; + private Color background = Color.WHITE; + + /** Fonts */ + private Font font; + + /** Empty listener to LinearMeterScale */ + protected PlotPartListener plot_part_listener = new PlotPartListener() + { + @Override + public void layoutPlotPart(PlotPart plotPart) { } + + @Override + public void refreshPlotPart(PlotPart plotPart) { } + }; + + private boolean validRange; + + public boolean getValidRange() { + return validRange; + } + + /** Optional scale of this linear meter */ + public LinearMeterScale linearMeterScale; + + /** Value to display */ + private double currentValue = Double.NaN; + + private Color normalStatusColor_lowlighted = Color.LIGHT_GRAY; + private Color normalStatusColorGradientStartPoint_lowlighted = Color.LIGHT_GRAY; + private Color normalStatusColorGradientEndPoint_lowlighted = Color.LIGHT_GRAY; + private Color normalStatusColor_highlighted = Color.LIGHT_GRAY; + private Color normalStatusColorGradientStartPoint_highlighted = Color.LIGHT_GRAY; + private Color normalStatusColorGradientEndPoint_highlighted = Color.LIGHT_GRAY; + + private Color minorAlarmColor_lowlighted = Color.ORANGE; + private Color minorAlarmColor_highlighted = Color.ORANGE; + private Color minorAlarmColorGradientStartPoint_lowlighted = Color.ORANGE; + private Color minorAlarmColorGradientEndPoint_lowlighted = Color.ORANGE; + private Color minorAlarmColorGradientStartPoint_highlighted = Color.ORANGE; + private Color minorAlarmColorGradientEndPoint_highlighted = Color.ORANGE; + + private Color majorAlarmColor_lowlighted = Color.RED; + private Color majorAlarmColor_highlighted = Color.RED; + private Color majorAlarmColorGradientStartPoint_lowlighted = Color.RED; + private Color majorAlarmColorGradientEndPoint_lowlighted = Color.RED; + private Color majorAlarmColorGradientStartPoint_highlighted = Color.RED; + private Color majorAlarmColorGradientEndPoint_highlighted = Color.RED; + + Paint normalStatusActiveColor_lowlighted, minorAlarmActiveColor_lowlighted, majorAlarmActiveColor_lowlighted; + + Paint normalStatusActiveColor_highlighted, majorAlarmActiveColor_highlighted, minorAlarmActiveColor_highlighted; + + private int needleWidth; + + private Color needleColor; + + private Boolean isGradientEnabled; + + private Boolean isHighlightActiveRegionEnabled; + + public synchronized void setIsGradientEnabled(boolean isGradientEnabled) { + this.isGradientEnabled = isGradientEnabled; + updateActiveColors(); + } + + public synchronized void setIsHighlightActiveRegionEnabled(boolean isHighlightActiveRegionEnabled) { + this.isHighlightActiveRegionEnabled = isHighlightActiveRegionEnabled; + } + + private void updateActiveColors() { + if (isGradientEnabled) { + if (linearMeterScale.isHorizontal()) { + majorAlarmActiveColor_lowlighted = createVerticalGradientPaint(loLoRectangle, majorAlarmColorGradientStartPoint_lowlighted, majorAlarmColorGradientEndPoint_lowlighted); + minorAlarmActiveColor_lowlighted = createVerticalGradientPaint(lowRectangle, minorAlarmColorGradientStartPoint_lowlighted, minorAlarmColorGradientEndPoint_lowlighted); + normalStatusActiveColor_lowlighted = createVerticalGradientPaint(normalRectangle, normalStatusColorGradientStartPoint_highlighted, normalStatusColorGradientEndPoint_highlighted); // The normal status region is never lowlighted. + + minorAlarmActiveColor_highlighted = createVerticalGradientPaint(lowRectangle, minorAlarmColorGradientStartPoint_highlighted, minorAlarmColorGradientEndPoint_highlighted); + majorAlarmActiveColor_highlighted = createVerticalGradientPaint(loLoRectangle, majorAlarmColorGradientStartPoint_highlighted, majorAlarmColorGradientEndPoint_highlighted); + normalStatusActiveColor_highlighted = createVerticalGradientPaint(normalRectangle, normalStatusColorGradientStartPoint_highlighted, normalStatusColorGradientEndPoint_highlighted); + } else { + majorAlarmActiveColor_lowlighted = createHorizontalGradientPaint(loLoRectangle, majorAlarmColorGradientStartPoint_lowlighted, majorAlarmColorGradientEndPoint_lowlighted); + minorAlarmActiveColor_lowlighted = createHorizontalGradientPaint(lowRectangle, minorAlarmColorGradientStartPoint_lowlighted, minorAlarmColorGradientEndPoint_lowlighted); + normalStatusActiveColor_lowlighted = createHorizontalGradientPaint(normalRectangle, normalStatusColorGradientStartPoint_highlighted, normalStatusColorGradientEndPoint_highlighted); // The normal status region is never lowlighted. + + minorAlarmActiveColor_highlighted = createHorizontalGradientPaint(lowRectangle, minorAlarmColorGradientStartPoint_highlighted, minorAlarmColorGradientEndPoint_highlighted); + majorAlarmActiveColor_highlighted = createHorizontalGradientPaint(loLoRectangle, majorAlarmColorGradientStartPoint_highlighted, majorAlarmColorGradientEndPoint_highlighted); + normalStatusActiveColor_highlighted = createHorizontalGradientPaint(normalRectangle, normalStatusColorGradientStartPoint_highlighted, normalStatusColorGradientEndPoint_highlighted); + } + } + else { + normalStatusActiveColor_lowlighted = normalStatusColor_highlighted; // The normal status region is never lowlighted. + majorAlarmActiveColor_lowlighted = majorAlarmColor_lowlighted; + minorAlarmActiveColor_lowlighted = minorAlarmColor_lowlighted; + + normalStatusActiveColor_highlighted = normalStatusColor_highlighted; + minorAlarmActiveColor_highlighted = minorAlarmColor_highlighted; + majorAlarmActiveColor_highlighted = majorAlarmColor_highlighted; + } + } + + private Color computeGradientStartPoint(Color color) { + float[] hsbValues = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + float newSaturationValue; + if (hsbValues[1] < 0.01) { + newSaturationValue = hsbValues[1]; // To prevent rounding errors leading to the color changing. + } + else { + newSaturationValue = (float) 1.0 - ((float) 1.0 - hsbValues[1]) * (float) 0.8; + } + float newBrightnessValue = hsbValues[2] * (float) 0.8; + Color gradientEndPoint_withoutAlpha = Color.getHSBColor(hsbValues[0], + newSaturationValue, + newBrightnessValue); + Color gradientEndPoint = new Color(gradientEndPoint_withoutAlpha.getRed(), + gradientEndPoint_withoutAlpha.getGreen(), + gradientEndPoint_withoutAlpha.getBlue(), + color.getAlpha()); + return gradientEndPoint; + } + + private Color computeGradientEndPoint(Color color) { + float[] hsbValues = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + float newSaturationValue; + if (hsbValues[1] < 0.01) { + newSaturationValue = hsbValues[1]; // To prevent rounding errors leading to the color changing. + } + else { + newSaturationValue = hsbValues[1] * (float) 0.85; + } + float newBrightnessValue = 1 - (1 - hsbValues[2]) * (float) 0.5; + Color gradientEndPoint_withoutAlpha = Color.getHSBColor(hsbValues[0], + newSaturationValue, + newBrightnessValue); + Color gradientEndPoint = new Color(gradientEndPoint_withoutAlpha.getRed(), + gradientEndPoint_withoutAlpha.getGreen(), + gradientEndPoint_withoutAlpha.getBlue(), + color.getAlpha()); + return gradientEndPoint; + } + + private Color computeLowlightedColor(Color color) { + float[] hsbValues = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + float newSaturationValue = hsbValues[1] * 0.1f; + Color lowlightedColor_withoutAlpha = Color.getHSBColor(hsbValues[0], + newSaturationValue, + hsbValues[2]); + Color lowlightedColor = new Color(lowlightedColor_withoutAlpha.getRed(), + lowlightedColor_withoutAlpha.getGreen(), + lowlightedColor_withoutAlpha.getBlue(), + color.getAlpha()); + return lowlightedColor; + } + + public synchronized void setNormalStatusColor(Color normalStatusColor) { + this.normalStatusColor_lowlighted = computeLowlightedColor(normalStatusColor); + this.normalStatusColor_highlighted = normalStatusColor; + + this.normalStatusColorGradientStartPoint_lowlighted = computeGradientStartPoint(normalStatusColor_lowlighted); + this.normalStatusColorGradientEndPoint_lowlighted = computeGradientEndPoint(normalStatusColor_lowlighted); + + this.normalStatusColorGradientStartPoint_highlighted = computeGradientStartPoint(normalStatusColor_highlighted); + this.normalStatusColorGradientEndPoint_highlighted = computeGradientEndPoint(normalStatusColor_highlighted); + + updateActiveColors(); + } + + public synchronized void setMinorAlarmColor(Color minorAlarmColor) { + this.minorAlarmColor_lowlighted = computeLowlightedColor(minorAlarmColor); + this.minorAlarmColor_highlighted = minorAlarmColor; + + this.minorAlarmColorGradientStartPoint_lowlighted = computeGradientStartPoint(minorAlarmColor_lowlighted); + this.minorAlarmColorGradientEndPoint_lowlighted = computeGradientEndPoint(minorAlarmColor_lowlighted); + + this.minorAlarmColorGradientStartPoint_highlighted = computeGradientStartPoint(minorAlarmColor_highlighted); + this.minorAlarmColorGradientEndPoint_highlighted = computeGradientEndPoint(minorAlarmColor_highlighted); + + updateActiveColors(); + } + + public synchronized void setMajorAlarmColor(Color majorAlarmColor) { + this.majorAlarmColor_lowlighted = computeLowlightedColor(majorAlarmColor); + this.majorAlarmColor_highlighted = majorAlarmColor; + + this.majorAlarmColorGradientStartPoint_lowlighted = computeGradientStartPoint(majorAlarmColor_lowlighted); + this.majorAlarmColorGradientEndPoint_lowlighted = computeGradientEndPoint(majorAlarmColor_lowlighted); + + this.majorAlarmColorGradientStartPoint_highlighted = computeGradientStartPoint(majorAlarmColor_highlighted); + this.majorAlarmColorGradientEndPoint_highlighted = computeGradientEndPoint(majorAlarmColor_highlighted); + + updateActiveColors(); + } + + public synchronized void setNeedleWidth(int needleWidth) { + this.needleWidth = needleWidth; + } + + public synchronized void setNeedleColor(Color needleColor) { + this.needleColor = needleColor; + } + + public synchronized void setShowUnits(boolean newValue) { + showUnits = newValue; + updateMeterBackground(); + redrawIndicator(currentValue, currentWarning); + } + + public synchronized void setUnits(String newValue) { + if (!units.equals(newValue)) { + units = newValue; + updateMeterBackground(); + redrawIndicator(currentValue, currentWarning); + } + } + + public synchronized void setShowLimits(boolean newValue) { + showLimits = newValue; + updateMeterBackground(); + determineWarning(); + redrawIndicator(currentValue, currentWarning); + } + + public synchronized void setRange(double minimum, double maximum, boolean validRange) { + + this.validRange = validRange; + linearMeterScale.setValueRange(minimum, maximum); + + updateMeterBackground(); + redrawIndicator(currentValue, currentWarning); + } + + public double getLoLo() { + return loLo; + } + + public synchronized void setLoLo(double loLo) { + this.loLo = loLo; + layout(); + updateMeterBackground(); + } + + public double getLow() { + return low; + } + + public synchronized void setLow(double low) { + this.low = low; + layout(); + updateMeterBackground(); + } + + public double getHigh() { + return high; + } + + public synchronized void setHigh(double high) { + this.high = high; + layout(); + updateMeterBackground(); + } + + public double getHiHi() { + return hiHi; + } + + public synchronized void setHiHi(double hiHi) { + this.hiHi = hiHi; + layout(); + updateMeterBackground(); + } + + private Color knobColor = new Color(0, 0, 0, 255); + + public synchronized void setKnobColor(Color knobColor) { + this.knobColor = knobColor; + requestLayout(); + } + + private int knobSize; + + public synchronized void setKnobSize(int knobSize) { + this.knobSize = knobSize; + requestLayout(); + } + + private BufferedImage meter_background = null; + + private WritableImage awt_jfx_convert_buffer = null; + + /** Redraw on UI thread by adding needle to 'meter_background' */ + private synchronized void redrawIndicator (double value, WARNING warning) + { + if (meter_background != null) + { + if (meter_background.getType() != BufferedImage.TYPE_INT_ARGB){ + throw new IllegalPathStateException("Need TYPE_INT_ARGB for direct buffer access, not " + meter_background.getType()); + } + + BufferedImage combined = new BufferedImage(linearMeterScale.getBounds().width, linearMeterScale.getBounds().height, BufferedImage.TYPE_INT_ARGB); + int[] src = ((DataBufferInt) meter_background.getRaster().getDataBuffer()).getData(); + int[] dest = ((DataBufferInt) combined.getRaster().getDataBuffer()).getData(); + System.arraycopy(src, 0, dest, 0, linearMeterScale.getBounds().width * linearMeterScale.getBounds().height); + + // Add needle & label + Graphics2D gc = combined.createGraphics(); + + drawValue(gc, value); + drawWarning(gc, warning); + if (showUnits) { + drawUnit(gc); + } + + // Convert to JFX image and show + if (awt_jfx_convert_buffer == null || + awt_jfx_convert_buffer.getWidth() != linearMeterScale.getBounds().width || + awt_jfx_convert_buffer.getHeight() != linearMeterScale.getBounds().height) + awt_jfx_convert_buffer = new WritableImage(linearMeterScale.getBounds().width, linearMeterScale.getBounds().height); + + awt_jfx_convert_buffer.getPixelWriter().setPixels(0, 0, linearMeterScale.getBounds().width, linearMeterScale.getBounds().height, PixelFormat.getIntArgbInstance(), dest, 0, linearMeterScale.getBounds().width); + + setImage(awt_jfx_convert_buffer); + logger.log(Level.FINE, "Redraw meter"); + } + }; + + /** Call to update size of meter + * + * @param width + * @param height + */ + public synchronized void setSize(int width, int height) + { + linearMeterScale.setBounds(0, 0, width, height); + layout(); + updateActiveColors(); + requestLayout(); + } + + /** @param color Foreground (labels, tick marks) color */ + public synchronized void setForeground(javafx.scene.paint.Color color) + { + foreground = GraphicsUtils.convert(color); + linearMeterScale.setColor(color); + } + + /** @param color Background color */ + public synchronized void setBackground(javafx.scene.paint.Color color) + { + background = GraphicsUtils.convert(color); + } + + /** @param font Label font */ + public synchronized void setFont(javafx.scene.text.Font font) + { + linearMeterScale.setScaleFont(font); + this.font = GraphicsUtils.convert(font); + } + + + private boolean lag = false; + private Boolean isValueWaitingToBeDrawn = false; + private double valueWaitingToBeDrawn; + /** @param newValue Current value */ + public synchronized void setCurrentValue(double newValue) + { + valueWaitingToBeDrawn = newValue; + + if (isValueWaitingToBeDrawn) { + lag = true; + } + else { + isValueWaitingToBeDrawn = true; + + Platform.runLater(() -> { + synchronized (this) { + drawNewValue(valueWaitingToBeDrawn); + isValueWaitingToBeDrawn = false; + lag = false; + } + }); + } + } + + private void drawNewValue(double newValue) { + double oldValue = currentValue; + currentValue = newValue; + + if (oldValue != newValue) { + if (!Double.isNaN(newValue)){ + int newIndicatorPosition; + if (linearMeterScale.isHorizontal()) { + newIndicatorPosition = (int) (marginLeft + pixelsPerScaleUnit * (newValue - linearMeterScale.getValueRange().getLow())); + } + else { + newIndicatorPosition = (int) (linearMeterScale.getBounds().height - marginBelow - pixelsPerScaleUnit * (newValue - linearMeterScale.getValueRange().getLow())); + } + WARNING newWarning = determineWarning(); + if (currentIndicatorPosition == null || currentIndicatorPosition != newIndicatorPosition || currentWarning != newWarning) { + redrawIndicator(newValue, newWarning); + } + } + else if (!Double.isNaN(oldValue)) { + redrawIndicator(newValue, determineWarning()); + } + } + } + + private WARNING determineWarning() { + if (lag) { + return WARNING.LAG; + } + else if (showUnits && units == "") { + return WARNING.NO_UNIT; + } + else if (!validRange) { + return WARNING.MIN_AND_MAX_NOT_DEFINED; + } + else if (currentValue < linearMeterScale.getValueRange().getLow()) { + return WARNING.VALUE_LESS_THAN_MIN; + } + else if (currentValue > linearMeterScale.getValueRange().getHigh()) { + return WARNING.VALUE_GREATER_THAN_MAX; + } + else { + return WARNING.NONE; + } + } + + private void drawWarning(Graphics2D gc, WARNING warning) { + if (warning != WARNING.NONE) { + String warningText = ""; + if (warning == WARNING.VALUE_LESS_THAN_MIN) { + warningText = "VALUE < MIN"; + + } + else if (warning == WARNING.VALUE_GREATER_THAN_MAX) { + warningText = "VALUE > MAX"; + + } + else if (warning == WARNING.NO_UNIT) { + warningText = "NO UNIT DEFINED"; + } + else if (warning == WARNING.MIN_AND_MAX_NOT_DEFINED) { + warningText = "MIN AND MAX ARE NOT SET"; + + } + else if (warning == WARNING.LAG) { + warningText = "LAG"; + } + + drawWarningText(gc, warningText); + if (currentWarning != warning) { + logger.log(Level.WARNING, warningText + " on Linear Meter!"); + } + } + currentWarning = warning; + } + + /** @param visible Whether the scale must be displayed or not. */ + public synchronized void setScaleVisible (boolean visible) + { + linearMeterScale.setVisible(visible); + updateMeterBackground(); + } + + /** Request a complete redraw with new layout */ + private void requestLayout() + { + updateMeterBackground(); + redrawIndicator(currentValue, currentWarning); + } + + private void computeLayout() + { + logger.log(Level.FINE, "computeLayout"); + layout(); + + if (linearMeterScale.isHorizontal()) { + linearMeterScale.configure( + loLoRectangle.x, + lowRectangle.y + loLoRectangle.height, + pixelsPerScaleUnit); + } else { + linearMeterScale.configure( + linearMeterScale.getBounds().width - marginRight, + linearMeterScale.getBounds().height - marginBelow, + pixelsPerScaleUnit); + } + } + + /** Draw meter background (scale) into image buffer + * @return Latest image, must be of type BufferedImage.TYPE_INT_ARGB + */ + private void updateMeterBackground() + { + int width = linearMeterScale.getBounds().width; + int height = linearMeterScale.getBounds().height; + + if (width <= 0 || height <= 0){ + return; + } + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D gc = image.createGraphics(); + + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gc.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + gc.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + gc.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + linearMeterScale.computeTicks(gc); + computeLayout(); + + gc.setBackground(background); + gc.clearRect(0, 0, width, height); + + linearMeterScale.paint(gc, new Rectangle(0,0,0,0)); + paintMeter(gc); + + meter_background = image; + } + + private void paintMeter(Graphics2D graphics) { + Color color = graphics.getColor(); + if (showLimits) { + if (isHighlightActiveRegionEnabled) { + paintRectangle(graphics, normalRectangle, normalStatusActiveColor_lowlighted); + paintRectangle(graphics, lowRectangle, minorAlarmActiveColor_lowlighted); + paintRectangle(graphics, highRectangle, minorAlarmActiveColor_lowlighted); + paintRectangle(graphics, loLoRectangle, majorAlarmActiveColor_lowlighted); + paintRectangle(graphics, hiHiRectangle, majorAlarmActiveColor_lowlighted); + } + else { + paintRectangle(graphics, normalRectangle, normalStatusActiveColor_highlighted); + paintRectangle(graphics, lowRectangle, minorAlarmActiveColor_highlighted); + paintRectangle(graphics, highRectangle, minorAlarmActiveColor_highlighted); + paintRectangle(graphics, loLoRectangle, majorAlarmActiveColor_highlighted); + paintRectangle(graphics, hiHiRectangle, majorAlarmActiveColor_highlighted); + } + } + else { + paintRectangle(graphics, + new Rectangle(marginLeft, + marginAbove, + linearMeterScale.getBounds().width - marginLeft - marginRight, + linearMeterScale.getBounds().height - marginAbove - marginBelow), + normalStatusActiveColor_lowlighted); + } + graphics.setColor(color); + } + + private GradientPaint createHorizontalGradientPaint(Rectangle rectangle, + Color colorGradientStartPoint, + Color colorGradientEndPoint) { + GradientPaint gradientPaint = new GradientPaint(rectangle.x, rectangle.y, colorGradientStartPoint, + rectangle.x + rectangle.width / 2, rectangle.y, colorGradientEndPoint, + true); + return gradientPaint; + } + + private GradientPaint createVerticalGradientPaint(Rectangle rectangle, + Color colorGradientStartPoint, + Color colorGradientEndPoint) { + GradientPaint gradientPaint = new GradientPaint(rectangle.x, rectangle.y, colorGradientStartPoint, + rectangle.x, rectangle.y + rectangle.height / 2, colorGradientEndPoint, + true); + return gradientPaint; + } + + Integer currentIndicatorPosition; + /** Draw needle and label for current value */ + private void drawValue(Graphics2D gc, double value) { + + if (Double.isNaN(value)) { + currentIndicatorPosition = null; + } + else { + Stroke oldStroke = gc.getStroke(); + Paint oldPaint = gc.getPaint(); + RenderingHints oldrenderingHints = gc.getRenderingHints(); + + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gc.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + gc.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + gc.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + if (showLimits) { + if (isHighlightActiveRegionEnabled) { + if (value <= loLo) { + paintRectangle(gc, loLoRectangle, majorAlarmColor_highlighted); + } + else if (value >= hiHi) { + paintRectangle(gc, hiHiRectangle, majorAlarmColor_highlighted); + } + else if (value <= low && value > loLo) { + paintRectangle(gc, lowRectangle, minorAlarmActiveColor_highlighted); + } + else if (value >= high && value < hiHi) { + paintRectangle(gc, highRectangle, minorAlarmActiveColor_highlighted); + } + else { + paintRectangle(gc, normalRectangle, normalStatusActiveColor_highlighted); + } + } + } + + if (linearMeterScale.isHorizontal()) { + if (value >= linearMeterScale.getValueRange().getLow() && value <= linearMeterScale.getValueRange().getHigh()) { + + currentIndicatorPosition = (int) (marginLeft + pixelsPerScaleUnit * (value - linearMeterScale.getValueRange().getLow())); + + if (knobSize > 0) { + int[] XVal = { currentIndicatorPosition - (int) Math.round((1.0 * knobSize) / 4.0), + currentIndicatorPosition + (int) Math.round((1.0 * knobSize) / 4.0), + currentIndicatorPosition }; + + int[] YVal = { 0, 0, marginAbove - 2 }; + + gc.setStroke(AxisPart.TICK_STROKE); + gc.setColor(knobColor); + gc.fillPolygon(XVal, YVal, 3); + gc.setColor(knobColor); + gc.drawPolygon(XVal, YVal, 3); + } + + if (needleWidth > 0) { + gc.setStroke(new BasicStroke((float) needleWidth)); + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + gc.setPaint(needleColor); + + int y1 = marginAbove + needleWidth / 2 + 1; + int y2 = linearMeterScale.getBounds().height - marginBelow - (needleWidth - 1) / 2 - 1; + + gc.drawLine(currentIndicatorPosition, y1, currentIndicatorPosition, y2); + } + } + } else { + if (value >= linearMeterScale.getValueRange().getLow() && value <= linearMeterScale.getValueRange().getHigh()) { + + currentIndicatorPosition = (int) (linearMeterScale.getBounds().height - marginBelow - pixelsPerScaleUnit * (value - linearMeterScale.getValueRange().getLow())); + + if (knobSize > 0) { + int[] YVal = { currentIndicatorPosition + (int) Math.round((1.0 * knobSize / 4.0)), + currentIndicatorPosition - (int) Math.round((1.0 * knobSize / 4.0)), + currentIndicatorPosition }; + + int[] XVal = { 0, 0, marginLeft - 2 }; + + gc.setStroke(AxisPart.TICK_STROKE); + gc.setColor(knobColor); + gc.fillPolygon(XVal, YVal, 3); + gc.setColor(knobColor); + gc.drawPolygon(XVal, YVal, 3); + } + + if (needleWidth > 0) { + gc.setStroke(new BasicStroke((float) needleWidth)); + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + gc.setPaint(needleColor); + + int x1 = marginLeft + (needleWidth)/2 + 1; + int x2 = linearMeterScale.getBounds().width - marginRight - (needleWidth+1)/2; + + gc.drawLine(x1, currentIndicatorPosition, x2, currentIndicatorPosition); + } + } + } + gc.setRenderingHints(oldrenderingHints); + gc.setStroke(oldStroke); + gc.setPaint(oldPaint); + } + } + + /** Should be invoked when meter no longer used to release resources */ + public void dispose() + { + // Release memory ASAP + meter_background = null; + } + + public synchronized void setHorizontal(boolean horizontal) { + linearMeterScale.setHorizontal(horizontal); + redrawLinearMeterScale(); + updateMeterBackground(); + redrawIndicator(currentValue, currentWarning); + } + + private void drawUnit(Graphics2D gc) { + int center_x = marginLeft + (linearMeterScale.getBounds().width - marginLeft - marginRight) / 2; + int center_y = linearMeterScale.getBounds().height; + RenderingHints renderingHints = gc.getRenderingHints(); + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gc.setFont(font); + gc.setColor(Color.BLACK); + FontMetrics fontMetrics = gc.getFontMetrics(gc.getFont()); + String stringToPrint = "[" + units + "]"; + int delta_x = fontMetrics.stringWidth(stringToPrint) / 2; + int delta_y = fontMetrics.getMaxDescent(); + + gc.drawString(stringToPrint, + center_x - delta_x, + center_y - delta_y); + gc.setRenderingHints(renderingHints); + } + + private void drawWarning_horizontal(Graphics2D gc, String warningText) { + int center_x = marginLeft + (linearMeterScale.getBounds().width - marginLeft - marginRight) / 2; + int center_y = marginAbove + (linearMeterScale.getBounds().height - marginAbove - marginBelow) / 2; + gc.setFont(font); + gc.setColor(Color.BLACK); + FontMetrics fontMetrics = gc.getFontMetrics(gc.getFont()); + int delta_x = (warningTriangle.getWidth(null) + fontMetrics.stringWidth(warningText)) / 2; + int delta_y = fontMetrics.getAscent() / 2; + + gc.drawImage(warningTriangle, + center_x - delta_x, + center_y + delta_y - warningTriangle.getHeight(null) / 2 - 3 * fontMetrics.getAscent() / 8, + null); + gc.drawString(warningText, + center_x - delta_x + warningTriangle.getWidth(null) + 2, + center_y + delta_y); + } + + private void drawWarningText(Graphics2D gc, String warningText) { + RenderingHints oldRenderingHints = gc.getRenderingHints(); + gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (linearMeterScale.isHorizontal()) { + drawWarning_horizontal(gc, warningText); + } + else { + drawWarning_vertical(gc, warningText); + } + gc.setRenderingHints(oldRenderingHints); + } + + private void drawWarning_vertical(Graphics2D gc, String warningText) { + + gc.setFont(font); + gc.setColor(Color.BLACK); + FontMetrics fontMetrics = gc.getFontMetrics(gc.getFont()); + String[] warningText_split; + if (fontMetrics.stringWidth(warningText) <= meterBreadth) { + warningText_split = new String[] { warningText }; + } else { + String[] warningText_splitByWhitespace = warningText.split("\\s+"); + if (Arrays.stream(warningText_splitByWhitespace).allMatch(subString -> fontMetrics.stringWidth(subString) <= meterBreadth)) { + warningText_split = warningText_splitByWhitespace; + } + else { + warningText_split = warningText.split(""); // Split on every character + } + } + + int center_x = marginLeft + (linearMeterScale.getBounds().width - marginLeft - marginRight) / 2; + int center_y = marginAbove + (linearMeterScale.getBounds().height - marginAbove - marginBelow) / 2; + int warningTriangleHeight = warningTriangle.getHeight(null); + int fontSizeInPixels = fontMetrics.getHeight(); + int delta_y = (warningTriangleHeight + warningText_split.length * fontSizeInPixels) / 2; + + gc.drawImage(warningTriangle, + center_x - warningTriangle.getWidth(null) / 2, + center_y - delta_y, + null); + + for (int i = 0; i < warningText_split.length; i++) { + gc.drawString(warningText_split[i], + center_x - fontMetrics.stringWidth(warningText_split[i]) / 2, + center_y - delta_y + warningTriangleHeight + (fontSizeInPixels + 4) * (i + 1)); + } + } + + private void paintRectangle(Graphics2D gc, Rectangle rectangle, Paint paint){ + + //Store old values of clip and color + Shape oldClip = gc.getClip(); + Color oldColor = gc.getColor(); + + //Paint rectangle with specified gradient + gc.setClip(rectangle); + gc.setPaint(paint); + gc.fill(rectangle); + gc.setClip(oldClip); + + //Draw border of rectangle. + //TODO: can this be included in the paint? + gc.setColor(foreground); + gc.draw(rectangle); + + gc.setColor(oldColor); + } + + private Rectangle loLoRectangle; + private Rectangle lowRectangle; + private Rectangle highRectangle; + private Rectangle hiHiRectangle; + private Rectangle normalRectangle; + + private int marginLeft, marginAbove, marginRight, marginBelow = 0; + + public double pixelsPerScaleUnit = 1.0; + + private int meterBreadth = 0; + + private void layout() { + + double displayedLoLo; + double displayedLow; + double displayedHiHi; + double displayedHigh; + + displayedLoLo = Double.isFinite(loLo) ? Math.max(loLo, linearMeterScale.getValueRange().getLow()) : linearMeterScale.getValueRange().getLow(); + displayedLow = Double.isFinite(low) ? Math.max(Math.max(low, linearMeterScale.getValueRange().getLow()), displayedLoLo) : linearMeterScale.getValueRange().getLow(); + + displayedHiHi = Double.isFinite(high) ? Math.min(hiHi, linearMeterScale.getValueRange().getHigh()) : linearMeterScale.getValueRange().getHigh(); + displayedHigh = Double.isFinite(hiHi) ? Math.min(Math.min(high, linearMeterScale.getValueRange().getHigh()), displayedHiHi) : linearMeterScale.getValueRange().getHigh(); + + FontMetrics fontMetrics = null; + if (font != null) { + Canvas canvas = new Canvas(); + fontMetrics = canvas.getFontMetrics(font); + } + + if (linearMeterScale.isHorizontal()) { + marginAbove = knobSize >= 1 ? knobSize + 2 : 0; + if (linearMeterScale.isVisible() && fontMetrics != null) { + var majorTicks = linearMeterScale.getTicks().getMajorTicks(); + if (majorTicks.size() >= 2) { + marginLeft = fontMetrics.stringWidth(majorTicks.get(0).getLabel()) / 2; + marginRight = fontMetrics.stringWidth(majorTicks.get(majorTicks.size() - 1).getLabel()) / 2; + } else if (majorTicks.size() == 1) { + marginRight = marginLeft = fontMetrics.stringWidth(majorTicks.get(0).getLabel()) / 2; + } else { + marginRight = 0; + marginLeft = 0; + } + marginBelow = (int) (0.5 * linearMeterScale.getTickLength() + 4 + fontMetrics.getAscent() + fontMetrics.getDescent()); + } else { + marginLeft = 0; + marginRight = 1; + marginBelow = 1; + } + + if (showUnits && fontMetrics != null) { + marginBelow += 1 + fontMetrics.getMaxAscent() + fontMetrics.getMaxDescent(); + } + + pixelsPerScaleUnit = (linearMeterScale.getBounds().width - marginLeft - marginRight) / (linearMeterScale.getValueRange().getHigh() - linearMeterScale.getValueRange().getLow()); + meterBreadth = Math.round(linearMeterScale.getBounds().height - marginAbove - marginBelow); + + double x_loLoRectangle = marginLeft; + double x_lowRectangle = marginLeft + pixelsPerScaleUnit * (displayedLoLo - linearMeterScale.getValueRange().getLow()); + double x_normalRectangle = marginLeft + pixelsPerScaleUnit * (displayedLow - linearMeterScale.getValueRange().getLow()); + double x_highRectangle = marginLeft + pixelsPerScaleUnit * (displayedHigh - linearMeterScale.getValueRange().getLow()); + double x_hiHiRectangle = marginLeft + pixelsPerScaleUnit * (displayedHiHi - linearMeterScale.getValueRange().getLow()); + + loLoRectangle = new Rectangle((int) Math.round(x_loLoRectangle), + marginAbove, + (int) (Math.round(x_lowRectangle) - Math.round(x_loLoRectangle)), + meterBreadth); + + lowRectangle = new Rectangle((int) Math.round(x_lowRectangle), + marginAbove, + (int) (Math.round(x_normalRectangle) - Math.round(x_lowRectangle)), + meterBreadth); + + normalRectangle = new Rectangle((int) Math.round(x_normalRectangle), + marginAbove, + (int) (Math.round(x_highRectangle) - Math.round(x_normalRectangle)), + meterBreadth); + + highRectangle = new Rectangle((int) Math.round(x_highRectangle), + marginAbove, + (int) (Math.round(x_hiHiRectangle) - Math.round(x_highRectangle)), + meterBreadth); + + hiHiRectangle = new Rectangle((int) Math.round(x_hiHiRectangle), + marginAbove, + (int) (Math.round(pixelsPerScaleUnit * (linearMeterScale.getValueRange().getHigh() - displayedHiHi))), + meterBreadth); + } + else { + marginLeft = knobSize >= 1 ? knobSize + 2 : 0; + if (linearMeterScale.isVisible() && fontMetrics != null) { + int maxTickLabelWidth = 0; + maxTickLabelWidth = 0; + var majorTicks = linearMeterScale.getTicks().getMajorTicks(); + for (var majorTick : majorTicks) { + int labelStringWidth = fontMetrics.stringWidth(majorTick.getLabel()); + maxTickLabelWidth = Math.max(maxTickLabelWidth, labelStringWidth); + } + marginRight = RTLinearMeter.this.linearMeterScale.getTickLength() + maxTickLabelWidth + 1; + marginAbove = fontMetrics.getAscent() / 2 + 1; + marginBelow = fontMetrics.getAscent() / 2 + 1; + } else { + marginRight = 1; + marginAbove = 0; + marginBelow = 1; + } + + if (showUnits && fontMetrics != null) { + marginBelow += 1 + fontMetrics.getMaxAscent() + fontMetrics.getMaxDescent(); + } + + pixelsPerScaleUnit = (linearMeterScale.getBounds().height - marginAbove - marginBelow) / (linearMeterScale.getValueRange().getHigh() - linearMeterScale.getValueRange().getLow()); + meterBreadth = Math.round(linearMeterScale.getBounds().width - marginLeft - marginRight); + + double y_loLoRectangle = marginAbove + pixelsPerScaleUnit * (linearMeterScale.getValueRange().getHigh() - displayedLoLo); + double y_lowRectangle = marginAbove + pixelsPerScaleUnit * (linearMeterScale.getValueRange().getHigh() - displayedLow); + double y_normalRectangle = marginAbove + pixelsPerScaleUnit * (linearMeterScale.getValueRange().getHigh() - displayedHigh); + double y_highRectangle = marginAbove + pixelsPerScaleUnit * (linearMeterScale.getValueRange().getHigh() - displayedHiHi); + + loLoRectangle = new Rectangle(marginLeft, + (int) Math.round(y_loLoRectangle), + meterBreadth, + (int) (Math.round(pixelsPerScaleUnit * (displayedLoLo - linearMeterScale.getValueRange().getLow()) ))); + + lowRectangle = new Rectangle(marginLeft, + (int) Math.round(y_lowRectangle), + meterBreadth, + (int) (Math.round(y_loLoRectangle) - Math.round(y_lowRectangle))); + + normalRectangle = new Rectangle(marginLeft, + (int) Math.round(y_normalRectangle), + meterBreadth, + (int) (Math.round(y_lowRectangle) - Math.round(y_normalRectangle))); + + highRectangle = new Rectangle(marginLeft, + (int) Math.round(y_highRectangle), + meterBreadth, + (int) (Math.round(y_normalRectangle) - Math.round(y_highRectangle))); + + hiHiRectangle = new Rectangle(marginLeft, + marginAbove, + meterBreadth, + (int) Math.round(y_highRectangle) - marginAbove); + } + } +} diff --git a/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.WidgetsService b/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.WidgetsService new file mode 100644 index 0000000000..b0eec70373 --- /dev/null +++ b/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.model.spi.WidgetsService @@ -0,0 +1 @@ +org.csstudio.display.extra.widgets.LinearMeterWidgetService diff --git a/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.representation.spi.WidgetRepresentationsService b/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.representation.spi.WidgetRepresentationsService new file mode 100644 index 0000000000..6a2ae23f2c --- /dev/null +++ b/app/display/linearmeter/src/main/resources/META-INF/services/org.csstudio.display.builder.representation.spi.WidgetRepresentationsService @@ -0,0 +1 @@ +org.csstudio.display.extra.widgets.LinearMeterRepresentationService diff --git a/app/display/linearmeter/src/main/resources/graphics/Warning_Triangle_Red.png b/app/display/linearmeter/src/main/resources/graphics/Warning_Triangle_Red.png new file mode 100644 index 0000000000..616ba4e502 Binary files /dev/null and b/app/display/linearmeter/src/main/resources/graphics/Warning_Triangle_Red.png differ diff --git a/app/display/linearmeter/src/main/resources/icons/linear-meter.png b/app/display/linearmeter/src/main/resources/icons/linear-meter.png new file mode 100644 index 0000000000..dcdae16e43 Binary files /dev/null and b/app/display/linearmeter/src/main/resources/icons/linear-meter.png differ diff --git a/app/display/linearmeter/src/main/resources/icons/linear-meter@2x.png b/app/display/linearmeter/src/main/resources/icons/linear-meter@2x.png new file mode 100644 index 0000000000..ba5ce0d3e5 Binary files /dev/null and b/app/display/linearmeter/src/main/resources/icons/linear-meter@2x.png differ diff --git a/app/display/linearmeter/src/main/resources/icons/linear-meter@3x.png b/app/display/linearmeter/src/main/resources/icons/linear-meter@3x.png new file mode 100644 index 0000000000..aa0955c173 Binary files /dev/null and b/app/display/linearmeter/src/main/resources/icons/linear-meter@3x.png differ diff --git a/app/display/model/.classpath b/app/display/model/.classpath index 43447eea5f..d2913cb056 100644 --- a/app/display/model/.classpath +++ b/app/display/model/.classpath @@ -8,7 +8,7 @@ - - + + diff --git a/app/display/model/pom.xml b/app/display/model/pom.xml index 8e9ea7319c..1cd8e3dc9f 100644 --- a/app/display/model/pom.xml +++ b/app/display/model/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-model @@ -22,12 +22,12 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/ChildrenProperty.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/ChildrenProperty.java index 052bdfb543..6afd08ae0c 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/ChildrenProperty.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/ChildrenProperty.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -133,6 +133,11 @@ public void setValue(final List new_value) value.clear(); value.addAll(new_value); } + // Inform widgets of their (new) parent + for (Widget child : old) + child.setParent(null); + for (Widget child : value) + child.setParent(getWidget()); firePropertyChange(old, new_value); } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/DisplayModel.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/DisplayModel.java index cf1d8cf393..fafff17280 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/DisplayModel.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/DisplayModel.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2021 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -89,16 +89,6 @@ public class DisplayModel extends Widget */ public static final String USER_DATA_EMBEDDING_WIDGET = "_embedding_widget"; - /** Macros set in preferences - * - *

Fetched once on display creation to - * use latest preference settings on newly opened display, - * while not fetching preferences for each macro evaluation - * within a running display to improve performance - */ - private final Macros preference_macros = Preferences.getMacros(); - - /** Custom configurator to read legacy *.opi files */ private static class CustomConfigurator extends WidgetConfigurator @@ -140,12 +130,12 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid /** 'grid_step_x' property */ public static final WidgetPropertyDescriptor propGridStepX = newIntegerPropertyDescriptor(WidgetPropertyCategory.MISC, "grid_step_x", Messages.WidgetProperties_GridStepX, - 4, Integer.MAX_VALUE); + 1, Integer.MAX_VALUE); /** 'grid_step_y' property */ public static final WidgetPropertyDescriptor propGridStepY = newIntegerPropertyDescriptor(WidgetPropertyCategory.MISC, "grid_step_y", Messages.WidgetProperties_GridStepY, - 4, Integer.MAX_VALUE); + 1, Integer.MAX_VALUE); private volatile WidgetProperty macros; private volatile WidgetProperty background; @@ -302,25 +292,27 @@ protected void setParent(final Widget parent) throw new IllegalStateException("Display cannot have parent widget " + parent); } + /** Expand macros for this display's widget hierarchy + * @param base Base macros from preferences, launcher, parent container + */ + @Override + public void expandMacros(final Macros base) + { + // Expand the display macros + propMacros().getValue().expandValues(base); + + // Recurse into child widgets + for (Widget child: getChildren()) + child.expandMacros(propMacros().getValue()); + } + /** Display model provides macros for all its widgets. * @return {@link Macros} */ @Override public Macros getEffectiveMacros() { - // 1) Lowest priority are either - // 1.a) .. global macros from preferences - // 1.b) .. macros from embedding widget, - // which may in turn be embedded elsewhere, - // ultimately fetching the macros from preferences. - final Widget embedder = getUserData(DisplayModel.USER_DATA_EMBEDDING_WIDGET); - Macros result = (embedder == null) - ? preference_macros - : embedder.getEffectiveMacros(); - - // 2) This display may provide added macros or replacement values - result = Macros.merge(result, propMacros().getValue()); - return result; + return propMacros().getValue(); } /** @return 'background_color' property */ diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java index 8cfb74de30..f2c07a5a33 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java @@ -279,6 +279,7 @@ public class Messages WidgetProperties_RingWidth, WidgetProperties_Rotation, WidgetProperties_Rules, + WidgetProperties_RunActionsOnMouseClick, WidgetProperties_Running, WidgetProperties_ScaleFactor, WidgetProperties_ScaleFormat, diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Preferences.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Preferences.java index 39e282621b..0b3317dbe1 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Preferences.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Preferences.java @@ -35,6 +35,8 @@ public class Preferences @Preference public static boolean skip_defaults; /** Preference setting */ @Preference(name="macros") private static String macro_spec; + /** Preference setting */ + @Preference public static boolean enable_saved_on_comments; private static Macros macros; static diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java index 54db01d25a..d5fddc0031 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Widget.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2022 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -730,6 +730,14 @@ public final void setPropertyValue(final String name, getProperty(name).setValueFromObject(value); } + /** Expand macros for this widget and potential child widgets + * @param input Input that should be used to expand macros + */ + public void expandMacros(final Macros input) + { + // Plain widget has no macros to expand + } + /** Determine effective macros. * *

Default implementation requests macros diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/DisplayMacroExpander.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/DisplayMacroExpander.java deleted file mode 100644 index 116d3af8c2..0000000000 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/DisplayMacroExpander.java +++ /dev/null @@ -1,47 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - *******************************************************************************/ -package org.csstudio.display.builder.model.macros; - -import java.util.Optional; - -import org.csstudio.display.builder.model.ChildrenProperty; -import org.csstudio.display.builder.model.DisplayModel; -import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.WidgetProperty; -import org.csstudio.display.builder.model.properties.CommonWidgetProperties; -import org.phoebus.framework.macros.MacroValueProvider; -import org.phoebus.framework.macros.Macros; - -/** Expand macros of a {@link DisplayModel} - * @author Kay Kasemir - */ -public class DisplayMacroExpander -{ - /** Expand macros of the model, recursing into child widgets - * @param model {@link DisplayModel} - * @throws Exception on error - */ - public static void expandDisplayMacros(final DisplayModel model) throws Exception - { - expand(model.runtimeChildren(), model.getMacrosOrProperties()); - } - - private static void expand(final ChildrenProperty widgets, MacroValueProvider input) throws Exception - { - for (Widget widget : widgets.getValue()) - { - final Optional> macros = widget.checkProperty(CommonWidgetProperties.propMacros); - if (macros.isPresent()) - macros.get().getValue().expandValues(input); - // Recurse - final ChildrenProperty children = ChildrenProperty.getChildren(widget); - if (children != null) - expand(children, widget.getMacrosOrProperties()); - } - } -} diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/MacroXMLUtil.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/MacroXMLUtil.java index 3096cc6632..e7bedde541 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/MacroXMLUtil.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/macros/MacroXMLUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,6 +11,8 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; import javax.xml.stream.XMLOutputFactory; @@ -34,11 +36,25 @@ public class MacroXMLUtil */ public static void writeMacros(final XMLStreamWriter writer, final Macros macros) throws Exception { - for (String name : macros.getNames()) + try { - writer.writeStartElement(name); - writer.writeCharacters(macros.getValue(name)); - writer.writeEndElement(); + macros.forEachSpec((name, value) -> + { + try + { + writer.writeStartElement(name); + writer.writeCharacters(value); + writer.writeEndElement(); + } + catch (Exception ex) + { // Abort iterator via Error... + throw new Error(ex); + } + }); + } + catch (Error ex) + { // .. then pass original exception back up + throw (Exception) ex.getCause(); } } @@ -104,4 +120,17 @@ public static String toString(final Macros macros) } return xml.toString(); } + + /** @deprecated Use macros.getValue("M") or macros.getNames() + * @param macros Macros to represent as Map + * @return Map representation for macros + */ + public static Map toMap(final Macros macros) + { + Map readMacroMap = new HashMap<>(); + macros.forEach((s, s2) -> { + readMacroMap.put(s,s2); + }); + return readMacroMap; + } } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/persist/ModelWriter.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/persist/ModelWriter.java index d87710ebe4..162b855f35 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/persist/ModelWriter.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/persist/ModelWriter.java @@ -43,6 +43,7 @@ public class ModelWriter implements Closeable { /** Add comments to the XML output? */ public static boolean with_comments = Preferences.with_comments; + public static boolean enable_saved_on_comment = Preferences.enable_saved_on_comments; /** Default values are usually not written, * but for tests they can be included in the XML output. @@ -83,8 +84,9 @@ public ModelWriter(final OutputStream stream) throws Exception writer = new IndentingXMLStreamWriter(base); writer.writeStartDocument(XMLUtil.ENCODING, "1.0"); - if (with_comments) - writer.writeComment("Created " + TimestampFormats.DATETIME_FORMAT.format(Instant.now())); + if (enable_saved_on_comment) { + writer.writeComment("Saved on " + TimestampFormats.SECONDS_FORMAT.format(Instant.now()) + " by " + System.getProperty("user.name")); + } writer.writeStartElement(XMLTags.DISPLAY); writer.writeAttribute(XMLTags.VERSION, DisplayModel.VERSION.toString()); } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/ActionsWidgetProperty.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/ActionsWidgetProperty.java index d7b7af50c7..942450e95d 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/ActionsWidgetProperty.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/properties/ActionsWidgetProperty.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Oak Ridge National Laboratory. + * Copyright (c) 2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,6 +19,7 @@ import javax.xml.stream.XMLStreamWriter; +import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyDescriptor; @@ -216,7 +217,7 @@ public void readFromXML(final ModelReader model_reader, final Element property_x action_xml.appendChild(target); } - final String description = XMLUtil.getChildString(action_xml, XMLTags.DESCRIPTION).orElse(""); + String description = XMLUtil.getChildString(action_xml, XMLTags.DESCRIPTION).orElse(""); if (OPEN_DISPLAY.equalsIgnoreCase(type)) // legacy used uppercase type name { // Use , falling back to legacy final String file = XMLUtil.getChildString(action_xml, XMLTags.FILE) @@ -251,7 +252,8 @@ else if (mode.isPresent()) macros = new Macros(); final String pane = XMLUtil.getChildString(action_xml, XMLTags.NAME).orElse(""); - + if (description.isEmpty()) + description = Messages.ActionOpenDisplay; actions.add(new OpenDisplayActionInfo(description, file, macros, target, pane)); } else if (WRITE_PV.equalsIgnoreCase(type)) // legacy used uppercase type name @@ -274,6 +276,8 @@ else if (WRITE_PV.equalsIgnoreCase(type)) // legacy used uppercase type name // In contrast to legacy opibuilder the value is _not_ trimmed, // so it's possible to write " " (which opibuilder wrote as "") final String value = XMLUtil.getChildString(action_xml, XMLTags.VALUE).orElse(""); + if (description.isEmpty()) + description = Messages.ActionWritePV; actions.add(new WritePVActionInfo(description, pv_name, value)); } else if (EXECUTE_SCRIPT.equals(type)) @@ -289,6 +293,8 @@ else if (EXECUTE_SCRIPT.equals(type)) final String path = el.getAttribute(XMLTags.FILE); final String text = XMLUtil.getChildString(el, XMLTags.TEXT).orElse(null); final ScriptInfo info = new ScriptInfo(path, text, false, Collections.emptyList()); + if (description.isEmpty()) + description = Messages.ActionExecuteScript; actions.add(new ExecuteScriptActionInfo(description, info)); } } @@ -314,6 +320,8 @@ else if ("EXECUTE_PYTHONSCRIPT".equalsIgnoreCase(type) || } else info = new ScriptInfo(path, null, false, Collections.emptyList()); + if (description.isEmpty()) + description = Messages.ActionExecuteScript; actions.add(new ExecuteScriptActionInfo(description, info)); } else if (OPEN_FILE.equalsIgnoreCase(type)) // legacy used uppercase type name @@ -321,6 +329,8 @@ else if (OPEN_FILE.equalsIgnoreCase(type)) // legacy used uppercase type name final String file = XMLUtil.getChildString(action_xml, XMLTags.FILE) .orElse(XMLUtil.getChildString(action_xml, XMLTags.PATH) .orElse("")); + if (description.isEmpty()) + description = Messages.ActionOpenFile; actions.add(new OpenFileActionInfo(description, file)); } else if (OPEN_WEBPAGE.equalsIgnoreCase(type)) // legacy used uppercase type name @@ -328,6 +338,8 @@ else if (OPEN_WEBPAGE.equalsIgnoreCase(type)) // legacy used uppercase type name final String url = XMLUtil.getChildString(action_xml, XMLTags.URL) .orElse(XMLUtil.getChildString(action_xml, "hyperlink") .orElse("")); + if (description.isEmpty()) + description = Messages.ActionOpenWebPage; actions.add(new OpenWebpageActionInfo(description, url)); } else if (EXECUTE_COMMAND.equalsIgnoreCase(type) || @@ -360,6 +372,8 @@ else if (EXECUTE_COMMAND.equalsIgnoreCase(type) || // If a legacy directory was provided, locate command there if (directory != null && !directory.isEmpty()) command = directory + "/" + command; + if (description.isEmpty()) + description = Messages.ActionExecuteCommand; actions.add(new ExecuteCommandActionInfo(description, command)); } else diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java index c2ee1427b9..a86712dee5 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java @@ -13,6 +13,8 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propEnabled; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontalAlignment; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propVerticalAlignment; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propRotationStep; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propText; @@ -36,7 +38,9 @@ import org.csstudio.display.builder.model.persist.WidgetFontService; import org.csstudio.display.builder.model.persist.XMLTags; import org.csstudio.display.builder.model.properties.CommonWidgetProperties; +import org.csstudio.display.builder.model.properties.HorizontalAlignment; import org.csstudio.display.builder.model.properties.RotationStep; +import org.csstudio.display.builder.model.properties.VerticalAlignment; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.properties.WidgetFont; import org.phoebus.framework.persistence.XMLUtil; @@ -226,6 +230,8 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty foreground; private volatile WidgetProperty background; private volatile WidgetProperty transparent; + private volatile WidgetProperty horizontal_alignment; + private volatile WidgetProperty vertical_alignment; private volatile WidgetProperty rotation_step; private volatile WidgetProperty pv_writable; private volatile WidgetProperty confirm_dialog; @@ -257,6 +263,8 @@ protected void defineProperties(final List> properties) properties.add(foreground = propForegroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.TEXT))); properties.add(background = propBackgroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.BUTTON_BACKGROUND))); properties.add(transparent = propTransparent.createProperty(this, false)); + properties.add(horizontal_alignment = propHorizontalAlignment.createProperty(this, HorizontalAlignment.CENTER)); + properties.add(vertical_alignment = propVerticalAlignment.createProperty(this, VerticalAlignment.MIDDLE)); properties.add(rotation_step = propRotationStep.createProperty(this, RotationStep.NONE)); properties.add(enabled = propEnabled.createProperty(this, true)); properties.add(pv_writable = runtimePropPVWritable.createProperty(this, true)); @@ -302,6 +310,18 @@ public WidgetProperty propTransparent() { return transparent; } + + /** @return 'horizontal_alignment' property */ + public WidgetProperty propHorizontalAlignment() + { + return horizontal_alignment; + } + + /** @return 'vertical_alignment' property */ + public WidgetProperty propVerticalAlignment() + { + return vertical_alignment; + } /** @return 'rotation_step' property */ public WidgetProperty propRotationStep() diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ArrayWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ArrayWidget.java index efc499e5dc..670f20d73b 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ArrayWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ArrayWidget.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -95,17 +95,20 @@ protected void defineProperties(final List> properties) properties.add(insets = runtimePropInsets.createProperty(this, new int[] { 0, 0 })); } - /** - * Array widget extends parent macros - * - * @return {@link Macros} - */ + /** {@inheritDoc} */ + @Override + public void expandMacros(final Macros input) + { + macros.getValue().expandValues(input); + for (Widget child : children.getValue()) + child.expandMacros(macros.getValue()); + } + + /** {@inheritDoc} */ @Override public Macros getEffectiveMacros() { - final Macros base = super.getEffectiveMacros(); - final Macros my_macros = propMacros().getValue(); - return Macros.merge(base, my_macros); + return propMacros().getValue(); } /** @return 'macros' property */ diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/BoolButtonWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/BoolButtonWidget.java index 57957d9d7e..b5a1742f6c 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/BoolButtonWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/BoolButtonWidget.java @@ -16,6 +16,8 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propEnabled; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontalAlignment; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propVerticalAlignment; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propLabelsFromPV; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propOffColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propOffLabel; @@ -42,6 +44,8 @@ import org.csstudio.display.builder.model.persist.WidgetFontService; import org.csstudio.display.builder.model.properties.ConfirmDialog; import org.csstudio.display.builder.model.properties.EnumWidgetProperty; +import org.csstudio.display.builder.model.properties.HorizontalAlignment; +import org.csstudio.display.builder.model.properties.VerticalAlignment; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.properties.WidgetFont; import org.phoebus.framework.persistence.XMLUtil; @@ -170,6 +174,8 @@ public EnumWidgetProperty createProperty(final Widget widget, final Mode d private volatile WidgetProperty font; private volatile WidgetProperty background; private volatile WidgetProperty foreground; + private volatile WidgetProperty horizontal_alignment; + private volatile WidgetProperty vertical_alignment; private volatile WidgetProperty labels_from_pv; private volatile WidgetProperty enabled; private volatile WidgetProperty mode; @@ -205,6 +211,8 @@ protected void defineProperties(final List> properties) properties.add(font = propFont.createProperty(this, WidgetFontService.get(NamedWidgetFonts.DEFAULT))); properties.add(foreground = propForegroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.TEXT))); properties.add(background = propBackgroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.BUTTON_BACKGROUND))); + properties.add(horizontal_alignment = propHorizontalAlignment.createProperty(this, HorizontalAlignment.CENTER)); + properties.add(vertical_alignment = propVerticalAlignment.createProperty(this, VerticalAlignment.MIDDLE)); properties.add(labels_from_pv = propLabelsFromPV.createProperty(this, false)); properties.add(enabled = propEnabled.createProperty(this, true)); properties.add(mode = propMode.createProperty(this, Mode.TOGGLE)); @@ -278,6 +286,18 @@ public WidgetProperty propForegroundColor() { return foreground; } + + /** @return 'horizontal_alignment' property */ + public WidgetProperty propHorizontalAlignment() + { + return horizontal_alignment; + } + + /** @return 'vertical_alignment' property */ + public WidgetProperty propVerticalAlignment() + { + return vertical_alignment; + } /** @return 'labels_from_pv' property */ public WidgetProperty propLabelsFromPV() diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ChoiceButtonWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ChoiceButtonWidget.java index 7a81407e96..5e3f407eec 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ChoiceButtonWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ChoiceButtonWidget.java @@ -14,6 +14,8 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontal; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontalAlignment; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propVerticalAlignment; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propItemsFromPV; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propPassword; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propSelectedColor; @@ -34,6 +36,8 @@ import org.csstudio.display.builder.model.persist.WidgetColorService; import org.csstudio.display.builder.model.persist.WidgetFontService; import org.csstudio.display.builder.model.properties.CommonWidgetProperties; +import org.csstudio.display.builder.model.properties.HorizontalAlignment; +import org.csstudio.display.builder.model.properties.VerticalAlignment; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.properties.WidgetFont; @@ -66,6 +70,8 @@ public Widget createWidget() private volatile WidgetProperty selected; private volatile ArrayWidgetProperty> items; private volatile WidgetProperty items_from_pv; + private volatile WidgetProperty horizontal_alignment; + private volatile WidgetProperty vertical_alignment; private volatile WidgetProperty horizontal; private volatile WidgetProperty enabled; private volatile WidgetProperty confirm_dialog; @@ -97,6 +103,8 @@ protected void defineProperties(final List> properties) propItem.createProperty(this, Messages.ComboWidget_Item + " 2"))); properties.add(items); properties.add(items_from_pv = propItemsFromPV.createProperty(this, true)); + properties.add(horizontal_alignment = propHorizontalAlignment.createProperty(this, HorizontalAlignment.CENTER)); + properties.add(vertical_alignment = propVerticalAlignment.createProperty(this, VerticalAlignment.MIDDLE)); properties.add(horizontal = propHorizontal.createProperty(this, true)); properties.add(enabled = propEnabled.createProperty(this, true)); properties.add(confirm_dialog = propConfirmDialog.createProperty(this, false)); @@ -139,6 +147,18 @@ public WidgetProperty< List> > propItems() { return items; } + + /** @return 'horizontal_alignment' property */ + public WidgetProperty propHorizontalAlignment() + { + return horizontal_alignment; + } + + /** @return 'vertical_alignment' property */ + public WidgetProperty propVerticalAlignment() + { + return vertical_alignment; + } /** @return 'horizontal' property */ public WidgetProperty propHorizontal() diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/EmbeddedDisplayWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/EmbeddedDisplayWidget.java index 68db2347fd..4eab584967 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/EmbeddedDisplayWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/EmbeddedDisplayWidget.java @@ -179,8 +179,10 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid widget.setPropertyValue(propResize, Resize.ResizeContent); else if (old_resize == 1) widget.setPropertyValue(propResize, Resize.SizeToContent); - else // 'scroll' or 'crop' -> crop + else if (old_resize == 2) //'crop' -> crop widget.setPropertyValue(propResize, Resize.Crop); + else + widget.setPropertyValue(propResize, Resize.None); } catch (NumberFormatException ex) { diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/GroupWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/GroupWidget.java index 273a5b9a08..c29b91f986 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/GroupWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/GroupWidget.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2020 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,6 +10,7 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propForegroundColor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propLineColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propTransparent; import static org.csstudio.display.builder.model.properties.InsetsWidgetProperty.runtimePropExtendedInsets; @@ -34,6 +35,7 @@ import org.csstudio.display.builder.model.properties.EnumWidgetProperty; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.model.properties.WidgetFont; +import org.phoebus.framework.macros.Macros; import org.phoebus.framework.persistence.XMLUtil; import org.w3c.dom.Element; @@ -55,20 +57,23 @@ @SuppressWarnings("nls") public class GroupWidget extends MacroWidget { + /** Group Widget version */ + public static final Version GROUP_WIDGET_VERSION = new Version(3, 0, 0); + /** Widget descriptor */ public static final WidgetDescriptor WIDGET_DESCRIPTOR = - new WidgetDescriptor("group", WidgetCategory.STRUCTURE, - Messages.GroupWidget_Name, - "/icons/group.png", - Messages.GroupWidget_Description, - Arrays.asList("org.csstudio.opibuilder.widgets.groupingContainer")) - { - @Override - public Widget createWidget() - { - return new GroupWidget(); - } - }; + new WidgetDescriptor("group", WidgetCategory.STRUCTURE, + Messages.GroupWidget_Name, + "/icons/group.png", + Messages.GroupWidget_Description, + Arrays.asList("org.csstudio.opibuilder.widgets.groupingContainer")) + { + @Override + public Widget createWidget() + { + return new GroupWidget(); + } + }; /** Group widget style */ public enum Style @@ -98,16 +103,16 @@ public String toString() /** 'style' property */ static final WidgetPropertyDescriptor + true + + Text Entry + 90 + 120 + 30 + + + Label_7 + Enter Value: + 80 + 32 + 1 + Label_4 + Array of Numbers: 170 121 - Array of Numbers: @@ -140,10 +158,10 @@ all child widgets. Label_5 + Array of Strings with Text Updates 355 170 240 - Array of Strings with Text Updates @@ -151,13 +169,25 @@ all child widgets. Label_6 + Same Strings with Text Entries 630 170 240 - Same Strings with Text Entries + + Label_8 + This example further demonstrates that a (small) group can be used to represent each array element. + 870 + 315 + 150 + 80 + + + + + diff --git a/app/display/model/src/main/resources/examples/textupdate_format.bob b/app/display/model/src/main/resources/examples/textupdate_format.bob index 88528e0327..54933d0910 100644 --- a/app/display/model/src/main/resources/examples/textupdate_format.bob +++ b/app/display/model/src/main/resources/examples/textupdate_format.bob @@ -1,6 +1,6 @@ - Display + Text Update Formatting DTL_LLRF:IOC1:Load diff --git a/app/display/model/src/main/resources/examples/use_classes.bob b/app/display/model/src/main/resources/examples/use_classes.bob index 0c4ddbecbb..6fb9d0204d 100644 --- a/app/display/model/src/main/resources/examples/use_classes.bob +++ b/app/display/model/src/main/resources/examples/use_classes.bob @@ -1,6 +1,6 @@ - Display + Classes Label TITLE diff --git a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties index 7dc85329cb..bc11431af0 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties @@ -262,6 +262,7 @@ WidgetProperties_RingColor=Ring Color WidgetProperties_RingWidth=Ring Width WidgetProperties_Rotation=Rotation WidgetProperties_Rules=Rules +WidgetProperties_RunActionsOnMouseClick=Run Actions on Mouse Click WidgetProperties_Running=Running WidgetProperties_ScaleFactor=Scale Factor WidgetProperties_ScaleFormat=Scale Format diff --git a/app/display/model/src/main/resources/org/csstudio/display/builder/model/persist/persist_example.xml b/app/display/model/src/main/resources/org/csstudio/display/builder/model/persist/persist_example.xml index ebf2852845..066e99ea8b 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/persist/persist_example.xml +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/persist/persist_example.xml @@ -4,7 +4,7 @@ Demo 7 - + My Group Jänner diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java index ee1bce07c0..12d1493dd2 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/AllWidgetsAllProperties.java @@ -56,6 +56,7 @@ public static void main(String[] args) throws Exception } ModelWriter.with_comments = true; ModelWriter.skip_defaults = false; + ModelWriter.enable_saved_on_comment = true; try { final ModelWriter writer = new ModelWriter(new FileOutputStream(filename)); diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/ArrayWidgetPropertyUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/ArrayWidgetPropertyUnitTest.java index 8e3238c51e..e1d753ffca 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/ArrayWidgetPropertyUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/ArrayWidgetPropertyUnitTest.java @@ -148,7 +148,7 @@ public void testPersist() throws Exception // Set value to non-default widget.propItems().getValue().get(1).setValue("Another (2)"); // Persist to XML - final String xml = ModelWriter.getXML(List.of(widget)); + final String xml = removeSavedOn(ModelWriter.getXML(List.of(widget))); System.out.println(xml); assertThat(xml, containsString("")); assertThat(xml, containsString("Another")); @@ -156,11 +156,16 @@ public void testPersist() throws Exception // Read from XML WidgetFactory.getInstance().addWidgetType(DemoWidget.WIDGET_DESCRIPTOR); final List read_back = ModelReader.parseXML(xml).getChildren(); - final String xml2 = ModelWriter.getXML(read_back); + final String xml2 = removeSavedOn(ModelWriter.getXML(read_back)); System.out.println(xml2); assertThat(xml2, equalTo(xml)); } + private String removeSavedOn(final String xml) + { + return xml.replaceFirst("", ""); + } + @Test public void testArrayAccess() { diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacroHierarchyUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacroHierarchyUnitTest.java index d577115396..8bb6b7dd02 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacroHierarchyUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacroHierarchyUnitTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2021 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -27,34 +27,12 @@ @SuppressWarnings("nls") public class MacroHierarchyUnitTest { - /** Test Macro Hierarchy - */ + /** Test Macro Hierarchy */ @Test public void testMacroHierarchy() { - // Macros start out empty - MacroValueProvider macros = new Macros(); - System.out.println(macros); - assertThat(macros.toString(), equalTo("[]")); - - // Preferences (at least in test setup where there is no preferences service) - macros = Preferences.getMacros(); - System.out.println(macros); - assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("Value from Preferences")); - - // Display model uses preferences - final DisplayModel model = new DisplayModel(); - macros = model.getEffectiveMacros(); - System.out.println(macros); - assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("Value from Preferences")); - - // .. but display can replace this value - model.propMacros().getValue().add("EXAMPLE_MACRO", "Value from Display"); - macros = model.getEffectiveMacros(); - System.out.println(macros); - assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("Value from Display")); + // prefs -> model -> group1 -> group2 -> child - // Similar, groups can replace macros final LabelWidget child = new LabelWidget(); final GroupWidget group2 = new GroupWidget(); @@ -65,8 +43,22 @@ public void testMacroHierarchy() group1.propMacros().getValue().add("EXAMPLE_MACRO", "In Group 1"); group1.runtimeChildren().addChild(group2); + final DisplayModel model = new DisplayModel(); + model.propMacros().getValue().add("TITLE", "Display Title"); model.runtimeChildren().addChild(group1); + Macros prefs = new Macros(); + prefs.add("EXAMPLE_MACRO", "Value from Preferences"); + + // Expand macros of model, recursively, with prefs as input + model.expandMacros(prefs); + + // Model inherits from prefs + Macros macros = model.getEffectiveMacros(); + System.out.println(macros); + assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("Value from Preferences")); + + // Groups replace... macros = group1.getEffectiveMacros(); System.out.println(macros); assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("In Group 1")); @@ -75,22 +67,21 @@ public void testMacroHierarchy() System.out.println(macros); assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("In Group 2")); + // child has replacement from group2 but also inherited macros macros = child.getEffectiveMacros(); System.out.println(macros); assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("In Group 2")); - - // Finally, the EmbeddedDisplayWidget can replace macros, - // but testing that requires the runtime to load the embedded content - // --> Leaving that to examples/macros + assertThat(macros.getValue("TITLE"), equalTo("Display Title")); } - /** Test access to widget properties, Java properties and environment - */ + /** Test access to widget properties, Java properties and environment */ @Test public void testPropertiesAndEnvironment() { // Display model uses preferences final DisplayModel model = new DisplayModel(); + model.expandMacros(Preferences.getMacros()); + MacroValueProvider macros = model.getEffectiveMacros(); assertThat(macros.getValue("EXAMPLE_MACRO"), equalTo("Value from Preferences")); @@ -108,11 +99,10 @@ public void testPropertiesAndEnvironment() // Check fall back to environment variables value = macros.getValue("HOME"); System.out.println("Environment variable $HOME: " + value); - if(System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) { + if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) assertThat(value, nullValue()); - }else { + else assertThat(value, not(nullValue())); - } } /** Test when macros get expanded @@ -140,23 +130,18 @@ public void testTimeOfExpansion() throws Exception model.propMacros().getValue().add("P", "display"); group.propMacros().getValue().add("P", "group"); subgroup.propMacros().getValue().add("P", "subgroup"); + model.expandMacros(null); Macros macros = label.getEffectiveMacros(); - System.out.println(macros); + System.out.println(macros.toExpandedString()); assertThat(macros.getValue("P"), equalTo("subgroup")); // When are macros expanded? - // In BOY, they were mostly expanded when set, - // except the following example would fail if all widgets - // were within one display. model.propMacros().getValue().add("P", "display"); - // If macros are expanded early on, - // this sets SAVE=display, - // then redefines P + // Group has P=group, so SAVE=group group.propMacros().getValue().add("SAVE", "$(P)"); - group.propMacros().getValue().add("P", "group"); - // .. and this would restore P='display', since that's what's im $(SAVE): + // Subgroup had set P to subgroup, but now updates it to $(SAVE)=group subgroup.propMacros().getValue().add("P", "$(SAVE)"); // With lazy macro expansion, @@ -164,12 +149,11 @@ public void testTimeOfExpansion() throws Exception // so $(P) results in a "recursive macro" error. // When macros are expanded as the runtime starts.. - DisplayMacroExpander.expandDisplayMacros(model); + model.expandMacros(null); - // ..you get $(P)="display" macros = label.getEffectiveMacros(); - System.out.println(macros); - assertThat(macros.getValue("P"), equalTo("display")); - assertThat(MacroHandler.replace(macros, "$(P)"), equalTo("display")); + System.out.println(macros.toExpandedString()); + assertThat(macros.getValue("P"), equalTo("group")); + assertThat(MacroHandler.replace(macros, "$(P)"), equalTo("group")); } } diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacrosUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacrosUnitTest.java index 5a09c19662..31b32deb86 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacrosUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/macros/MacrosUnitTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2021 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,31 +8,103 @@ package org.csstudio.display.builder.model.macros; import org.junit.jupiter.api.Test; +import org.phoebus.framework.macros.MacroHandler; import org.phoebus.framework.macros.Macros; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.widgets.GroupWidget; +import org.csstudio.display.builder.model.widgets.LabelWidget; + /** JUnit test of macro handling * @author Kay Kasemir */ @SuppressWarnings("nls") public class MacrosUnitTest { + /** Test redefinition of macros + * @throws Exception on error + */ + @Test + public void testMacroRedefinition() throws Exception + { + final Macros macros = new Macros(); + + // Macro 101 + macros.add("X", "x"); + assertThat(MacroHandler.replace(macros, "$(X)"), equalTo("x")); + + // Macros can be re-defined + macros.add("X", "a"); + assertThat(MacroHandler.replace(macros, "$(X)"), equalTo("a")); + + // This may be recursive, using the previous value + macros.add("X", "$(X)$(X)$(X)$(X)$(X)"); + + System.out.println("Specs: " + macros.toString()); + System.out.println("Value: " + macros.toExpandedString()); + + assertThat(MacroHandler.replace(macros, "$(X)"), equalTo("aaaaa")); + } + @Test public void testXML() throws Exception { final Macros macros = new Macros(); + macros.add("N", "13"); macros.add("S", "System"); macros.add("N", "42"); final String xml = MacroXMLUtil.toString(macros); System.out.println(xml); - assertThat(xml, equalTo("42System")); + assertThat(xml, equalTo("13System42")); final Macros readback = MacroXMLUtil.readMacros(xml); assertThat(readback.getValue("S"), equalTo("System")); + assertThat(readback.getValue("N"), equalTo("42")); assertThat(readback.getNames(), hasItems("S", "N")); } + + /** Test 'swap' of macros + * @throws Exception on error + */ + @Test + public void testMacroSwap() throws Exception + { + // display -> group -> label + final DisplayModel display = new DisplayModel(); + + final GroupWidget group = new GroupWidget(); + display.runtimeChildren().addChild(group); + + final LabelWidget label = new LabelWidget(); + group.runtimeChildren().addChild(label); + + // Display sets A=a, B=b + display.propMacros().getValue().add("A", "a"); + display.propMacros().getValue().add("B", "b"); + + // Group swaps them into A=b, B=a (and leaves X) + group.propMacros().getValue().add("X", "$(A)"); + group.propMacros().getValue().add("A", "$(B)"); + group.propMacros().getValue().add("B", "$(X)"); + + // Expanded macros from display down + display.expandMacros(null); + + // Display keeps original settings + Macros macros = display.getEffectiveMacros(); + System.out.println("Display: " + macros.toExpandedString()); + assertThat(MacroHandler.replace(macros, "$(A)"), equalTo("a")); + assertThat(MacroHandler.replace(macros, "$(B)"), equalTo("b")); + + // Label gets the swapped A/B from the group + macros = label.getEffectiveMacros(); + System.out.println("Label : " + macros.toExpandedString()); + assertThat(MacroHandler.replace(macros, "$(A)"), equalTo("b")); + assertThat(MacroHandler.replace(macros, "$(B)"), equalTo("a")); + } } diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/persist/PersistenceUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/persist/PersistenceUnitTest.java index 1d5b4ee2b2..2988e13ccd 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/persist/PersistenceUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/persist/PersistenceUnitTest.java @@ -110,7 +110,7 @@ public void testWidgetWriting() throws Exception System.out.println(xml); final String desired = getExampleFile(); - assertThat(xml.replace("\r", ""), equalTo(desired)); + assertThat(xml.replace("\r", "").replaceAll("\n", ""), equalTo(desired)); } /** Read widgets from XML diff --git a/app/display/navigation/pom.xml b/app/display/navigation/pom.xml index b4bcd832f1..487f1c9e64 100644 --- a/app/display/navigation/pom.xml +++ b/app/display/navigation/pom.xml @@ -3,7 +3,7 @@ app-display org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -13,22 +13,22 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java b/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java index f56a45ec50..9df2d3f871 100644 --- a/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java +++ b/app/display/navigation/src/main/java/org/phoebus/applications/display/navigation/ProcessOPI.java @@ -9,6 +9,7 @@ import org.csstudio.display.builder.model.properties.OpenDisplayActionInfo; import org.csstudio.display.builder.model.util.ModelResourceUtil; import org.phoebus.framework.macros.MacroHandler; +import org.phoebus.framework.macros.MacroOrSystemProvider; import org.phoebus.framework.macros.Macros; import java.io.File; @@ -93,10 +94,14 @@ public static synchronized Set getLinkedFiles(File file) // Path to resolve, after expanding macros of source widget and action OpenDisplayActionInfo action = (OpenDisplayActionInfo) openAction; try { - final Macros expanded = new Macros(action.getMacros()); - expanded.expandValues(widget.getEffectiveMacros()); - final Macros macros = Macros.merge(widget.getEffectiveMacros(), expanded); - final String expanded_path = MacroHandler.replace(macros, action.getFile()); + // Path to resolve, after expanding macros of action in environment of source widget + final Macros macros = action.getMacros(); // Not copying, just using action's macros + macros.expandValues(widget.getEffectiveMacros()); + + // For display path, use the combined macros... + String expanded_path = MacroHandler.replace(new MacroOrSystemProvider(macros), action.getFile()); + // .. but fall back to properties + expanded_path = MacroHandler.replace(widget.getMacrosOrProperties(), expanded_path); String resource = ModelResourceUtil.resolveResource(file.getPath(), expanded_path); result.add(new File(resource)); diff --git a/app/display/pom.xml b/app/display/pom.xml index e3ea4c12ca..5635275117 100644 --- a/app/display/pom.xml +++ b/app/display/pom.xml @@ -5,7 +5,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT model @@ -19,5 +19,6 @@ navigation adapters thumbwheel + linearmeter diff --git a/app/display/representation-javafx/pom.xml b/app/display/representation-javafx/pom.xml index 072fe361c0..ad7f49551e 100644 --- a/app/display/representation-javafx/pom.xml +++ b/app/display/representation-javafx/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-representation-javafx @@ -22,7 +22,7 @@ org.controlsfx controlsfx - 11.0.3 + 11.1.2 org.openjfx @@ -46,32 +46,32 @@ org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-rtplot - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-representation - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-databrowser - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-3d-viewer - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXUtil.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXUtil.java index 62f8b94d3c..adee005308 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXUtil.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXUtil.java @@ -7,6 +7,8 @@ *******************************************************************************/ package org.csstudio.display.builder.representation.javafx; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; @@ -32,6 +34,8 @@ public class JFXUtil extends org.phoebus.ui.javafx.JFXUtil { private static double font_calibration = 1.0; + private static final DecimalFormat RGBA_ALPHA_DECIMAL_FORMAT = new DecimalFormat("0.00", + DecimalFormatSymbols.getInstance(Locale.US)); static { @@ -88,12 +92,20 @@ public static String webRgbOrHex(final WidgetColor color) return "rgba(" + col.getRed() + ',' + col.getGreen() + ',' + col.getBlue() + ',' + - col.getAlpha()/255f + ')'; + RGBA_ALPHA_DECIMAL_FORMAT.format(col.getAlpha()/255f) + ')'; else return webHex(col); }); } + /** + * Extract alpha value, formatted as expected for e.g. an RGBA function + * @return alpha value in decimal form, from 0.00 to 1.00 + */ + public static String webAlpha(final WidgetColor color) { + return RGBA_ALPHA_DECIMAL_FORMAT.format(color.getAlpha()/255f); + } + /** Convert model color into web-type RGB text * @param buf StringBuilder where RGB text of the form "#FF8080" is added * @param color {@link WidgetColor} @@ -171,7 +183,7 @@ public static Font convert(final WidgetFont font) } /** Convert font to Java FX "-fx-font" shorthand form; e.g. - * [[ || ]? ] + * {@literal [[ || ]? ]} * per https://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html#typefont * @param prefix Typically "-fx-font" * @param font {@link Font} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/MacrosTable.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/MacrosTable.java index 3b2bb78ffa..07a771a645 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/MacrosTable.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/MacrosTable.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -333,7 +333,7 @@ public void addListener(final InvalidationListener listener) public void setMacros(final Macros macros) { data.clear(); - macros.forEach((name, value) -> data.add(new MacroItem(name, value))); + macros.forEachSpec((name, value) -> data.add(new MacroItem(name, value))); // Add empty final row data.add(new MacroItem("", "")); } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java index 4f46c859ab..d6b3e08b26 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java @@ -78,6 +78,10 @@ public class Messages FontDialog_Size, FontDialog_Style, Green, + HIGH, + HIHI, + LOLO, + LOW, MacrosDialog_Info, MacrosDialog_NameCol, MacrosDialog_Title, @@ -87,6 +91,7 @@ public class Messages MacrosTable_ValueHint, MoveDown, MoveUp, + NotSet, OpenInExternalEditor, Password, Password_Caption, diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetColorPopOverController.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetColorPopOverController.java index 9581645eb5..2ca9a5590c 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetColorPopOverController.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetColorPopOverController.java @@ -96,7 +96,6 @@ public class WidgetColorPopOverController implements Initializable { @FXML private Circle defaultColorCircle; @FXML private Circle originalColorCircle; - @FXML private ButtonBar buttonBar; @FXML private Button defaultButton; @FXML private Button cancelButton; @FXML private Button okButton; @@ -154,7 +153,7 @@ void setColor( WidgetColor color ) { */ private final PauseTransition debounce = new PauseTransition(Duration.millis(500)); private final UnaryOperator hexTextFilter = change -> change.getControlNewText().matches("[0-6]?\\p{XDigit}{0,6}") ? change : null; - private final StringConverter hexTextIntegerConverter = new StringConverter() { + private final StringConverter hexTextIntegerConverter = new StringConverter<>() { @Override public String toString(Integer hexAsInt) { @@ -177,7 +176,7 @@ public Integer fromString(String hexAsString) { }; - private final StringConverter hexTextColorConverter = new StringConverter() { + private final StringConverter hexTextColorConverter = new StringConverter<>() { @Override public String toString(WidgetColor widgetColor) { @@ -220,7 +219,7 @@ public void initialize( URL location, ResourceBundle resources ) { } }); - hexField.setTextFormatter(new TextFormatter(hexTextIntegerConverter, null, hexTextFilter)); + hexField.setTextFormatter(new TextFormatter<>(hexTextIntegerConverter, null, hexTextFilter)); hexField.textProperty().addListener(( observable, oldColor, newColor) -> { if(!updating) { debounce.setOnFinished(evt -> setColor(hexTextColorConverter.fromString(newColor))); @@ -228,9 +227,6 @@ public void initialize( URL location, ResourceBundle resources ) { } }); - defaultButton.disableProperty().bind(Bindings.createBooleanBinding(() -> getColor().equals(defaultColor), colorProperty())); - okButton.disableProperty().bind(Bindings.createBooleanBinding(() -> getColor().equals(originalColor), colorProperty())); - final SpinnerValueFactory redSpinnerValueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 255); final TextFormatter redSpinnerFormatter = new TextFormatter<>(redSpinnerValueFactory.getConverter(), redSpinnerValueFactory.getValue()); @@ -436,6 +432,9 @@ void setInitialConditions ( defaultColorCircle.setFill(JFXUtil.convert(defaultColor)); setColor(originalColor); + defaultButton.disableProperty().bind(Bindings.createBooleanBinding(() -> getColor().equals(defaultColor), colorProperty())); + okButton.disableProperty().bind(Bindings.createBooleanBinding(() -> getColor().equals(originalColor), colorProperty())); + ModelThreadPool.getExecutor().execute( ( ) -> { try @@ -542,6 +541,6 @@ protected void updateItem ( final NamedWidgetColor color, final boolean empty ) gc.fillRect(0, 0, SIZE, SIZE); } } - }; + } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java index 90f4357940..b685b56ce7 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java @@ -126,7 +126,7 @@ protected void updateItem(final String item, final boolean empty) } else severity = AlarmSeverity.NONE; - text.setStyle("-fx-text-fill: " + JFXUtil.webRGB(SeverityColors.getTextColor(severity))); + text.setStyle("-fx-text-fill: " + JFXUtil.webRGB(SeverityColors.getTextColor(severity)) + "; -fx-control-inner-background: " + JFXUtil.webRGB(SeverityColors.getBackgroundColor(severity))); } } @@ -210,7 +210,13 @@ private void exportToCSV(File file){ buffer.append("PVS (name, state, value, widget path)").append(System.lineSeparator()) .append(horizontalRuler).append(System.lineSeparator()); pvs.stream().sorted(Comparator.comparing(pv -> pv.name)).forEach(pv -> { - buffer.append(pv.name).append(itemSeparator).append(pv.state).append(itemSeparator).append(getPVValue(pv.value)).append(System.lineSeparator()); + buffer.append(pv.name).append(itemSeparator) + .append(pv.state) + .append(itemSeparator) + .append(getPVValue(pv.value)) + .append(itemSeparator) + .append(pv.path) + .append(System.lineSeparator()); }); buffer.append(System.lineSeparator()); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java index 499b0c7f61..3815762371 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java @@ -37,6 +37,7 @@ import javafx.application.Platform; import javafx.geometry.Dimension2D; +import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; @@ -50,6 +51,7 @@ import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; +import javafx.scene.text.TextAlignment; import javafx.scene.transform.Rotate; import javafx.scene.transform.Translate; @@ -96,6 +98,7 @@ public class ActionButtonRepresentation extends RegionBaseRepresentationIf not, we don't have to disable the button if the PV is readonly and/or disconnected */ private volatile boolean is_writePV = false; + private volatile boolean is_openDisplay = false; /** Optional modifier of the open display 'target */ private Optional target_modifier = Optional.empty(); @@ -105,6 +108,7 @@ public class ActionButtonRepresentation extends RegionBaseRepresentation enablementChangedListener = this::enablementChanged; + private volatile Pos pos; @Override protected boolean isFilteringEditModeClicks() @@ -136,7 +140,8 @@ private void checkModifiers(final MouseEvent event) } // 'control' ('command' on Mac OS X) - if (event.isShortcutDown()) + boolean middle_click = event.isMiddleButtonDown() && is_openDisplay && !is_writePV; + if (event.isShortcutDown() || middle_click) target_modifier = Optional.of(OpenDisplayActionInfo.Target.TAB); else if (event.isShiftDown()) target_modifier = Optional.of(OpenDisplayActionInfo.Target.WINDOW); @@ -169,6 +174,8 @@ private ButtonBase makeBaseButton() is_writePV = true; else has_non_writePVAction = true; + if (action instanceof OpenDisplayActionInfo) + is_openDisplay = true; } if (actions.isExecutedAsOne() || actions.getActions().size() < 2) @@ -348,7 +355,8 @@ protected void registerListeners() { updateColors(); super.registerListeners(); - + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); model_widget.propWidth().addUntypedPropertyListener(representationChangedListener); model_widget.propHeight().addUntypedPropertyListener(representationChangedListener); model_widget.propText().addUntypedPropertyListener(representationChangedListener); @@ -361,6 +369,8 @@ protected void registerListeners() model_widget.propBackgroundColor().addUntypedPropertyListener(buttonChangedListener); model_widget.propForegroundColor().addUntypedPropertyListener(buttonChangedListener); model_widget.propTransparent().addUntypedPropertyListener(buttonChangedListener); + model_widget.propHorizontalAlignment().addUntypedPropertyListener(buttonChangedListener); + model_widget.propVerticalAlignment().addUntypedPropertyListener(buttonChangedListener); model_widget.propActions().addUntypedPropertyListener(buttonChangedListener); if (! toolkit.isEditMode() && isLabelValue()) @@ -384,6 +394,8 @@ protected void unregisterListeners() model_widget.propBackgroundColor().removePropertyListener(buttonChangedListener); model_widget.propForegroundColor().removePropertyListener(buttonChangedListener); model_widget.propTransparent().removePropertyListener(buttonChangedListener); + model_widget.propHorizontalAlignment().removePropertyListener(buttonChangedListener); + model_widget.propVerticalAlignment().removePropertyListener(buttonChangedListener); model_widget.propActions().removePropertyListener(buttonChangedListener); super.unregisterListeners(); } @@ -399,6 +411,8 @@ protected void attachTooltip() /** Complete button needs to be updated */ private void buttonChanged(final WidgetProperty property, final Object old_value, final Object new_value) { + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); dirty_actionls.mark(); representationChanged(property, old_value, new_value); } @@ -496,6 +510,8 @@ public void updateChanges() was_ever_transformed = true; break; } + base.setAlignment(pos); + base.setTextAlignment(TextAlignment.values()[model_widget.propHorizontalAlignment().getValue().ordinal()]); } if (dirty_enablement.checkAndClear()) { diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BoolButtonRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BoolButtonRepresentation.java index 6458f04f4b..865b94b295 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BoolButtonRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BoolButtonRepresentation.java @@ -32,6 +32,7 @@ import org.phoebus.ui.javafx.Styles; import javafx.application.Platform; +import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.image.Image; @@ -43,6 +44,7 @@ import javafx.scene.paint.Paint; import javafx.scene.paint.Stop; import javafx.scene.shape.Ellipse; +import javafx.scene.text.TextAlignment; /** Creates JavaFX item for model widget * @author Megan Grodowitz @@ -89,6 +91,7 @@ public class BoolButtonRepresentation extends RegionBaseRepresentation valueChangedListener = this::valueChanged; private final WidgetPropertyListener modeChangeListener = this::modeChanged; private final WidgetPropertyListener confirmDialogWidgetPropertyListener = this::confirmationDialogChanged; + private volatile Pos pos; @Override public Pane createJFXNode() throws Exception @@ -185,6 +188,8 @@ protected void registerListeners() { super.registerListeners(); representationChanged(null,null,null); + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); model_widget.propWidth().addUntypedPropertyListener(representationChangedListener); model_widget.propHeight().addUntypedPropertyListener(representationChangedListener); model_widget.propOffLabel().addUntypedPropertyListener(representationChangedListener); @@ -197,6 +202,8 @@ protected void registerListeners() model_widget.propFont().addUntypedPropertyListener(representationChangedListener); model_widget.propForegroundColor().addUntypedPropertyListener(representationChangedListener); model_widget.propBackgroundColor().addUntypedPropertyListener(representationChangedListener); + model_widget.propHorizontalAlignment().addUntypedPropertyListener(representationChangedListener); + model_widget.propVerticalAlignment().addUntypedPropertyListener(representationChangedListener); model_widget.propEnabled().addPropertyListener(enablementChangedListener); model_widget.runtimePropPVWritable().addPropertyListener(enablementChangedListener); model_widget.propBit().addPropertyListener(bitChangedListener); @@ -226,6 +233,8 @@ protected void unregisterListeners() model_widget.propFont().removePropertyListener(representationChangedListener); model_widget.propForegroundColor().removePropertyListener(representationChangedListener); model_widget.propBackgroundColor().removePropertyListener(representationChangedListener); + model_widget.propHorizontalAlignment().removePropertyListener(representationChangedListener); + model_widget.propVerticalAlignment().removePropertyListener(representationChangedListener); model_widget.propEnabled().removePropertyListener(enablementChangedListener); model_widget.runtimePropPVWritable().removePropertyListener(enablementChangedListener); model_widget.propBit().removePropertyListener(bitChangedListener); @@ -338,6 +347,9 @@ private void representationChanged(final WidgetProperty property, final Objec value_label = state_labels[on_state]; computeBackground(); + + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); dirty_representation.mark(); toolkit.scheduleUpdate(this); @@ -418,6 +430,8 @@ public void updateChanges() else button.setGraphic(image); } + button.setAlignment(pos); + button.setTextAlignment(TextAlignment.values()[model_widget.propHorizontalAlignment().getValue().ordinal()]); } @Override diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ChoiceButtonRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ChoiceButtonRepresentation.java index 67601e2ef4..cc73220c03 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ChoiceButtonRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ChoiceButtonRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2021 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -42,13 +42,14 @@ import javafx.scene.layout.TilePane; import javafx.scene.paint.Color; import javafx.scene.text.Font; +import javafx.scene.text.TextAlignment; /** Creates JavaFX item for model widget * @author Kay Kasemir * @author Amanda Carpenter original RadioButton implementation */ @SuppressWarnings("nls") -public class ChoiceButtonRepresentation extends JFXBaseRepresentation +public class ChoiceButtonRepresentation extends RegionBaseRepresentation { private volatile boolean active = false; private final ToggleGroup toggle = new ToggleGroup(); @@ -60,6 +61,7 @@ public class ChoiceButtonRepresentation extends JFXBaseRepresentation > > itemsChangedListener = this::itemsChanged; private final UntypedWidgetPropertyListener sizeChangedListener = this::sizeChanged; private final UntypedWidgetPropertyListener styleChangedListener = this::styleChanged; + private volatile Pos pos; private volatile List items = Collections.emptyList(); private volatile int index = -1; @@ -88,6 +90,8 @@ private ButtonBase createButton(final String text) protected void registerListeners() { super.registerListeners(); + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); model_widget.propWidth().addUntypedPropertyListener(sizeChangedListener); model_widget.propHeight().addUntypedPropertyListener(sizeChangedListener); model_widget.propHorizontal().addUntypedPropertyListener(sizeChangedListener); @@ -95,6 +99,8 @@ protected void registerListeners() model_widget.propFont().addUntypedPropertyListener(styleChangedListener); model_widget.propForegroundColor().addUntypedPropertyListener(styleChangedListener); model_widget.propBackgroundColor().addUntypedPropertyListener(styleChangedListener); + model_widget.propHorizontalAlignment().addUntypedPropertyListener(styleChangedListener); + model_widget.propVerticalAlignment().addUntypedPropertyListener(styleChangedListener); model_widget.propEnabled().addUntypedPropertyListener(styleChangedListener); model_widget.runtimePropPVWritable().addUntypedPropertyListener(styleChangedListener); @@ -154,13 +160,19 @@ private void selectionChanged(final ObservableValue obs, final { final Object value; final VType pv_value = model_widget.runtimePropValue().getValue(); - if (pv_value instanceof VEnum || pv_value instanceof VNumber) - // PV uses enumerated or numeric type, so write the index + if (pv_value instanceof VNumber) + // PV uses numeric type, so write the index value = toggle.getToggles().indexOf(newval); - else // PV uses text + else + { // Use the text of selected option. + // For pv_value of type VEnum, this will attempt + // to match the correct enum option and provide the enum index + // (which may be different from the index within the toggles!) + // Otherwise fall back to writing the option as text. value = FormatOptionHandler.parse(pv_value, ((ButtonBase) newval).getText(), FormatOption.DEFAULT); + } logger.log(Level.FINE, "Writing " + value); Platform.runLater(() -> confirm(value)); } @@ -192,6 +204,8 @@ else if (!toolkit.showConfirmationDialog(model_widget, message)) private void styleChanged(final WidgetProperty property, final Object old_value, final Object new_value) { + pos = JFXUtil.computePos(model_widget.propHorizontalAlignment().getValue(), + model_widget.propVerticalAlignment().getValue()); dirty_style.mark(); toolkit.scheduleUpdate(this); } @@ -220,10 +234,10 @@ private List computeItems(final VType value, final boolean fromPV) private int determineIndex(final List labels, final VType value) { - if (value instanceof VEnum) - return ((VEnum)value).getIndex(); if (value instanceof VNumber) return ((VNumber)value).getValue().intValue(); + // For VEnum, PV labels may differ from choice button options, + // so locate toggle by text, not PV's enum index return labels.indexOf(VTypeUtil.getValueString(value, false)); } @@ -293,9 +307,14 @@ public void updateChanges() sizeButtons(); // Select one of the buttons - toggle.selectToggle(save_index < 0 || save_index >= buttons.size() - ? null - : (Toggle) buttons.get(save_index)); + final Toggle selected = save_index < 0 || save_index >= buttons.size() + ? null + : (Toggle) buttons.get(save_index); + toggle.selectToggle(selected); + // If current value does not match any of the button options, + // indicate just like disconnected. + // If option matches, we do have a valid value and are thus connected + model_widget.runtimePropConnected().setValue(selected != null); } finally { @@ -320,6 +339,8 @@ public void updateChanges() final ButtonBase b = (ButtonBase) node; b.setTextFill(fg); b.setFont(font); + b.setAlignment(pos); + b.setTextAlignment(TextAlignment.values()[model_widget.propHorizontalAlignment().getValue().ordinal()]); if (((Toggle)b).isSelected()) b.setStyle(selected); else diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ComboRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ComboRepresentation.java index 522db2b9c6..4897df72cd 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ComboRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ComboRepresentation.java @@ -266,7 +266,7 @@ public void updateChanges() Font f = JFXUtil.convert(model_widget.propFont().getValue()); jfx_node.setStyle(MessageFormat.format( - "-fx-body-color: linear-gradient(to bottom,ladder({0}, derive({0},8%) 75%, derive({0},10%) 80%), derive({0},-8%)); " + "-fx-background-color: linear-gradient(to bottom,ladder({0}, derive({0},8%) 75%, derive({0},10%) 80%), derive({0},-8%)); " + "-fx-text-base-color: ladder(-fx-color, -fx-light-text-color 45%, -fx-dark-text-color 46%, -fx-dark-text-color 59%, {1}); " + "-fx-font: {2} {3}px \"{4}\";", JFXUtil.webRgbOrHex(model_widget.propBackgroundColor().getValue()), diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/EmbeddedDisplayRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/EmbeddedDisplayRepresentation.java index 2e63311964..e6b032542d 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/EmbeddedDisplayRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/EmbeddedDisplayRepresentation.java @@ -40,6 +40,7 @@ import javafx.scene.paint.Stop; import javafx.scene.shape.Rectangle; import javafx.scene.transform.Scale; +import org.phoebus.ui.javafx.NonCachingScrollPane; /** Creates JavaFX item for model widget * @@ -143,7 +144,7 @@ public Pane createJFXNode() throws Exception inner = new Pane(); inner.getTransforms().add(zoom = new Scale()); - scroll = new ScrollPane(inner); + scroll = new NonCachingScrollPane(inner); scroll.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); scroll.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); // By default it seems that the minimum size is set to 36x36. diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java index f70738f518..8b89c85d41 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/GroupRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2018 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -42,12 +42,12 @@ public class GroupRepresentation extends JFXBaseRepresentation property, final Object old_va private void computeColors() { + line_color = JFXUtil.convert(model_widget.propLineColor().getValue()); foreground_color = JFXUtil.convert(model_widget.propForegroundColor().getValue()); background_color = JFXUtil.convert(model_widget.propBackgroundColor().getValue()); } @@ -148,8 +151,13 @@ public void updateChanges() // Reset position and size as if style == Style.NONE. int[] insets = new int[4]; - - System.arraycopy(model_widget.runtimePropInsets().getValue(), 0, insets, 0, insets.length); + // If the widget was deleted and then via 'undo' restored, + // this could be a first update while the model item + // contains the old 'inset' data. + // ==> Ignore 'inset' on first update, + // From then on, use the 'insets' which we compute and store below + if (! firstUpdate) + System.arraycopy(model_widget.runtimePropInsets().getValue(), 0, insets, 0, insets.length); final boolean hasChildren = !model_widget.runtimeChildren().getValue().isEmpty(); if (hasChildren) @@ -174,7 +182,7 @@ public void updateChanges() // In edit mode, show outline because otherwise hard to // handle the totally invisible group if (toolkit.isEditMode()) - jfx_node.setBorder(new Border(new BorderStroke(foreground_color, EDIT_NONE_DASHED, CornerRadii.EMPTY, EDIT_NONE_BORDER))); + jfx_node.setBorder(new Border(new BorderStroke(line_color, EDIT_NONE_DASHED, CornerRadii.EMPTY, EDIT_NONE_BORDER))); else jfx_node.setBorder(null); @@ -189,7 +197,7 @@ public void updateChanges() insets[2] = 2 * insets[0]; insets[3] = 2 * insets[1]; - jfx_node.setBorder(new Border(new BorderStroke(foreground_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); + jfx_node.setBorder(new Border(new BorderStroke(line_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); label.setVisible(false); break; } @@ -201,13 +209,13 @@ public void updateChanges() insets[2] = 2 * insets[0]; insets[3] = insets[0] + insets[1]; - jfx_node.setBorder(new Border(new BorderStroke(foreground_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); + jfx_node.setBorder(new Border(new BorderStroke(line_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); label.setVisible(true); label.relocate(0, BORDER_WIDTH); label.setPadding(TITLE_PADDING); label.setPrefSize(width + ( ( !firstUpdate && hasChildren ) ? insets[2] : 0 ), inset); - label.setTextFill(background_color); - label.setBackground(new Background(new BackgroundFill(foreground_color, CornerRadii.EMPTY, Insets.EMPTY))); + label.setTextFill(foreground_color); + label.setBackground(new Background(new BackgroundFill(line_color, CornerRadii.EMPTY, Insets.EMPTY))); break; } case GROUP: @@ -219,7 +227,7 @@ public void updateChanges() insets[2] = 2 * insets[0]; insets[3] = 2 * insets[1]; - jfx_node.setBorder(new Border(new BorderStroke(foreground_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT, new Insets(inset / 2)))); + jfx_node.setBorder(new Border(new BorderStroke(line_color, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT, new Insets(inset / 2)))); label.setVisible(true); label.relocate(inset, 0); label.setPadding(TITLE_PADDING); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabs.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabs.java index dd6b559600..7af2b89f6d 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabs.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabs.java @@ -26,6 +26,7 @@ import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; +import org.phoebus.ui.javafx.NonCachingScrollPane; /** Navigation Tabs * @@ -95,7 +96,7 @@ public static interface Listener public NavigationTabs() { // Scroll pane in case body exceeds size of this, the other BorderPane - final ScrollPane scroll = new ScrollPane(body); + final ScrollPane scroll = new NonCachingScrollPane(body); scroll.getStyleClass().add("navtab_scroll"); // Inner border pane to auto-resize 'body' and add border + padding via style sheet diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabsRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabsRepresentation.java index e5dab742ed..19144a0f6d 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabsRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/NavigationTabsRepresentation.java @@ -12,14 +12,18 @@ import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; +import javafx.util.Pair; +import org.apache.commons.lang3.tuple.MutablePair; import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; +import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.widgets.NavigationTabsWidget; @@ -54,6 +58,13 @@ public class NavigationTabsRepresentation extends RegionBaseRepresentation, HashMap>> { + public SelectedNavigationTabs(int activeTab) { + left = activeTab; + right = new HashMap<>(); // 'right' is a mapping of the form: Tab Number & Tab Name (Integer, String) -> Name of Navigator Tab Widget (String) -> Opened Tab and Sub-Tabs (SelectedNavigationTabs) + } + }; + protected SelectedNavigationTabs selectedNavigationTabs = new SelectedNavigationTabs(0); private final UntypedWidgetPropertyListener sizesChangedListener = this::sizesChanged; private final UntypedWidgetPropertyListener tabLookChangedListener = this::tabLookChanged; private final WidgetPropertyListener activeTabChangedListener = this::activeTabChanged; @@ -176,6 +187,7 @@ private void activeTabChanged(final WidgetProperty property, final Inte dirty_active_tab.mark(); toolkit.scheduleUpdate(this); tab_display_listener.propertyChanged(null, null, null); + selectedNavigationTabs.left = tab_index; } /** Update to the next pending display @@ -231,6 +243,39 @@ private synchronized void updatePendingDisplay(final JobMonitor monitor) return null; }); checkCompletion(model_widget, completion, "timeout representing new content"); + + int tabNumber = model_widget.propActiveTab().getValue(); + String tabName = model_widget.propTabs().getValue().get(model_widget.propActiveTab().getValue()).name().getValue(); + Pair tabNumberAndTabName = new Pair<>(tabNumber, tabName); + + if (!selectedNavigationTabs.right.containsKey(tabNumberAndTabName)) { + selectedNavigationTabs.right.put(tabNumberAndTabName, new HashMap<>()); + } + HashMap selectedNavigationTabsHashMapForCurrentTab = selectedNavigationTabs.right.get(tabNumberAndTabName); + + new_model.getChildren() + .stream() + .filter(widget -> widget instanceof NavigationTabsWidget) + .forEach(widget -> { + NavigationTabsWidget nestedNavigationTabsWidget = (NavigationTabsWidget) widget; + NavigationTabsRepresentation nestedNavigationTabsRepresentation = (NavigationTabsRepresentation) nestedNavigationTabsWidget.getUserData(Widget.USER_DATA_REPRESENTATION); + if (nestedNavigationTabsRepresentation != null) { + SelectedNavigationTabs nestedNavigationTabsRepresentation_selectedNavigationTabs; + + if (!selectedNavigationTabsHashMapForCurrentTab.containsKey(nestedNavigationTabsWidget.getName())) { + nestedNavigationTabsRepresentation_selectedNavigationTabs = new SelectedNavigationTabs(nestedNavigationTabsWidget.propActiveTab().getValue()); + selectedNavigationTabsHashMapForCurrentTab.put(nestedNavigationTabsWidget.getName(), nestedNavigationTabsRepresentation_selectedNavigationTabs); + } + else { + nestedNavigationTabsRepresentation_selectedNavigationTabs = selectedNavigationTabsHashMapForCurrentTab.get(nestedNavigationTabsWidget.getName()); + if (nestedNavigationTabsWidget.propTabs().size() > nestedNavigationTabsRepresentation_selectedNavigationTabs.left) { + nestedNavigationTabsWidget.propActiveTab().setValue(nestedNavigationTabsRepresentation_selectedNavigationTabs.left); + } + } + nestedNavigationTabsRepresentation.selectedNavigationTabs = nestedNavigationTabsRepresentation_selectedNavigationTabs; + } + }); + model_widget.runtimePropEmbeddedModel().setValue(new_model); } catch (Exception ex) diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/PictureRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/PictureRepresentation.java index 8f99820813..e0ae845d98 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/PictureRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/PictureRepresentation.java @@ -11,6 +11,8 @@ import java.util.logging.Level; +import javafx.scene.CacheHint; +import javafx.scene.Node; import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; @@ -21,6 +23,7 @@ import org.csstudio.display.builder.model.util.ModelThreadPool; import org.csstudio.display.builder.model.widgets.PictureWidget; import org.csstudio.display.builder.representation.javafx.SVGHelper; +import org.phoebus.ui.Preferences; import org.phoebus.ui.javafx.ImageCache; import javafx.scene.image.Image; @@ -51,11 +54,34 @@ public class PictureRepresentation extends JFXBaseRepresentation orientationChangedListener = this::orientationChanged; private final UntypedWidgetPropertyListener valueChangedListener = this::valueChanged; + private final UntypedWidgetPropertyListener propretyChangedListener = this::propertyChanged; + private volatile double percentage = 0.0; @Override @@ -49,11 +51,12 @@ protected void registerListeners() { super.registerListeners(); model_widget.propFillColor().addUntypedPropertyListener(lookChangedListener); + model_widget.propBackgroundColor().addUntypedPropertyListener(lookChangedListener); model_widget.propWidth().addUntypedPropertyListener(lookChangedListener); model_widget.propHeight().addUntypedPropertyListener(lookChangedListener); model_widget.propLimitsFromPV().addUntypedPropertyListener(valueChangedListener); - model_widget.propMinimum().addUntypedPropertyListener(valueChangedListener); - model_widget.propMaximum().addUntypedPropertyListener(valueChangedListener); + model_widget.propMinimum().addUntypedPropertyListener(propretyChangedListener); + model_widget.propMaximum().addUntypedPropertyListener(propretyChangedListener); model_widget.propLogScale().addUntypedPropertyListener(valueChangedListener); model_widget.runtimePropValue().addUntypedPropertyListener(valueChangedListener); model_widget.propHorizontal().addPropertyListener(orientationChangedListener); @@ -64,11 +67,12 @@ protected void registerListeners() protected void unregisterListeners() { model_widget.propFillColor().removePropertyListener(lookChangedListener); + model_widget.propBackgroundColor().removePropertyListener(lookChangedListener); model_widget.propWidth().removePropertyListener(lookChangedListener); model_widget.propHeight().removePropertyListener(lookChangedListener); model_widget.propLimitsFromPV().removePropertyListener(valueChangedListener); - model_widget.propMinimum().removePropertyListener(valueChangedListener); - model_widget.propMaximum().removePropertyListener(valueChangedListener); + model_widget.propMinimum().removePropertyListener(propretyChangedListener); + model_widget.propMaximum().removePropertyListener(propretyChangedListener); model_widget.propLogScale().removePropertyListener(valueChangedListener); model_widget.runtimePropValue().removePropertyListener(valueChangedListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); @@ -92,6 +96,12 @@ private void orientationChanged(final WidgetProperty prop, final Boolea lookChanged(prop, old, horizontal); } + private void propertyChanged(final WidgetProperty property, final Object old_value, final Object new_value) + { + lookChanged(property, old_value, new_value ); + valueChanged(property, old_value, new_value ); + } + private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) { dirty_look.mark(); @@ -103,8 +113,22 @@ private void valueChanged(final WidgetProperty property, final Object old_val final VType vtype = model_widget.runtimePropValue().getValue(); final boolean limits_from_pv = model_widget.propLimitsFromPV().getValue(); - double min_val = model_widget.propMinimum().getValue(); - double max_val = model_widget.propMaximum().getValue(); + + double min_val = 0; + double max_val = 0; + + // Inverted if low limit and higher than high limit + if (model_widget.propMaximum().getValue() > model_widget.propMinimum().getValue()) + { + min_val = model_widget.propMinimum().getValue(); + max_val = model_widget.propMaximum().getValue(); + } + else + { + max_val = model_widget.propMinimum().getValue(); + min_val = model_widget.propMaximum().getValue(); + } + if (limits_from_pv) { // Try display range from PV @@ -116,7 +140,7 @@ private void valueChanged(final WidgetProperty property, final Object old_val } } // Fall back to 0..100 range - if (min_val >= max_val) + if (min_val == max_val) { min_val = 0.0; max_val = 100.0; @@ -136,8 +160,8 @@ private void valueChanged(final WidgetProperty property, final Object old_val } else percentage = (value - min_val) / (max_val - min_val); - - // Limit to 0.0 .. 1.0 + + // Limit to 0.0 .. 1.0 if (percentage < 0.0 || !Double.isFinite(percentage)) this.percentage = 0.0; else if (percentage > 1.0) @@ -148,6 +172,7 @@ else if (percentage > 1.0) toolkit.scheduleUpdate(this); } + @Override public void updateChanges() { @@ -157,17 +182,37 @@ public void updateChanges() boolean horizontal = model_widget.propHorizontal().getValue(); double width = model_widget.propWidth().getValue(); double height = model_widget.propHeight().getValue(); + double min_val = model_widget.propMinimum().getValue(); + double max_val = model_widget.propMaximum().getValue(); + if (!horizontal) { jfx_node.getTransforms().setAll( - new Translate(0, height), - new Rotate(-90, 0, 0)); + new Translate(0, height), + new Rotate(-90, 0, 0)); jfx_node.setPrefSize(height, width); + + if (min_val > max_val) + { + jfx_node.getTransforms().setAll( + new Translate(0, height), + new Rotate(-90, 0, 0, 0), + new Translate(height, 0), + new Rotate(180, 0, 0, 0, Rotate.Y_AXIS)); + jfx_node.setPrefSize(height, width); + } } else { jfx_node.getTransforms().clear(); jfx_node.setPrefSize(width, height); + + if (min_val > max_val) + { + jfx_node.getTransforms().setAll( + new Translate(width, 0), + new Rotate(180, 0, 0, 0, Rotate.Y_AXIS)); + } } // Default 'inset' of .bar uses 7 pixels. @@ -181,8 +226,36 @@ public void updateChanges() // Tweaking the color used by CSS keeps overall style. // See also http://stackoverflow.com/questions/13467259/javafx-how-to-change-progressbar-color-dynamically final StringBuilder style = new StringBuilder(); - style.append("-fx-accent: ").append(JFXUtil.webRgbOrHex(model_widget.propFillColor().getValue())).append(';'); - style.append("-fx-control-inner-background: ").append(JFXUtil.webRgbOrHex(model_widget.propBackgroundColor().getValue())).append(';'); + + // Color of the progress bar / foreground + style.append("-fx-accent: ").append(JFXUtil.webRGB( + JFXUtil.convert( + model_widget.propFillColor().getValue() + ) + )).append(" !important; "); + + // Color of the background underneath the progress bar + // Note per moderna.css the background is actually three layers of color + // with fx-shadow-highlight-color on the bottom, + // then fx-text-box-border, + // and finally fx-control-inner-background on top, all stacked in place with offsets. + // This gives the illusion of having a bordered box with a shadow instead of actually being a + // bordered box with a shadow... + // Fortunately, the bottom-most color (the 'shadow') is already transparent so we can leave it alone + // Unfortunately, the middle color (the "border" color) is a solid gray color (#ececec), so we must + // override it with its rgba equivalent so that it has transparency matching the picked background color. + style.append("-fx-control-inner-background: ") + .append(JFXUtil.webRGB( + JFXUtil.convert( + model_widget.propBackgroundColor().getValue())) + ) + .append(";"); + style.append("-fx-text-box-border: rgba(236, 236, 236, ") + .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) + .append(");"); + style.append("-fx-shadow-highlight-color: rgba(236, 236, 236, ") + .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) + .append(");"); jfx_node.setStyle(style.toString()); } if (dirty_value.checkAndClear()) diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SpinnerRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SpinnerRepresentation.java index f5e6efc435..2b3b37ac49 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SpinnerRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SpinnerRepresentation.java @@ -95,10 +95,12 @@ else if(focused){ setActive(false); } break; - case ENTER: + case ENTER: // ENTER event is triggered twice by native JavaFX Spinner // Submit value, leave active state - submit(); - setActive(false); + if(active){ + submit(); + setActive(false); + } break; //incrementing by keyboard case UP: diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SymbolRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SymbolRepresentation.java index 6ddb6c8151..2dc7965da8 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SymbolRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/SymbolRepresentation.java @@ -23,6 +23,12 @@ import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.scene.effect.ColorAdjust; +import javafx.scene.effect.DropShadow; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import org.csstudio.display.builder.model.ArrayWidgetProperty; import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.DisplayModel; @@ -413,6 +419,8 @@ public void updateChanges ( ) { @Override protected StackPane createJFXNode ( ) throws Exception { + PictureRepresentation.setCacheHintAccordingToPreferences(imageView); + autoSize = model_widget.propAutoSize().getValue(); symbol = new Symbol(); //getDefaultSymbol(); @@ -457,10 +465,125 @@ protected StackPane createJFXNode ( ) throws Exception { symbolChanged(null, null, null); + if (!toolkit.isEditMode() && model_widget.propEnabled().getValue() && model_widget.propRunActionsOnMouseClick().getValue()) { + enableRunActionsOnMouseClick(); + } + return symbolPane; } + private void enableRunActionsOnMouseClick() { + imageView.focusTraversableProperty().set(true); + imageView.setStyle("-fx-cursor: hand;"); + + ColorAdjust[] clickEffect = { null }; // Values are wrapped in arrays as a workaround of the fact that Java doesn't allow non-final variables to be captured by closures. + DropShadow[] focusEffect = { null }; + Runnable setEffect = () -> { + if (focusEffect[0] != null) { + focusEffect[0].setInput(clickEffect[0]); + imageView.setEffect(focusEffect[0]); + } + else { + imageView.setEffect(clickEffect[0]); + } + }; + + Runnable runActions = () -> { + model_widget.propActions().getValue().getActions().forEach(actionInfo -> toolkit.fireAction(model_widget, actionInfo)); + }; + + ColorAdjust increaseBrightness = new ColorAdjust(0, 0, 0.3, 0); + ColorAdjust decreaseBrightness = new ColorAdjust(0, 0, -0.3, 0); + + boolean[] buttonIsClicked = { false }; // Value is wrapped in an array as a workaround of the fact that Java doesn't allow non-final variables to be captured by closures. + + imageView.addEventFilter(MouseEvent.MOUSE_ENTERED, mouseEvent -> { + if (!buttonIsClicked[0]) { + clickEffect[0] = increaseBrightness; + } + else { + clickEffect[0] = decreaseBrightness; + } + setEffect.run(); + mouseEvent.consume(); + }); + + imageView.addEventFilter(MouseEvent.MOUSE_EXITED, mouseEvent -> { + clickEffect[0] = null; + setEffect.run(); + }); + + imageView.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseEvent -> { + if (mouseEvent.isPrimaryButtonDown()) { + buttonIsClicked[0] = true; + clickEffect[0] = decreaseBrightness; + mouseEvent.consume(); + } + + setEffect.run(); + }); + + imageView.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseEvent -> { + buttonIsClicked[0] = false; + }); + + imageView.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseEvent -> { + if (mouseEvent.getButton() == MouseButton.PRIMARY) { + runActions.run(); + clickEffect[0] = null; + setEffect.run(); + mouseEvent.consume(); + } + + imageView.requestFocus(); + }); + + Color focusColor = Color.web("rgba(3,158,211,1)"); + DropShadow dropShadow = new DropShadow(5, focusColor); + imageView.focusedProperty().addListener((observable, old_value, new_value) -> { + if (new_value) { + focusEffect[0] = dropShadow; + setEffect.run(); + } + else { + focusEffect[0] = null; + setEffect.run(); + } + }); + + imageView.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER || keyEvent.getCode() == KeyCode.SPACE) { + clickEffect[0] = decreaseBrightness; + setEffect.run(); + + buttonIsClicked[0] = true; + + keyEvent.consume(); + } + else if (keyEvent.getCode() == KeyCode.TAB) { + clickEffect[0] = null; + focusEffect[0] = null; + setEffect.run(); + + buttonIsClicked[0] = false; + } + }); + + imageView.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER || keyEvent.getCode() == KeyCode.SPACE) { + runActions.run(); + + clickEffect[0] = null; + setEffect.run(); + + buttonIsClicked[0] = false; + + keyEvent.consume(); + } + }); + } + @Override protected void registerListeners() { diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TableRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TableRepresentation.java index f5bcef5e2f..fe5e294139 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TableRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TableRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2018 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -361,9 +361,12 @@ public void updateChanges() } } } - if (dirty_data.checkAndClear()) + if (dirty_data.checkAndClear()) // Show data with current coloring + { jfx_node.setData(data); - if (dirty_cell_colors.checkAndClear()) + jfx_node.setCellColors(cell_colors); + } + if (dirty_cell_colors.checkAndClear()) // Keep data, only update colors jfx_node.setCellColors(cell_colors); if (dirty_set_selection.checkAndClear()) jfx_node.setSelection(model_widget.runtimePropSetSelection().getValue()); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java index 783a3b7246..418b1957ce 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2018 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -61,7 +61,7 @@ protected void registerListeners() model_widget.propLimitsFromPV().addUntypedPropertyListener(valueListener); model_widget.propMinimum().addUntypedPropertyListener(valueListener); model_widget.propMaximum().addUntypedPropertyListener(valueListener); - model_widget.propLogScale().addUntypedPropertyListener(valueListener); + model_widget.propLogScale().addUntypedPropertyListener(valueListener); model_widget.runtimePropValue().addUntypedPropertyListener(valueListener); model_widget.propHorizontal().addPropertyListener(orientationChangedListener); valueChanged(null, null, null); @@ -82,7 +82,7 @@ protected void unregisterListeners() model_widget.propLimitsFromPV().removePropertyListener(valueListener); model_widget.propMinimum().removePropertyListener(valueListener); model_widget.propMaximum().removePropertyListener(valueListener); - model_widget.propLogScale().removePropertyListener(valueListener); + model_widget.propLogScale().removePropertyListener(valueListener); model_widget.runtimePropValue().removePropertyListener(valueListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); super.unregisterListeners(); @@ -125,7 +125,8 @@ private void valueChanged(final WidgetProperty property, final Object old_val private void orientationChanged(final WidgetProperty prop, final Boolean old, final Boolean horizontal) { - if (toolkit.isEditMode()) { + if (toolkit.isEditMode()) + { // Swap width <-> height so widget basically rotates final int w = model_widget.propWidth().getValue(); final int h = model_widget.propHeight().getValue(); model_widget.propWidth().setValue(h); @@ -134,6 +135,9 @@ private void orientationChanged(final WidgetProperty prop, final Boolea lookChanged(prop, old, horizontal); } + /** Track if we ever set transformations because just 'clearing' would otherwise allocate them */ + private boolean was_transformed = false; + @Override public void updateChanges() { @@ -142,23 +146,22 @@ public void updateChanges() { double width = model_widget.propWidth().getValue(); double height = model_widget.propHeight().getValue(); - boolean horizontal = model_widget.propHorizontal().getValue(); - if (horizontal) + if (model_widget.propHorizontal().getValue()) { - jfx_node.getTransforms().setAll( - new Translate(width, 0), - new Rotate(90, 0, 0)); - jfx_node.setPrefSize(height, width); + tank.getTransforms().setAll(new Translate(width, 0), + new Rotate(90, 0, 0)); + was_transformed = true; tank.setWidth(height); tank.setHeight(width); } else { - jfx_node.getTransforms().clear(); - jfx_node.setPrefSize(width, height); + if (was_transformed) + tank.getTransforms().clear(); tank.setWidth(width); tank.setHeight(height); } + jfx_node.setPrefSize(width, height); tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); tank.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TemplateInstanceRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TemplateInstanceRepresentation.java index c4b6729d28..a040068390 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TemplateInstanceRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TemplateInstanceRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021-2022 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -127,7 +127,6 @@ protected void registerListeners() model_widget.propFile().addPropertyListener(fileChangedListener); // Initial updates - instancesChanged(model_widget.propInstances(), null, model_widget.propInstances().getValue()); fileChanged(null, null, null); } @@ -351,6 +350,7 @@ private void instantiateTemplateAndRepresent() { final DisplayModel inst = ModelReader.parseXML(template_xml); final GroupWidget wrapper = new GroupWidget(); + wrapper.propName().setValue("Instance " + i); wrapper.propStyle().setValue(Style.NONE); wrapper.propX().setValue(x); wrapper.propY().setValue(y); @@ -385,6 +385,9 @@ private void instantiateTemplateAndRepresent() } } + // Expand macros, which includes the 'groups' created for the 'instances' + new_model.expandMacros(model_widget.getEffectiveMacros()); + // Stop (old) runtime // TemplateInstanceRuntime tracks this property to start/stop the embedded model's runtime model_widget.runtimePropEmbeddedModel().setValue(null); diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TooltipSupport.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TooltipSupport.java index b77ff6039a..c40f741593 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TooltipSupport.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TooltipSupport.java @@ -20,9 +20,12 @@ import org.csstudio.display.builder.model.properties.StringWidgetProperty; import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.javafx.JFXPreferences; +import org.csstudio.display.builder.representation.javafx.Messages; +import org.epics.util.stats.Range; import org.epics.vtype.Alarm; import org.epics.vtype.AlarmSeverity; import org.epics.vtype.Display; +import org.epics.vtype.DisplayProvider; import org.epics.vtype.Time; import org.phoebus.framework.macros.MacroHandler; import org.phoebus.framework.macros.MacroValueProvider; @@ -93,6 +96,32 @@ public static void attach(final Node node, final WidgetProperty tooltip_ { String spec = ((MacroizedWidgetProperty)tooltip_property).getSpecification(); + final Widget widget = tooltip_property.getWidget(); + Object vtype = null; + if (widget.checkProperty(runtimePropPVValue.getName()).isPresent()) { + vtype = widget.getPropertyValue(runtimePropPVValue); + } + Display display = Display.displayOf(vtype); + + // If 'vtype' supports it (i.e., it is an instance of "DisplayProvider"), + // append the alarm limits to $(pv_value): + if (vtype instanceof DisplayProvider) { + + Range alarmRange = display.getAlarmRange(); + double lolo = alarmRange.getMinimum(); + double hihi = alarmRange.getMaximum(); + + Range warningRange = display.getWarningRange(); + double low = warningRange.getMinimum(); + double high = warningRange.getMaximum(); + + String pv_alarm_limits = Messages.HIHI + ": " + (Double.isNaN(hihi) ? Messages.NotSet : hihi) + System.lineSeparator() + + Messages.HIGH + ": " + (Double.isNaN(high) ? Messages.NotSet : high) + System.lineSeparator() + + Messages.LOW + ": " + (Double.isNaN(low) ? Messages.NotSet : low) + System.lineSeparator() + + Messages.LOLO + ": " + (Double.isNaN(lolo) ? Messages.NotSet : lolo); + spec = spec.replace("$(pv_value)", "$(pv_value)" + System.lineSeparator() + pv_alarm_limits); + } + // Use custom supplier for $(pv_value)? // Otherwise replace like other macros, i.e. use toString of the property if (pv_value != null) @@ -100,7 +129,6 @@ public static void attach(final Node node, final WidgetProperty tooltip_ final StringBuilder buf = new StringBuilder(); buf.append(pv_value.get()); - final Object vtype = tooltip_property.getWidget().getPropertyValue(runtimePropPVValue); final Alarm alarm = Alarm.alarmOf(vtype); if (alarm != null && alarm.getSeverity() != AlarmSeverity.NONE) buf.append(", ").append(alarm.getSeverity()).append(" - ").append(alarm.getName()); @@ -109,14 +137,14 @@ public static void attach(final Node node, final WidgetProperty tooltip_ if (time != null) buf.append(", ").append(TimestampFormats.FULL_FORMAT.format(time.getTimestamp())); - String display = Display.displayOf(vtype).getDescription(); + String displayDescription = display.getDescription(); // Description is non-null only for pva. - if(display != null && !display.isEmpty()){ - buf.append(System.lineSeparator()).append(display); + if(displayDescription != null && !displayDescription.isEmpty()){ + buf.append(System.lineSeparator()).append(displayDescription); } spec = spec.replace("$(pv_value)", buf.toString()); } - final Widget widget = tooltip_property.getWidget(); + final MacroValueProvider macros = widget.getMacrosOrProperties(); String expanded; try diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/plots/StripchartRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/plots/StripchartRepresentation.java index a838c5e6f4..0136db93d1 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/plots/StripchartRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/plots/StripchartRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,7 +19,6 @@ import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.widgets.plots.StripchartWidget; -import org.csstudio.display.builder.model.widgets.plots.StripchartWidget.AxisWidgetProperty; import org.csstudio.display.builder.model.widgets.plots.StripchartWidget.TraceWidgetProperty; import org.csstudio.display.builder.representation.javafx.JFXUtil; import org.csstudio.display.builder.representation.javafx.widgets.RegionBaseRepresentation; @@ -201,7 +200,6 @@ private void ignoreAxisChanges(final StripchartWidget.YAxisWidgetProperty axis) axis.grid().removePropertyListener(modelChangedListener); axis.visible().removePropertyListener(modelChangedListener); axis.color().removePropertyListener(modelChangedListener); - } @@ -350,9 +348,6 @@ private void updateModel() for (TraceWidgetProperty trace : model_widget.propTraces().getValue()) { - if (! trace.traceVisible().getValue()) - continue; - final PVItem item = new PVItem(trace.traceYPV().getValue(), 0.0); item.setDisplayName(trace.traceName().getValue()); item.setAxis(model.getAxis(trace.traceYAxis().getValue())); @@ -361,6 +356,7 @@ private void updateModel() item.setLineWidth(trace.traceWidth().getValue()); item.setPointType(XYPlotRepresentation.map(trace.tracePointType().getValue())); item.setPointSize(trace.tracePointSize().getValue()); + item.setVisible(trace.traceVisible().getValue()); item.useDefaultArchiveDataSources(); try { diff --git a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties index ba5f266eaa..05c6b429d1 100644 --- a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties +++ b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties @@ -60,6 +60,10 @@ FontDialog_Preview=Preview: FontDialog_Size=Size: FontDialog_Style=Style: Green=Green +HIGH=HIGH +HIHI=HIHI +LOLO=LOLO +LOW=LOW MacrosDialog_Info=Enter macros MacrosDialog_NameCol=Macro Name MacrosDialog_Title=Macros @@ -69,6 +73,7 @@ MacrosTable_ToolTip=Edit names or values. Add new macro in last row MacrosTable_ValueHint= MoveDown=Down MoveUp=Up +NotSet=Not set OpenInExternalEditor=Open in external editor Password=Password Password_Caption=Password: diff --git a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/opibuilder.css b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/opibuilder.css index 2a21f458cb..e6be6b0196 100644 --- a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/opibuilder.css +++ b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/opibuilder.css @@ -237,14 +237,15 @@ { -db-toggle-switch-off: #f5f5f5; -db-toggle-switch-on: #0b99c9; + -db-transparent-text-box-border: rgba(236, 236, 236, 0.50); } .toggle-switch .thumb-area { -fx-background-radius: 1em; -fx-background-color: linear-gradient( to bottom, - derive(-fx-text-box-border, -20%), - derive(-fx-text-box-border, -30%) + derive(-db-transparent-text-box-border, -20%), + derive(-db-transparent-text-box-border, -30%) ), -db-toggle-switch-off; -fx-background-insets: 0, 1; @@ -254,8 +255,8 @@ { -fx-background-color: linear-gradient( to bottom, - derive(-fx-text-box-border, -20%), - derive(-fx-text-box-border, -30%) + derive(-db-transparent-text-box-border, -20%), + derive(-db-transparent-text-box-border, -30%) ), linear-gradient( to bottom, diff --git a/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/JFXUtilTest.java b/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/JFXUtilTest.java index b0e7320abe..5103516acf 100644 --- a/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/JFXUtilTest.java +++ b/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/JFXUtilTest.java @@ -38,7 +38,16 @@ public void testRGB() { assertThat(JFXUtil.webRgbOrHex(new WidgetColor(15, 255, 0)), equalTo("#0FFF00")); assertThat(JFXUtil.webRgbOrHex(new WidgetColor(0, 16, 255)), equalTo("#0010FF")); - assertThat(JFXUtil.webRgbOrHex(new WidgetColor(0, 16, 255, 50)), equalTo("rgba(0,16,255,0.19607843)")); + assertThat(JFXUtil.webRgbOrHex(new WidgetColor(0, 16, 255, 50)), equalTo("rgba(0,16,255,0.20)")); + } + + @Test + public void testRGBWithAlpha() + { + // NOTE that the actual decimal value for transparency would have been 0.019607844, however it needs + // to be formatted to two decimal places + assertThat(JFXUtil.webRgbOrHex(new WidgetColor(15, 255, 0, 5)), equalTo("rgba(15,255,0,0.02)")); + assertThat(JFXUtil.webRgbOrHex(new WidgetColor(0, 16, 255)), equalTo("#0010FF")); } @Test diff --git a/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/MacrosDialogDemo.java b/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/MacrosDialogDemo.java index 2e83a8726c..3feae5b301 100644 --- a/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/MacrosDialogDemo.java +++ b/app/display/representation-javafx/src/test/java/org/csstudio/display/builder/representation/javafx/MacrosDialogDemo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2016 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -29,6 +29,8 @@ public void start(final Stage stage) final Macros macros = new Macros(); macros.add("S", "Test"); macros.add("N", "17"); + macros.add("ANOTHER", "$(PASSED_IN)"); + macros.add("ONEMORE", "$(HOME)"); final MacrosDialog dialog = new MacrosDialog(macros, null); System.out.println(dialog.showAndWait()); } diff --git a/app/display/representation/.classpath b/app/display/representation/.classpath index c352446e29..b00bd27ae9 100644 --- a/app/display/representation/.classpath +++ b/app/display/representation/.classpath @@ -7,6 +7,6 @@ - + diff --git a/app/display/representation/pom.xml b/app/display/representation/pom.xml index 0205892a98..4aadcc373b 100644 --- a/app/display/representation/pom.xml +++ b/app/display/representation/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-representation @@ -22,7 +22,7 @@ org.phoebus app-display-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/EmbeddedDisplayRepresentationUtil.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/EmbeddedDisplayRepresentationUtil.java index c50a65867d..a16137c91e 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/EmbeddedDisplayRepresentationUtil.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/EmbeddedDisplayRepresentationUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016-2021 Oak Ridge National Laboratory. + * Copyright (c) 2016-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -75,7 +75,7 @@ public String toString() } /** Load display model, optionally trimmed to group - * @param model_widget Parent widget + * @param model_widget Parent widget, provides basis for expanding macros in the loaded model * @param display_and_group Display (and optional group) to load * @return {@link DisplayModel} */ @@ -105,6 +105,11 @@ public static DisplayModel loadDisplayModel(final VisibleWidget model_widget, fi // Tell embedded model that it is held by this widget, // which provides access to macros of model_widget. embedded_model.setUserData(DisplayModel.USER_DATA_EMBEDDING_WIDGET, model_widget); + + // reduceDisplayModelToGroup() might need macros to resolve a group name, + // so expand macros down the embedded model + embedded_model.expandMacros(model_widget.getEffectiveMacros()); + if (!display_and_group.getGroupName().isEmpty()) reduceDisplayModelToGroup(model_widget, embedded_model, display_and_group); // Adjust model name to reflect source file @@ -124,9 +129,9 @@ public static DisplayModel loadDisplayModel(final VisibleWidget model_widget, fi /** Reduce display model to content of one named group - * @param display_file Name of the display file + * @param model_widget Container widget * @param model Model loaded from that file - * @param group_name Name of group to use + * @param display_and_group Name of display and the group to use */ private static void reduceDisplayModelToGroup(final Widget model_widget, final DisplayModel model, final DisplayAndGroup display_and_group) { diff --git a/app/display/representation/src/main/resources/display_representation_preferences.properties b/app/display/representation/src/main/resources/display_representation_preferences.properties index 70698345cd..4a0dee2507 100644 --- a/app/display/representation/src/main/resources/display_representation_preferences.properties +++ b/app/display/representation/src/main/resources/display_representation_preferences.properties @@ -37,7 +37,7 @@ image_update_delay = 250 # Length limit for tool tips # Tool tips that are too long can be a problem # on some window systems. -tooltip_length=150 +tooltip_length=200 # Timeout for load / unload of Embedded Widget content [ms] embedded_timeout=5000 diff --git a/app/display/runtime/pom.xml b/app/display/runtime/pom.xml index 613e8ce3d5..63b0520865 100644 --- a/app/display/runtime/pom.xml +++ b/app/display/runtime/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-display - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-display-runtime @@ -37,22 +37,22 @@ org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-display-representation-javafx - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ActionUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ActionUtil.java index d5dbea9848..6607191ea9 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ActionUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ActionUtil.java @@ -84,12 +84,15 @@ private static void openDisplay(final Widget source_widget, String expanded_path = ""; try { - // Path to resolve, after expanding macros of source widget and action - final Macros expanded = new Macros(action.getMacros()); - expanded.expandValues(source_widget.getEffectiveMacros()); - final Macros macros = Macros.merge(source_widget.getEffectiveMacros(), expanded); + // Path to resolve, after expanding macros of action in environment of source widget + final Macros macros = action.getMacros(); // Not copying, just using action's macros + macros.expandValues(source_widget.getEffectiveMacros()); + + // For display path, use the combined macros... expanded_path = MacroHandler.replace(new MacroOrSystemProvider(macros), action.getFile()); - logger.log(Level.FINER, "{0}, effective macros {1} ({2})", new Object[] { action, macros, expanded_path }); + // .. but fall back to properties + expanded_path = MacroHandler.replace(source_widget.getMacrosOrProperties(), expanded_path); + logger.log(Level.FINER, "{0}, effective macros {1} ({2})", new Object[] { action, macros.toExpandedString(), expanded_path }); // Resolve new display file relative to the source widget model (not 'top'!) final DisplayModel widget_model = source_widget.getDisplayModel(); @@ -100,8 +103,8 @@ private static void openDisplay(final Widget source_widget, // Model is standalone; source_widget (Action button, ..) is _not_ the parent, // but it does add macros to those already defined in the display file. - final Macros combined_macros = Macros.merge(macros, new_model.propMacros().getValue()); - new_model.propMacros().setValue(combined_macros); + // Expand macros down the widget hierarchy + new_model.expandMacros(macros); // Schedule representation on UI thread... final DisplayModel top_model = source_widget.getTopDisplayModel(); @@ -277,8 +280,14 @@ private static void executeCommand(final Widget source_widget, final ExecuteComm // Resolve command relative to the source widget model (not 'top'!) final DisplayModel widget_model = source_widget.getDisplayModel(); final String parent_file = widget_model.getUserData(DisplayModel.USER_DATA_INPUT_FILE); - final String parent_dir = ModelResourceUtil.getDirectory(parent_file); + String parent_dir = ModelResourceUtil.getDirectory(parent_file); + // Check if the parent_dir exists or is reachable. If not, use "." as the parent_dir. + File parentDirectory = new File(parent_dir); + if (!parentDirectory.exists() || !parentDirectory.isDirectory()) { + logger.log(Level.WARNING, "Parent directory {0} does not exist or is not reachable. Using current directory instead to execute command.", parent_dir); + parent_dir = "."; + } // Execute (this is already running on background thread) logger.log(Level.FINE, "Executing command {0} in {1}", new Object[] { command, parent_dir }); final CommandExecutor executor = new CommandExecutor(command, new File(parent_dir)); @@ -370,7 +379,7 @@ private static void openWebpage(final Widget source_widget, final OpenWebpageAct private static String resolve(final Widget source_widget, final String path) throws Exception { // Path to resolve, after expanding macros - final Macros macros = source_widget.getEffectiveMacros(); + final MacroValueProvider macros = source_widget.getMacrosOrProperties(); final String expanded_path = MacroHandler.replace(macros, path); // Resolve file relative to the source widget model (not 'top'!) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java index 8587cadb70..01d1ec89f5 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayInfo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2017 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,7 +13,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; -import java.util.Objects; +import java.util.Locale; import java.util.logging.Level; import org.csstudio.display.builder.model.DisplayModel; @@ -58,7 +58,7 @@ public static DisplayInfo forURI(final URI uri) // Get basic file or http 'path' from path final String path; if (uri.getScheme() == null || uri.getScheme().equals("file")) - path = uri.getRawPath(); + path = uri.getPath(); else { final StringBuilder buf = new StringBuilder(); @@ -113,7 +113,26 @@ private static String basename(final String path) */ public static DisplayInfo forModel(final DisplayModel model) { - return new DisplayInfo(model.getUserData(DisplayModel.USER_DATA_INPUT_FILE), + String path; + { + String userDataInputFile = model.getUserData(DisplayModel.USER_DATA_INPUT_FILE); + String userDataInputFile_lowerCase = userDataInputFile.toLowerCase(Locale.ROOT); + if ( !userDataInputFile_lowerCase.startsWith("/") + && !userDataInputFile_lowerCase.startsWith("examples:") + && !userDataInputFile_lowerCase.startsWith("file:") + && !userDataInputFile_lowerCase.startsWith("http:") + && !userDataInputFile_lowerCase.startsWith("https:") + && !userDataInputFile_lowerCase.startsWith("ftp:") + && !userDataInputFile_lowerCase.startsWith("jar:")) { + // Add leading '/' and replace occurrences of '\' by '/' in the file path on Windows: + path = "/" + userDataInputFile.replace('\\', '/'); + } + else { + path = userDataInputFile; + } + } + + return new DisplayInfo(path, model.getDisplayName(), model.propMacros().getValue(), false); @@ -131,7 +150,11 @@ public DisplayInfo(final String path, final String name, final Macros macros, fi this.name = basename(path); else this.name = name; - this.macros = Objects.requireNonNull(macros); + // 'Normalize' the macros, define specs based on the current values + // (which are sorted) to assert we identify a display with macros + // no matter in which order they were spec'ed + this.macros = new Macros(); + macros.forEach(this.macros::add); this.resolve = resolve; } @@ -203,7 +226,10 @@ else if (path.contains(":")) // In path, keep ':' and '/', but replace spaces // Windows platform tweak replace \ with / - buf.append(path.replace(" ", "%20").replace('\\', '/')); + buf.append(path.replace(" ", "%20") + .replace('\\', '/') + .replace("[", "%5B") + .replace("]", "%5D")); // Add macros as path parameters boolean first = true; diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index e9fd3547ba..ffeeb6c07a 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2022 Oak Ridge National Laboratory. + * Copyright (c) 2017-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,8 +11,6 @@ import java.awt.geom.Rectangle2D; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; @@ -22,8 +20,8 @@ import java.util.logging.Level; import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Preferences; import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.macros.DisplayMacroExpander; import org.csstudio.display.builder.model.persist.ModelLoader; import org.csstudio.display.builder.model.util.ModelResourceUtil; import org.csstudio.display.builder.representation.javafx.JFXRepresentation; @@ -243,11 +241,14 @@ public void save(final Memento memento) /** Handle Alt-left & right as navigation keys */ private void handleKeys(final KeyEvent event) { + KeyCode keycode = event.getCode(); + if(keycode == KeyCode.F5) + this.reload(); if (event.isAltDown()) { - if (event.getCode() == KeyCode.LEFT && !navigate_backward.isDisabled()) + if (keycode == KeyCode.LEFT && !navigate_backward.isDisabled()) navigate_backward.getOnAction().handle(null); - else if (event.getCode() == KeyCode.RIGHT && !navigate_forward.isDisabled()) + else if (keycode == KeyCode.RIGHT && !navigate_forward.isDisabled()) navigate_forward.getOnAction().handle(null); } } @@ -337,15 +338,18 @@ public void loadDisplayFile(final DisplayInfo info) display_info = Optional.empty(); - if (dock_item.prepareToClose()) - Platform.runLater(() -> - { - final Parent parent = representation.getModelParent(); - JFXRepresentation.getChildren(parent).clear(); + boolean shouldClose = dock_item.okToClose().get(); - close(); - }); - return; + if (shouldClose) { + dock_item.prepareToClose(); + Platform.runLater(() -> + { + final Parent parent = representation.getModelParent(); + JFXRepresentation.getChildren(parent).clear(); + + close(); + }); + } } }); } @@ -376,12 +380,12 @@ private DisplayModel loadModel(final JobMonitor monitor, final DisplayInfo info) // Could simply use info's macros if they are non-empty, // but merging macros with those loaded from model file // allows for newly added macros in the display file. - final Macros macros = Macros.merge(model.propMacros().getValue(), info.getMacros()); - model.propMacros().setValue(macros); + final Macros environment = new Macros(); + Preferences.getMacros().forEachSpec(environment::add); + info.getMacros().forEachSpec(environment::add); + + model.expandMacros(environment); - // For runtime, expand macros - if (! representation.isEditMode()) - DisplayMacroExpander.expandDisplayMacros(model); return model; } @@ -409,6 +413,7 @@ void trackCurrentModel(final DisplayModel model) { final DisplayInfo old_info = display_info.orElse(null); final DisplayInfo info = DisplayInfo.forModel(model); + // A display might be loaded without macros, // but the DisplayModel may then have macros configured in the display itself. // @@ -452,21 +457,6 @@ private void disposeModel() ActionUtil.handleClose(model); } - /** Show error message and stack trace - * @param message Message - * @param error - */ - private void showError(final String message, final Throwable error) - { - logger.log(Level.WARNING, message, error); - - final ByteArrayOutputStream buf = new ByteArrayOutputStream(); - final PrintStream out = new PrintStream(buf); - out.append(message).append("\n\n"); - error.printStackTrace(out); - showMessage(buf.toString()); - } - /** Show message * @param message Message */ diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java index 5a13862a88..fc23c0dce4 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java @@ -141,24 +141,4 @@ public void representModel(final Parent model_parent, final DisplayModel model) app_instance.trackCurrentModel(model); super.representModel(model_parent, model); } - - @Override - public void closeWindow(final DisplayModel model) throws Exception - { - // Is called from ScriptUtil, i.e. scripts, from background thread - final Parent model_parent = Objects.requireNonNull(model.getUserData(Widget.USER_DATA_TOOLKIT_PARENT)); - if (model_parent.getProperties().get(DisplayRuntimeInstance.MODEL_PARENT_DISPLAY_RUNTIME) == app_instance) - { - // Prepare-to-close, which might take time and must be called off the UI thread - final DisplayRuntimeInstance instance = (DisplayRuntimeInstance) app_instance.getRepresentation().getModelParent().getProperties().get(DisplayRuntimeInstance.MODEL_PARENT_DISPLAY_RUNTIME); - if (instance != null) - instance.getDockItem().prepareToClose(); - else - logger.log(Level.SEVERE, "Missing DisplayRuntimeInstance to prepare closing", new Exception("Stack Trace")); - // 'close' on the UI thread - execute(() -> app_instance.close()); - } - else - throw new Exception("Wrong model"); - } } diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/FileUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/FileUtil.java index 4e18f51c57..0d1d941929 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/FileUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/FileUtil.java @@ -175,14 +175,18 @@ public static String openSaveFileDialog(String dialogPath) return selected.getPath(); } - /**Open a file save dialog. - * @param inWorkspace true if it is a workspace file dialog; Otherwise, it is a local - * file system file dialog. - * @return the full file path. Or null if it is cancelled. + /** Show file "Save As" dialog for selecting/entering a new file name + * + *

Call blocks until the user closes the dialog + * by either either entering/selecting a file name, or pressing "Cancel". + * + * @param widget Widget, used to create and position the dialog + * @param initial_value Initial path and file name + * @return Path and file name or null */ - public static String saveFileDialog(boolean inWorkspace) + public static String saveFileDialog(Widget widget, String initial_value) { - return ScriptUtil.showSaveAsDialog(null, null); + return ScriptUtil.showSaveAsDialog(widget, initial_value); } diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/PVUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/PVUtil.java index 86f9970a75..c9a930e336 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/PVUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/PVUtil.java @@ -202,7 +202,7 @@ public final static long getTimeInMilliseconds(final RuntimePV pv) throws NullPo /** Get alarm severity of the PV as an integer value. * @param pv PV - * @return 0: OK; 1: Major; 2:Minor, -1: Invalid or Undefined + * @return 0: OK; 1: Minor; 2: Major; 3: Invalid; 4: Undefined */ public final static int getSeverity(final RuntimePV pv) { diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java index 86432a6b94..600fff19bd 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016-2020 Oak Ridge National Laboratory. + * Copyright (c) 2016-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -132,38 +132,15 @@ public static void openDisplay(final Widget widget, final String file, final Str the_target = OpenDisplayActionInfo.Target.TAB; } - final Macros the_macros; - if (macros == null || macros.isEmpty()) - the_macros = null; - else - { - the_macros = new Macros(); + final Macros the_macros = new Macros(); + if (macros != null) for (String name : macros.keySet()) the_macros.add(name, macros.get(name)); - } final OpenDisplayActionInfo open = new OpenDisplayActionInfo("Open from script", file, the_macros, the_target, pane); ActionUtil.handleAction(widget, open); } - /** Close a display - * - * @param widget Widget within the display to close - */ - public static void closeDisplay(final Widget widget) - { - try - { - final DisplayModel model = widget.getTopDisplayModel(); - final ToolkitRepresentation toolkit = ToolkitRepresentation.getToolkit(model); - toolkit.closeWindow(model); - } - catch (Throwable ex) - { - logger.log(Level.WARNING, "Cannot close display", ex); - } - } - // ==================== // public alert dialog utils diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/internal/JythonScriptSupport.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/internal/JythonScriptSupport.java index 637385c12e..c49a65fe9d 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/internal/JythonScriptSupport.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/internal/JythonScriptSupport.java @@ -11,6 +11,8 @@ import java.io.InputStream; import java.io.InputStreamReader; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Properties; import java.util.concurrent.Future; import java.util.logging.Level; @@ -108,7 +110,8 @@ private static boolean init() // Add the examples/connect2j to path. // During development, examples are in // "file:/some/path/phoebus/applications/display/model/target/classes/examples" - final String examples = ModelPlugin.class.getResource("/examples").toString(); + String examplesURL = ModelPlugin.class.getResource("/examples").toString(); + String examples = URLDecoder.decode(examplesURL, StandardCharsets.UTF_8); if (examples.startsWith("file:")) paths.add(examples.substring(5) + "/connect2j"); // In the compiled version, examples are in diff --git a/app/display/thumbwheel/pom.xml b/app/display/thumbwheel/pom.xml index 21b0ee7e34..367c9c716c 100644 --- a/app/display/thumbwheel/pom.xml +++ b/app/display/thumbwheel/pom.xml @@ -3,7 +3,7 @@ app-display org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -17,19 +17,19 @@ org.phoebus app-display-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile org.phoebus app-display-representation - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile org.phoebus app-display-representation-javafx - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile diff --git a/app/display/thumbwheel/src/main/java/org/csstudio/display/widget/ThumbWheel.java b/app/display/thumbwheel/src/main/java/org/csstudio/display/widget/ThumbWheel.java index 4b964d2f31..25d74975be 100644 --- a/app/display/thumbwheel/src/main/java/org/csstudio/display/widget/ThumbWheel.java +++ b/app/display/thumbwheel/src/main/java/org/csstudio/display/widget/ThumbWheel.java @@ -21,7 +21,6 @@ import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; @@ -36,7 +35,6 @@ import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.input.ScrollEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; @@ -65,15 +63,28 @@ @SuppressWarnings( "ClassWithoutLogger" ) public class ThumbWheel extends GridPane { + public ThumbWheel(double widgetWidth, + double widgetHeight, + boolean hasNegativeSign, + Consumer writeValueToPV) { + this.widgetWidth = widgetWidth; + this.widgetHeight = widgetHeight; + this.hasNegativeSign = hasNegativeSign; + this.writeValueToPV = writeValueToPV; + initialize(); + } + + private double widgetWidth; + private double widgetHeight; + private Consumer writeValueToPV; private static final Color DEFAULT_DECREMENT_BUTTON_COLOR = Color.web("#d7d7ec"); private static final Font DEFAULT_FONT = new Label().getFont(); - private static final double DEFAULT_HGAP = 2.0; + private static final double DEFAULT_MARGIN = 2.0; private static final Color DEFAULT_INCREMENT_BUTTON_COLOR = Color.web("#ecd7d7"); - private static final char INVALID_MARK = '\u00D7'; + private static final String INVALID_MARK = "\u00D7"; private static final Logger LOGGER = Logger.getLogger(ThumbWheel.class.getName()); private static final char SIGN_MARK = '\u2013'; private static final char SIGN_SPACE = '\u2002'; - private static final double SPINNER_HGAP = 0; private static final Consumer STYLE_CLASS_REMOVER = button -> { button.getStyleClass().remove("thumb-wheel-increment-spinner-button"); button.getStyleClass().remove("thumb-wheel-decrement-spinner-button"); @@ -83,9 +94,12 @@ public class ThumbWheel extends GridPane { private final List - -

- - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +
+
+
+
+
+
+
+
diff --git a/app/imageviewer/src/main/resources/image_viewer_preferences.properties b/app/imageviewer/src/main/resources/image_viewer_preferences.properties new file mode 100644 index 0000000000..a98de5fe78 --- /dev/null +++ b/app/imageviewer/src/main/resources/image_viewer_preferences.properties @@ -0,0 +1,6 @@ +# -------------------------------------------- +# Package org.phoebus.applications.imageviewer +# -------------------------------------------- + +# Watermark text +watermark_text=W A T E R M A R K diff --git a/app/imageviewer/src/main/resources/org/phoebus/applications/imageviewer/messages.properties b/app/imageviewer/src/main/resources/org/phoebus/applications/imageviewer/messages.properties index 16c9c21849..93e713bd16 100644 --- a/app/imageviewer/src/main/resources/org/phoebus/applications/imageviewer/messages.properties +++ b/app/imageviewer/src/main/resources/org/phoebus/applications/imageviewer/messages.properties @@ -19,4 +19,5 @@ ErrorDialogTitle=Error ErrorDialogText=Failed to load image resource {0} OneHundredPercent=Show 100% -ScaleToFit=Scale to fit \ No newline at end of file +ScaleToFit=Scale to fit +watermarkButton=Watermark diff --git a/app/imageviewer/src/main/resources/style.css b/app/imageviewer/src/main/resources/style.css new file mode 100644 index 0000000000..927619da41 --- /dev/null +++ b/app/imageviewer/src/main/resources/style.css @@ -0,0 +1,4 @@ +.outline.label .text { + -fx-stroke: #22222290; + -fx-stroke-width: 1px; +} \ No newline at end of file diff --git a/app/log-configuration/pom.xml b/app/log-configuration/pom.xml index 1a107fa398..f2445162e3 100644 --- a/app/log-configuration/pom.xml +++ b/app/log-configuration/pom.xml @@ -4,13 +4,13 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/log-configuration/src/main/java/org/phoebus/applications/utility/LoggingConfiguration.java b/app/log-configuration/src/main/java/org/phoebus/applications/utility/LoggingConfiguration.java index 27cd9f61c4..89e1e36157 100644 --- a/app/log-configuration/src/main/java/org/phoebus/applications/utility/LoggingConfiguration.java +++ b/app/log-configuration/src/main/java/org/phoebus/applications/utility/LoggingConfiguration.java @@ -1,12 +1,14 @@ package org.phoebus.applications.utility; import java.util.Arrays; +import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import javafx.scene.control.Button; import org.phoebus.framework.spi.AppDescriptor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.ui.docking.DockItem; @@ -180,11 +182,21 @@ public void changed(ObservableValue observable, Boolean oldVa treeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); treeTableView.getSelectionModel().setCellSelectionEnabled(true); - AnchorPane.setTopAnchor(treeTableView, 10.0); + Button refresh = new Button(); + refresh.setOnAction(event -> updateLoggerMap()); + refresh.setText("refresh"); + refresh.setPrefWidth(80); + refresh.setPrefHeight(15); + + AnchorPane.setTopAnchor(refresh, 5.0); + AnchorPane.setRightAnchor(refresh, 10.0); + + AnchorPane.setTopAnchor(treeTableView, 35.0); AnchorPane.setBottomAnchor(treeTableView, 10.0); AnchorPane.setLeftAnchor(treeTableView, 10.0); AnchorPane.setRightAnchor(treeTableView, 10.0); + anchorpane.getChildren().add(refresh); anchorpane.getChildren().add(treeTableView); updateLoggerMap(); @@ -267,6 +279,7 @@ public void updateLoggerMap() { manager.getLogger(fullName), manager.getLogger(fullName) != null ? true : false)); newNode.setExpanded(true); parent.getChildren().add(newNode); + FXCollections.sort(parent.getChildren(), Comparator.comparing(o -> o.getValue().getFullName())); parent = newNode; } } diff --git a/app/logbook/elog/pom.xml b/app/logbook/elog/pom.xml index cfc6256c9b..bfa4e18c2f 100644 --- a/app/logbook/elog/pom.xml +++ b/app/logbook/elog/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-logbook-elog @@ -14,12 +14,12 @@ org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT net.dongliu diff --git a/app/logbook/elog/src/main/java/org/phoebus/elog/api/ElogApi.java b/app/logbook/elog/src/main/java/org/phoebus/elog/api/ElogApi.java index 73a1aeaa67..8ea902ad9c 100644 --- a/app/logbook/elog/src/main/java/org/phoebus/elog/api/ElogApi.java +++ b/app/logbook/elog/src/main/java/org/phoebus/elog/api/ElogApi.java @@ -233,7 +233,7 @@ public void delete( Long msgId ) throws LogbookException { * Searches the logbook and returns the message ids. * * @param search_term - * @param {@link ElogEntry} + * */ public List search( Map search_term ) throws LogbookException { @@ -332,7 +332,7 @@ public File getAttachment( String filename ) throws LogbookException { /** * Get list of available types * - * @return Collection + * @return {@literal Collection} */ public Collection getTypes() throws LogbookException { Map cookies = new HashMap<>(); @@ -392,7 +392,7 @@ public Collection getTypes() throws LogbookException { /** * Get list of available categories * - * @return Collection + * @return {@literal Collection} */ public Collection getCategories() throws LogbookException { Map cookies = new HashMap<>(); diff --git a/app/logbook/elog/src/main/java/org/phoebus/elog/api/Sha256.java b/app/logbook/elog/src/main/java/org/phoebus/elog/api/Sha256.java index e1dab06fc4..ad0e0f03b5 100644 --- a/app/logbook/elog/src/main/java/org/phoebus/elog/api/Sha256.java +++ b/app/logbook/elog/src/main/java/org/phoebus/elog/api/Sha256.java @@ -29,7 +29,7 @@ /** * - * @author Juan F. Esteban Müller + * @author Juan F. Esteban Müller {@literal } */ public class Sha256 { diff --git a/app/logbook/inmemory/pom.xml b/app/logbook/inmemory/pom.xml index 3bd7d5bb4b..448a2effac 100644 --- a/app/logbook/inmemory/pom.xml +++ b/app/logbook/inmemory/pom.xml @@ -3,14 +3,14 @@ org.phoebus app-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-logbook-inmemory org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT com.google.guava diff --git a/app/logbook/olog/client-es/pom.xml b/app/logbook/olog/client-es/pom.xml index eb27f7bc17..2296caace4 100644 --- a/app/logbook/olog/client-es/pom.xml +++ b/app/logbook/olog/client-es/pom.xml @@ -3,7 +3,7 @@ app-logbook-olog org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -57,22 +57,22 @@ org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java index 8feb129173..256a034c9a 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/OlogESLogbook.java @@ -1,42 +1,50 @@ package org.phoebus.applications.logbook; -import java.util.logging.Level; -import java.util.logging.Logger; - import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogFactory; -import org.phoebus.olog.es.api.OlogClient; import org.phoebus.olog.es.api.OlogClient.OlogClientBuilder; import org.phoebus.security.tokens.SimpleAuthenticationToken; + +import java.util.logging.Level; +import java.util.logging.Logger; + /** * Logbook client for the new es based olog. * TODO: in the future this client would replace the old olog client - * @author kunal * + * @author kunal */ public class OlogESLogbook implements LogFactory { private static final Logger logger = Logger.getLogger(OlogESLogbook.class.getName()); private static final String ID = "olog-es"; - private OlogClient oLogClient; @Override public String getId() { return ID; } + /** + * + * @return A fresh instance of the client. Instead of maintaining a client reference in this class, + * a new instance is return since the user may have signed out or signed in thus invalidating + * the authentication token. + */ @Override public LogClient getLogClient() { - if (oLogClient == null) { - try { - oLogClient = OlogClientBuilder.serviceURL().create(); - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to create olog es client", e); - } + try { + return OlogClientBuilder.serviceURL().create(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to create olog es client", e); } - return oLogClient; + return null; } + /** + * @param authToken An authentication token. + * @return A fresh instance of the client. Instead of maintaining a client reference in this class, + * a new instance to force usage of the specified authentication token. + */ @Override public LogClient getLogClient(Object authToken) { try { @@ -44,14 +52,12 @@ public LogClient getLogClient(Object authToken) { SimpleAuthenticationToken token = (SimpleAuthenticationToken) authToken; return OlogClientBuilder.serviceURL().withHTTPAuthentication(true).username(token.getUsername()).password(token.getPassword()) .create(); - } else if (oLogClient == null) { - oLogClient = OlogClientBuilder.serviceURL().create(); - + } else { + return getLogClient(); } } catch (Exception e) { logger.log(Level.SEVERE, "Failed to create olog client", e); } - return oLogClient; + return null; } - } diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java index 4420faf333..5ea44a17a1 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/applications/logbook/authentication/OlogServiceAuthenticationProvider.java @@ -21,6 +21,7 @@ import org.phoebus.olog.es.api.OlogClient; import org.phoebus.olog.es.api.OlogClient.OlogClientBuilder; import org.phoebus.security.authorization.ServiceAuthenticationProvider; +import org.phoebus.security.tokens.AuthenticationScope; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,7 +46,7 @@ public void logout(String token) { } @Override - public String getServiceName() { - return "logbook"; + public AuthenticationScope getAuthenticationScope() { + return AuthenticationScope.LOGBOOK; } } diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java index 606257a11c..4129f51640 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java @@ -2,12 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientHandlerException; -import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.*; import com.sun.jersey.api.client.ClientResponse.Status; -import com.sun.jersey.api.client.UniformInterfaceException; -import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; @@ -16,18 +12,13 @@ import com.sun.jersey.multipart.FormDataMultiPart; import com.sun.jersey.multipart.file.FileDataBodyPart; import com.sun.jersey.multipart.impl.MultiPartWriter; -import org.phoebus.logbook.Attachment; -import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogEntry; -import org.phoebus.logbook.Logbook; -import org.phoebus.logbook.LogbookException; -import org.phoebus.logbook.Messages; -import org.phoebus.logbook.Property; -import org.phoebus.logbook.SearchResult; -import org.phoebus.logbook.Tag; +import org.phoebus.logbook.*; import org.phoebus.olog.es.api.model.OlogLog; import org.phoebus.olog.es.api.model.OlogObjectMappers; import org.phoebus.olog.es.api.model.OlogSearchResult; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; import javax.net.ssl.SSLContext; import javax.ws.rs.core.MediaType; @@ -36,14 +27,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -60,6 +44,8 @@ public class OlogClient implements LogClient { private static final String CLIENT_INFO = "CS Studio " + Messages.AppVersion + " on " + System.getProperty("os.name"); + private List changeHandlers = new ArrayList<>(); + /** * Builder Class to help create a olog client. * @@ -145,8 +131,17 @@ public OlogClient create() { this.clientConfig = new DefaultClientConfig(); } } - this.username = ifNullReturnPreferenceValue(this.username, "username"); - this.password = ifNullReturnPreferenceValue(this.password, "password"); + if(this.username == null || this.password == null){ + ScopedAuthenticationToken scopedAuthenticationToken = getCredentialsFromSecureStore(); + if(scopedAuthenticationToken != null){ + this.username = scopedAuthenticationToken.getUsername(); + this.password = scopedAuthenticationToken.getPassword(); + } + else{ + this.username = ifNullReturnPreferenceValue(this.username, "username"); + this.password = ifNullReturnPreferenceValue(this.password, "password"); + } + } this.connectTimeoutAsString = ifNullReturnPreferenceValue(this.connectTimeoutAsString, "connectTimeout"); int connectTimeout = 0; try { @@ -166,6 +161,16 @@ private String ifNullReturnPreferenceValue(String value, String key) { return value; } } + + private ScopedAuthenticationToken getCredentialsFromSecureStore(){ + try { + SecureStore secureStore = new SecureStore(); + return secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); + } catch (Exception e) { + Logger.getLogger(OlogClientBuilder.class.getName()).log(Level.WARNING, "Unable to instantiate SecureStore", e); + return null; + } + } } private OlogClient(URI ologURI, ClientConfig config, boolean withHTTPBasicAuthFilter, String username, String password) { @@ -180,11 +185,16 @@ private OlogClient(URI ologURI, ClientConfig config, boolean withHTTPBasicAuthFi client.setFollowRedirects(true); client.setConnectTimeout(3000); this.service = client.resource(UriBuilder.fromUri(ologURI).build()); + + ServiceLoader serviceLoader = ServiceLoader.load(LogEntryChangeHandler.class); + serviceLoader.stream().forEach(p -> changeHandlers.add(p.get())); } @Override public LogEntry set(LogEntry log) throws LogbookException { - return save(log, null); + LogEntry newEntry = save(log, null); + changeHandlers.forEach(h -> h.logEntryChanged(newEntry)); + return newEntry; } /** @@ -197,7 +207,6 @@ public LogEntry set(LogEntry log) throws LogbookException { * @throws LogbookException E.g. due to invalid log entry data. */ private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException { - ClientResponse clientResponse; try { MultivaluedMap queryParams = new MultivaluedMapImpl(); @@ -205,51 +214,34 @@ private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException if (inReplyTo != null) { queryParams.putSingle("inReplyTo", Long.toString(inReplyTo.getId())); } - clientResponse = service.path("logs") + + FormDataMultiPart form = new FormDataMultiPart(); + try { + form.bodyPart(new FormDataBodyPart("logEntry", OlogObjectMappers.logEntrySerializer.writeValueAsString(log), MediaType.APPLICATION_JSON_TYPE)); + } catch (JsonProcessingException e) { + logger.log(Level.SEVERE, "Got unexpected exception", e); + throw e; + } + log.getAttachments().forEach(attachment -> { + // Add all files as represented in the attachment objects. Note that each gets + // the "multipart name" files, but that is OK. + form.bodyPart(new FileDataBodyPart("files", attachment.getFile())); + }); + + ClientResponse clientResponse = service.path("logs") .queryParams(queryParams) - .type(MediaType.APPLICATION_JSON) - .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) - .accept(MediaType.APPLICATION_XML) + .path("multipart") + .type(MediaType.MULTIPART_FORM_DATA) .accept(MediaType.APPLICATION_JSON) - .put(ClientResponse.class, OlogObjectMappers.logEntrySerializer.writeValueAsString(log)); - - if (clientResponse.getStatus() < 300) { - OlogLog createdLog = OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); - log.getAttachments().forEach(attachment -> { - FormDataMultiPart form = new FormDataMultiPart(); - // Add id only if it is set, otherwise Jersey will complain and cause the submission to fail. - if (attachment.getId() != null && !attachment.getId().isEmpty()) { - form.bodyPart(new FormDataBodyPart("id", attachment.getId())); - } - form.bodyPart(new FileDataBodyPart("file", attachment.getFile())); - form.bodyPart(new FormDataBodyPart("filename", attachment.getName())); - form.bodyPart(new FormDataBodyPart("fileMetadataDescription", attachment.getContentType())); - - ClientResponse attachmentResponse = service.path("logs") - .path("attachments") - .path(String.valueOf(createdLog.getId())) - .type(MediaType.MULTIPART_FORM_DATA) - .accept(MediaType.APPLICATION_XML) - .accept(MediaType.APPLICATION_JSON) - .post(ClientResponse.class, form); - if (attachmentResponse.getStatus() > 300) { - // TODO failed to add attachments - logger.log(Level.SEVERE, "Failed to submit attachment(s), HTTP status: " + attachmentResponse.getStatus()); - } - }); - - clientResponse = service.path("logs").path(String.valueOf(createdLog.getId())) - .type(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .get(ClientResponse.class); - return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); - } else if (clientResponse.getStatus() == 401) { - logger.log(Level.SEVERE, "Submission of log entry returned HTTP status, invalid credentials"); - throw new LogbookException(Messages.SubmissionFailedInvalidCredentials); - } else { - logger.log(Level.SEVERE, "Submission of log entry returned HTTP status" + clientResponse.getStatus()); - throw new LogbookException(MessageFormat.format(Messages.SubmissionFailedWithHttpStatus, clientResponse.getStatus())); + .put(ClientResponse.class, form); + + if (clientResponse.getStatus() > 300) { + logger.log(Level.SEVERE, "Failed to create log entry: " + clientResponse); + throw new LogbookException(clientResponse.toString()); } + + return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); + } catch (UniformInterfaceException | ClientHandlerException | IOException e) { logger.log(Level.SEVERE, "Failed to submit log entry, got client exception", e); throw new LogbookException(e); @@ -310,11 +302,11 @@ private SearchResult findLogs(MultivaluedMap searchParams) throw .get(String.class), OlogSearchResult.class); return SearchResult.of(new ArrayList<>(ologSearchResult.getLogs()), - ologSearchResult.getHitCount()); + ologSearchResult.getHitCount()); } catch (UniformInterfaceException | ClientHandlerException | IOException e) { logger.log(Level.WARNING, "failed to retrieve log entries", e); - if(e instanceof UniformInterfaceException){ - if(((UniformInterfaceException) e).getResponse().getStatus() == Status.BAD_REQUEST.getStatusCode()){ + if (e instanceof UniformInterfaceException) { + if (((UniformInterfaceException) e).getResponse().getStatus() == Status.BAD_REQUEST.getStatusCode()) { throw new RuntimeException(Messages.BadRequestFailure); } } @@ -448,7 +440,7 @@ public String getServiceUrl() { } @Override - public LogEntry updateLogEntry(LogEntry logEntry) { + public LogEntry update(LogEntry logEntry) { ClientResponse clientResponse; try { @@ -458,7 +450,9 @@ public LogEntry updateLogEntry(LogEntry logEntry) { .accept(MediaType.APPLICATION_XML) .accept(MediaType.APPLICATION_JSON) .post(ClientResponse.class, OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntry)); - return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); + LogEntry updated = OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), OlogLog.class); + changeHandlers.forEach(h -> h.logEntryChanged(updated)); + return updated; } catch (Exception e) { logger.log(Level.SEVERE, "Unable to update log entry id=" + logEntry.getId(), e); return null; @@ -473,7 +467,7 @@ public SearchResult search(Map map) { } @Override - public void groupLogEntries(List logEntryIds) throws LogbookException{ + public void groupLogEntries(List logEntryIds) throws LogbookException { try { ClientResponse clientResponse = service.path("logs/group") .type(MediaType.APPLICATION_JSON) @@ -516,8 +510,25 @@ public void authenticate(String username, String password) throws Exception { } @Override - public String serviceInfo(){ + public String serviceInfo() { ClientResponse clientResponse = service.path("").get(ClientResponse.class); return clientResponse.getEntity(String.class); } + + @Override + public SearchResult getArchivedEntries(long id){ + try { + final OlogSearchResult ologSearchResult = OlogObjectMappers.logEntryDeserializer.readValue( + service.path("logs/archived/" + id) + .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) + .accept(MediaType.APPLICATION_JSON) + .get(String.class), + OlogSearchResult.class); + return SearchResult.of(new ArrayList<>(ologSearchResult.getLogs()), + ologSearchResult.getHitCount()); + } catch (UniformInterfaceException | ClientHandlerException | IOException e) { + logger.log(Level.WARNING, "failed to retrieve archived log entries", e); + throw new RuntimeException(e); + } + } } diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLog.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLog.java index f469482e24..0500e76871 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLog.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLog.java @@ -6,6 +6,7 @@ package org.phoebus.olog.es.api.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.phoebus.logbook.Attachment; import org.phoebus.logbook.LogEntry; @@ -20,7 +21,7 @@ /** * Log object that can be represented as XML/JSON in payload data. * - * @author Kunal Shroff taken from Ralph Lange + * @author Kunal Shroff taken from Ralph Lange {@literal } */ @JsonIgnoreProperties(ignoreUnknown = true) public class OlogLog implements LogEntry { @@ -188,6 +189,7 @@ public void setCreatedDate(Instant createdDate) { * * @return modifiedDate */ + @JsonProperty("modifyDate") @Override public Instant getModifiedDate() { return modifiedDate; diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLogbook.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLogbook.java index e3747429d7..1dcf7429ef 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLogbook.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogLogbook.java @@ -11,7 +11,7 @@ /** * Logbook object that can be represented as XML/JSON in payload data. * - * @author Kunal Shroff taken from Ralph Lange + * @author Kunal Shroff taken from Ralph Lange {@literal } */ @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogProperty.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogProperty.java index 40738349b6..bca4e09a40 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogProperty.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogProperty.java @@ -13,8 +13,7 @@ /** * Property object that can be represented as XML/JSON in payload data. * - * @author Kunal Shroff taken from Ralph Lange - * + * @author Kunal Shroff taken from Ralph Lange {@literal } */ @JsonIgnoreProperties(ignoreUnknown = true) public class OlogProperty implements Property { diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogTag.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogTag.java index ca2b86b65b..2ab244338e 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogTag.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/model/OlogTag.java @@ -12,7 +12,7 @@ /** * Tag object that can be represented as XML/JSON in payload data. * - * @author Kunal Shroff taken from Ralph Lange + * @author Kunal Shroff taken from Ralph Lange {@literal } */ @JsonIgnoreProperties(ignoreUnknown = true) public class OlogTag implements Tag { diff --git a/app/logbook/olog/client/pom.xml b/app/logbook/olog/client/pom.xml index f447cc6db7..b63ed3093f 100644 --- a/app/logbook/olog/client/pom.xml +++ b/app/logbook/olog/client/pom.xml @@ -3,7 +3,7 @@ app-logbook-olog org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -17,17 +17,17 @@ org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT com.fasterxml.jackson.core diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/OlogLog.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/OlogLog.java index 4d1d85797c..f22f08b147 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/OlogLog.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/OlogLog.java @@ -134,7 +134,7 @@ public void setVersion(int version) { @Override public String getTitle() { - return getSource().substring(0, getSource().length() < 50 ? getSource().length() : 50); + return getSource().substring(0, getSource().length() < 50 ? getSource().length() : (getSource().substring(0, 50).contains(" ") ? getSource().substring(0, 50).lastIndexOf(" ") : 50)); } /** diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLog.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLog.java index fbf1aab76a..dd2abd4d7f 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLog.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLog.java @@ -23,7 +23,7 @@ /** * Log object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlAccessorType(XmlAccessType.NONE) @XmlRootElement(name = "log") @@ -71,7 +71,6 @@ public XmlLog(Long logId) { /** * Creates a new instance of XmlLog. * - * @param subject log subject * @param owner log owner */ public XmlLog(/*String subject, */String owner) { diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbook.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbook.java index cef160f943..212365f615 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbook.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbook.java @@ -15,7 +15,7 @@ /** * Logbook object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlType(propOrder = {"id", "name", "owner", "xmlLogs"}) @XmlRootElement(name = "logbook") @@ -61,7 +61,7 @@ public Long getId() { /** * Setter for logbook id. * - * @param name logbook id + * @param id logbook id */ public void setId(Long id) { this.id = id; diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbooks.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbooks.java index 72494d983a..f078f3f8db 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbooks.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogbooks.java @@ -14,7 +14,7 @@ /** * Logbooks (collection) object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlRootElement(name = "logbooks") diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogs.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogs.java index 038acca8ec..8b557db835 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogs.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlLogs.java @@ -14,7 +14,7 @@ /** * Logs (collection) object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlRootElement(name = "logs") diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperties.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperties.java index c0dba38372..0495279a13 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperties.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperties.java @@ -14,7 +14,7 @@ /** * Properties (collection) object that can be represented as XML/JSON in payload data. * - * @author Eric Beryman taken from Ralph Lange + * @author Eric Beryman taken from Ralph Lange {@literal } */ @XmlRootElement(name = "properties") @@ -26,7 +26,7 @@ public XmlProperties() { } /** Creates a new instance of XmlProperties with one initial property. - * @param c initial element + * @param p initial element */ public XmlProperties(XmlProperty p) { properties.add(p); diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperty.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperty.java index 7ea99305b2..3a80a786c1 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperty.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlProperty.java @@ -16,8 +16,7 @@ /** * Property object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange - * + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlRootElement(name = "property") public class XmlProperty { @@ -39,7 +38,6 @@ public XmlProperty() { * Creates a new instance of XmlProperty. * * @param name - * @param value */ public XmlProperty(String name) { this.name = name; @@ -89,9 +87,9 @@ public int getGroupingNum() { } /** - * Setter for property id. + * Setter for grouping number * - * @param id property id + * @param groupingNum */ public void setGroupingNum(int groupingNum) { this.groupingNum = groupingNum; diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTag.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTag.java index 09069c745c..db9a46867b 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTag.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTag.java @@ -16,7 +16,7 @@ /** * Tag object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from {@literal } */ @XmlType(propOrder = {"name","state","xmlLogs"}) @XmlRootElement(name = "tag") diff --git a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTags.java b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTags.java index 392ea82338..806e563835 100644 --- a/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTags.java +++ b/app/logbook/olog/client/src/main/java/org/phoebus/olog/api/XmlTags.java @@ -14,7 +14,7 @@ /** * Tags (collection) object that can be represented as XML/JSON in payload data. * - * @author Eric Berryman taken from Ralph Lange + * @author Eric Berryman taken from Ralph Lange {@literal } */ @XmlRootElement(name = "tags") diff --git a/app/logbook/olog/pom.xml b/app/logbook/olog/pom.xml index 9e1ff179b8..f2cdd46494 100644 --- a/app/logbook/olog/pom.xml +++ b/app/logbook/olog/pom.xml @@ -9,7 +9,7 @@ org.phoebus app-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-logbook-olog diff --git a/app/logbook/olog/ui/doc/images/LogEntryTable.png b/app/logbook/olog/ui/doc/images/LogEntryTable.png index cc9c0f6ac8..90fa3ab6f1 100644 Binary files a/app/logbook/olog/ui/doc/images/LogEntryTable.png and b/app/logbook/olog/ui/doc/images/LogEntryTable.png differ diff --git a/app/logbook/olog/ui/doc/images/ReplyAnnotation.png b/app/logbook/olog/ui/doc/images/ReplyAnnotation.png new file mode 100644 index 0000000000..377645b4bc Binary files /dev/null and b/app/logbook/olog/ui/doc/images/ReplyAnnotation.png differ diff --git a/app/logbook/olog/ui/doc/index.rst b/app/logbook/olog/ui/doc/index.rst index 126188cad3..9b0dd4bd7d 100644 --- a/app/logbook/olog/ui/doc/index.rst +++ b/app/logbook/olog/ui/doc/index.rst @@ -23,9 +23,15 @@ Features Launching the log entry editor ------------------------------ -To launch the log entry editor, the user may select Applications -> Utility -> Send to Logbook from the menu: +The log entry editor is launched as a non-modal window using one of the following methods: -.. image:: images/SendToLogbook.png +- From the dedicated button in the application toolbar. + +- From application menu Applications -> Utility -> Send to Logbook. + +- Using the New Log Entry button in the log entry details view of the logbook application. + +- Using the New Log Entry context menu item in the search result list view of the logbook application. This option also supports the keyboard combination CTRL+N. The log entry editor may also be launched from context menus, where applicable. For instance, with a right click on the background of an OPI the launched context menu will include the Create Log item: @@ -207,11 +213,19 @@ feature user can choose to: .. image:: images/ContextMenuLogEntryTable.png -Log entries that are contained in a log entry group are rendered with a grey background in the search result table view. +Log entries that are contained in a log entry group are rendered with a "reply" icon in the search result table view: + +.. image:: images/ReplyAnnotation.png + In the log entry view, the "Show/Hide Group" button (see screen shot above) can be used to show all log entries of a group sequentially, ordered on created date with oldest log entry on top. In this merged view attachments and properties are not shown. Clicking on a header in the merged view will show that log entry in full. +**NOTE**: To be able to group log entries user must be authenticated in one of the following manners: + +* Use "credentials caching" through preference setting ``org.phoebus.logbook.olog.ui/save_credentials``. Once a log entry has been created, credentials will be reused when creating a group. +* Use the Credentials Management app to sign in to the logbook context. + Limitations ^^^^^^^^^^^ diff --git a/app/logbook/olog/ui/pom.xml b/app/logbook/olog/ui/pom.xml index 04b95b2629..36101fdfe6 100644 --- a/app/logbook/olog/ui/pom.xml +++ b/app/logbook/olog/ui/pom.xml @@ -3,7 +3,7 @@ app-logbook-olog org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -13,32 +13,32 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-logbook-olog-client-es - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT com.google.guava diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/ArchivedLogEntriesManager.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/ArchivedLogEntriesManager.java new file mode 100644 index 0000000000..212d173906 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/ArchivedLogEntriesManager.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.logbook.olog.ui; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.stage.FileChooser; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.logbook.LogClient; +import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.SearchResult; +import org.phoebus.ui.application.ApplicationLauncherService; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.MessageFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class downloads archived log entries and then prompt user for a file in which the + * list of log entries is written as pretty printed JSON. + * + * If an external application has been configured for extension json, it will be launched once the + * file has been saved successfully. + */ +public class ArchivedLogEntriesManager { + private LogClient logClient; + + public ArchivedLogEntriesManager(LogClient logClient) { + this.logClient = logClient; + } + + /** + * Downloads archived log entries as a {@link org.phoebus.framework.jobs.Job} and then + * launches a {@link FileChooser} such that user can specify target file name for the retrieved data. + *

+ * The current {@link LogEntry} is written as first JSON object in the output file. + * + * @param ownerNode {@link Node} used by {@link FileChooser}. + * @param logEntry A (valid) log entry id shared among all archived entries with same id. + */ + public void handle(Node ownerNode, LogEntry logEntry) { + JobManager.schedule("Get Archived Log Entries", monitor -> { + long logEntryId = logEntry.getId(); + SearchResult searchResult; + try { + searchResult = logClient.getArchivedEntries(logEntryId); + } catch (Exception e) { + Platform.runLater(() -> { + ExceptionDetailsErrorDialog.openError("Error", Messages.ArchivedDownloadFailed, e); + }); + return; + } + if (searchResult.getHitCount() == 0) { // Should not happen! + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setHeaderText(MessageFormat.format(Messages.ArchivedNoEntriesFound, logEntryId)); + alert.show(); + }); + return; + } + String fileName = "archived_log_entries_" + logEntryId + ".json"; + + FileChooser fileChooser = new FileChooser(); + FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("Json files (.json)", "*.json"); + fileChooser.getExtensionFilters().add(extFilter); + fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); + fileChooser.setInitialFileName(fileName); + + Platform.runLater(() -> { + File destinationFile = fileChooser.showSaveDialog(ownerNode.getScene().getWindow()); + if (destinationFile != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + .withZone(ZoneId.systemDefault()); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // Since this write to file that someone will read, format time accordingly... + javaTimeModule.addSerializer(Instant.class, new JsonSerializer<>() { + @Override + public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(formatter.format(value)); + } + }); + + ObjectMapper objectMapper = new ObjectMapper().registerModule(javaTimeModule); + BufferedOutputStream writer = null; + try { + writer = new BufferedOutputStream(new FileOutputStream(destinationFile)); + List allLogEntries = new ArrayList<>(); + allLogEntries.add(logEntry); // Current log entry is first element in array... + allLogEntries.addAll(searchResult.getLogs()); // ...add all archived log entries + writer.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(allLogEntries).getBytes()); + } catch (Exception e) { + Logger.getLogger(ArchivedLogEntriesManager.class.getName()) + .log(Level.WARNING, "Unable to save archived log entries to file", e); + Platform.runLater(() -> { + ExceptionDetailsErrorDialog.openError("Error", Messages.ArchivedSaveFailed, e); + }); + } finally { + if (writer != null) { + // Launch viewer, if one has been configured for extension json + ApplicationLauncherService.openFile(destinationFile, false, null); + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + }); + }); + } +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java index a098bbc7bf..dd3918a410 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java @@ -54,6 +54,7 @@ import org.phoebus.ui.javafx.ImageCache; import javax.imageio.ImageIO; +import javax.ws.rs.core.UriBuilder; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -144,7 +145,9 @@ public void initialize() { } } } - ApplicationLauncherService.openFile(attachment.getFile(), false, null); + else { + showImageAttachment(); + } } }); @@ -187,13 +190,22 @@ public void initialize() { imagePreview.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { if (selectedAttachment.get() != null && selectedAttachment.get().getContentType().startsWith("image")) { - ApplicationLauncherService.openFile(selectedAttachment.get().getFile(), - false, null); + showImageAttachment(); } event.consume(); }); } + /** + * Launches the Image Viewer application to show the selected image attachment with a watermark. + */ + private void showImageAttachment(){ + URI uri = selectedAttachment.get().getFile().toURI(); + URI withWatermark = UriBuilder.fromUri(uri).queryParam("watermark", "true").build(); + ApplicationLauncherService.openResource(withWatermark, + false, null); + } + public ObservableList getSelectedAttachments() { return attachmentListView.getSelectionModel().getSelectedItems(); } @@ -212,7 +224,7 @@ public void setAttachments(Collection attachments) { this.attachments.setAll(attachments); attachmentListView.setItems(this.attachments); // Update UI - if (this.attachments.size() > 0) { + if (!this.attachments.isEmpty()) { attachmentListView.getSelectionModel().select(this.attachments.get(0)); } }); @@ -253,18 +265,25 @@ private void showPreview() { * @param attachment The image {@link Attachment} selected by user. */ private void showImagePreview(Attachment attachment) { - try { - BufferedImage bufferedImage = ImageIO.read(attachment.getFile()); - // BufferedImage may be null due to lazy loading strategy. - if (bufferedImage == null) { - return; - } - Image image = SwingFXUtils.toFXImage(bufferedImage, null); - imagePreview.visibleProperty().setValue(true); - imagePreview.setImage(image); - } catch (IOException ex) { - Logger.getLogger(AttachmentsEditorController.class.getName()) - .log(Level.SEVERE, "Unable to load image file " + attachment.getFile().getAbsolutePath(), ex); + if (attachment.getFile() != null) { + // Load image data off UI thread... + JobManager.schedule("Show image attachment", monitor -> { + try { + BufferedImage bufferedImage = ImageIO.read(attachment.getFile()); + // BufferedImage may be null due to lazy loading strategy. + if (bufferedImage == null) { + return; + } + Platform.runLater(() -> { + Image image = SwingFXUtils.toFXImage(bufferedImage, null); + imagePreview.visibleProperty().setValue(true); + imagePreview.setImage(image); + }); + } catch (IOException ex) { + Logger.getLogger(AttachmentsEditorController.class.getName()) + .log(Level.SEVERE, "Unable to load image file " + attachment.getFile().getAbsolutePath(), ex); + } + }); } } @@ -278,7 +297,7 @@ public void copySelectedAttachments() { dialog.setInitialDirectory(new File(System.getProperty("user.home"))); File targetFolder = dialog.showDialog(splitPane.getScene().getWindow()); JobManager.schedule("Save attachments job", (monitor) -> - selectedAttachments.stream().forEach(a -> copyAttachment(targetFolder, a))); + selectedAttachments.forEach(a -> copyAttachment(targetFolder, a))); } private void copyAttachment(File targetFolder, Attachment attachment) { @@ -297,10 +316,6 @@ public void addListSelectionChangeListener(ListChangeListener change listSelectionChangeListeners.add(changeListener); } - public void removeListSelectionChangeListener(ListChangeListener changeListener) { - listSelectionChangeListeners.remove(changeListener); - } - public void removeAttachments(List attachmentsToRemove) { attachments.removeAll(attachmentsToRemove); } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java index bb872ce73b..4fa8ca4194 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java @@ -54,8 +54,8 @@ public boolean canOpenResource(String resource) { /** * Support the launching of log entry table using resource - * logbook://? e.g. -resource - * logbook://?search=*Fault*Motor*&tag=operation + * {@literal logbook://? e.g. -resource} + * {@literal logbook://?search=*Fault*Motor*&tag=operation} */ @Override public AppInstance create(URI resource) { diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 57c50db3e2..20530f9750 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -256,7 +256,7 @@ public void search() { }, (msg, ex) -> { searchInProgress.set(false); - ExceptionDetailsErrorDialog.openError("Logbook Search Error", ex.getMessage(), ex); + ExceptionDetailsErrorDialog.openError("Logbook Search Error", ex); }); } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCellController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCellController.java index 175c62cf27..33e7c45aeb 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCellController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCellController.java @@ -2,6 +2,7 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -70,8 +71,10 @@ public class LogEntryCellController { @FXML private Pane detailsPane; + private SimpleBooleanProperty expanded = new SimpleBooleanProperty(true); + public LogEntryCellController() { List extensions = Arrays.asList(TablesExtension.create(), ImageAttributesExtension.create()); @@ -123,6 +126,7 @@ public void refresh() { logEntryId.setText(logEntry.getLogEntry().getId() != null ? logEntry.getLogEntry().getId().toString() : ""); level.setText(logEntry.getLogEntry().getLevel()); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryChangeHandlerImpl.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryChangeHandlerImpl.java new file mode 100644 index 0000000000..ee63cc3f06 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryChangeHandlerImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.logbook.olog.ui; + +import org.phoebus.framework.workbench.ApplicationService; +import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.LogEntryChangeHandler; + +/** + * Implements {@link LogEntryChangeHandler} to update the {@link LogEntryTableApp} UI. + */ +public class LogEntryChangeHandlerImpl implements LogEntryChangeHandler { + + /** + * Updates the {@link LogEntryTableApp} UI as needed. + * @param logEntry new or updated {@link LogEntry} + */ + public void logEntryChanged(LogEntry logEntry){ + LogEntryTableApp logEntryTableApp = ApplicationService.findApplication(LogEntryTableApp.NAME); + if(logEntryTableApp != null){ + logEntryTableApp.handleLogEntryChange(logEntry); + } + } +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java index 44d140b521..87e10f73f5 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java @@ -106,14 +106,16 @@ public void reply() { new LogEntryEditorStage(new OlogLog(), logEntryProperty.get(), null).show(); } + @FXML + public void newLogEntry(){ + // Show a new editor dialog. + new LogEntryEditorStage(new OlogLog(), null, null).show(); + } + public void setLogEntry(LogEntry logEntry) { if(logEntry == null){ currentViewProperty.set(EMPTY); } - // There is no need to update the view if it is already showing it. - else if(logEntryProperty.get() != null && logEntryProperty.get().getId().equals(logEntry.getId())){ - return; - } else{ logEntryProperty.set(logEntry); singleLogEntryDisplayController.setLogEntry(logEntry); @@ -127,4 +129,15 @@ else if(logEntryProperty.get() != null && logEntryProperty.get().getId().equals( public LogEntry getLogEntry() { return logEntryProperty.get(); } + + /** + * Updates the current {@link LogEntry} if it matches the passed argument. + * @param logEntry A log entry that has been updated by user and saved by service. + */ + public void updateLogEntry(LogEntry logEntry){ + // Log entry display may be "empty", i.e. logEntryProperty not set yet + if(!logEntryProperty.isNull().get() && logEntryProperty.get().getId() == logEntry.getId()){ + setLogEntry(logEntry); + } + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java index 2a3f525043..17f20a5a3c 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTable.java @@ -7,6 +7,7 @@ import org.phoebus.framework.spi.AppDescriptor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.logbook.LogClient; +import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; import org.phoebus.logbook.olog.ui.write.AttachmentsEditorController; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -107,4 +108,14 @@ public void save(final Memento memento) { OlogQueryManager.getInstance().save(); memento.setBoolean(HIDE_DETAILS, controller.getShowDetails()); } + + /** + * Handler for a {@link LogEntry} change, new or updated. + * A search is triggered to make sure the result list reflects the change, and + * the detail view controller is called to refresh, if applicable. + * @param logEntry A new or updated {@link LogEntry} + */ + public void logEntryChanged(LogEntry logEntry){ + controller.logEntryChanged(logEntry); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableApp.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableApp.java index b2ac1100a8..a7bb74de53 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableApp.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableApp.java @@ -4,10 +4,7 @@ import javafx.scene.image.Image; import org.phoebus.framework.spi.AppInstance; import org.phoebus.framework.spi.AppResourceDescriptor; -import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogFactory; -import org.phoebus.logbook.LogService; -import org.phoebus.logbook.LogbookPreferences; +import org.phoebus.logbook.*; import org.phoebus.ui.javafx.ImageCache; import java.net.URI; @@ -23,6 +20,8 @@ public class LogEntryTableApp implements AppResourceDescriptor { private static final String SUPPORTED_SCHEMA = "logbook"; private LogFactory logFactory; + private LogEntryTable logEntryTable; + @Override public void start() { logFactory = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory); @@ -44,7 +43,8 @@ public String getName() { @Override public AppInstance create() { - return new LogEntryTable(this); + logEntryTable = new LogEntryTable(this); + return logEntryTable; } @Override @@ -54,12 +54,12 @@ public boolean canOpenResource(String resource) { /** * Support the launching of log entry table view using resource - * logbook://? e.g. -resource - * logbook://?search=*Fault*Motor*&tag=operation + * {@literal logbook://? e.g. -resource} + * {@literal logbook://?search=*Fault*Motor*&tag=operation} */ @Override public AppInstance create(URI resource) { - LogEntryTable logEntryTable = new LogEntryTable(this); + logEntryTable = new LogEntryTable(this); logEntryTable.setResource(resource); return logEntryTable; } @@ -67,4 +67,13 @@ public AppInstance create(URI resource) { public LogClient getClient() { return logFactory.getLogClient(); } + + /** + * Handler for a {@link LogEntry} change, new or updated. The log table + * controller is called to refresh the UI as needed. + * @param logEntry A new or updated {@link LogEntry} + */ + public void handleLogEntryChange(LogEntry logEntry){ + logEntryTable.logEntryChanged(logEntry); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 7fa0b53975..3211a1458a 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -16,7 +16,6 @@ import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -42,11 +41,19 @@ import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.LogService; import org.phoebus.logbook.LogbookException; +import org.phoebus.logbook.LogbookPreferences; import org.phoebus.logbook.SearchResult; import org.phoebus.logbook.olog.ui.query.OlogQuery; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; +import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; +import org.phoebus.logbook.olog.ui.write.LogEntryUpdateStage; import org.phoebus.olog.es.api.model.LogGroupProperty; +import org.phoebus.olog.es.api.model.OlogLog; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -66,8 +73,6 @@ */ public class LogEntryTableViewController extends LogbookSearchController { - @FXML - private Button resize; @FXML private ComboBox query; @@ -96,6 +101,9 @@ public class LogEntryTableViewController extends LogbookSearchController { @FXML private TextField pageSizeTextField; + + @FXML + private Label openAdvancedSearchLabel; // Model private SearchResult searchResult; /** @@ -106,6 +114,8 @@ public class LogEntryTableViewController extends LogbookSearchController { private final SimpleBooleanProperty showDetails = new SimpleBooleanProperty(); + private final SimpleBooleanProperty advancedSearchVisibile = new SimpleBooleanProperty(false); + /** * Constructor. * @@ -123,6 +133,7 @@ public LogEntryTableViewController(LogClient logClient, OlogQueryManager ologQue private final SimpleIntegerProperty pageCountProperty = new SimpleIntegerProperty(0); private final OlogQueryManager ologQueryManager; private final ObservableList ologQueries = FXCollections.observableArrayList(); + private final SimpleBooleanProperty userHasSignedIn = new SimpleBooleanProperty(false); private final SearchParameters searchParameters; @@ -145,7 +156,7 @@ public void initialize() { groupSelectedEntries.disableProperty() .bind(Bindings.createBooleanBinding(() -> - selectedLogEntries.size() < 2, selectedLogEntries)); + selectedLogEntries.size() < 2 || userHasSignedIn.not().get(), selectedLogEntries, userHasSignedIn)); ContextMenu contextMenu = new ContextMenu(); MenuItem menuItemShowHideAll = new MenuItem(Messages.ShowHideDetails); @@ -155,7 +166,28 @@ public void initialize() { tableView.getItems().forEach(item -> item.setShowDetails(!item.isShowDetails().get())); }); - contextMenu.getItems().addAll(groupSelectedEntries, menuItemShowHideAll); + MenuItem menuItemNewLogEntry = new MenuItem(Messages.NewLogEntry); + menuItemNewLogEntry.acceleratorProperty().setValue(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); + menuItemNewLogEntry.setOnAction(ae -> new LogEntryEditorStage(new OlogLog(), null, null).show()); + + MenuItem menuItemUpdateLogEntry = new MenuItem(Messages.UpdateLogEntry); + menuItemUpdateLogEntry.visibleProperty().bind(Bindings.createBooleanBinding(()-> selectedLogEntries.size() == 1, selectedLogEntries)); + menuItemUpdateLogEntry.acceleratorProperty().setValue(new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN)); + menuItemUpdateLogEntry.setOnAction(ae -> new LogEntryUpdateStage(selectedLogEntries.get(0), null).show()); + + contextMenu.getItems().addAll(groupSelectedEntries, menuItemShowHideAll, menuItemNewLogEntry); + if (LogbookUIPreferences.log_entry_update_support) { + contextMenu.getItems().add(menuItemUpdateLogEntry); + } + contextMenu.setOnShowing(e -> { + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); + userHasSignedIn.set(scopedAuthenticationToken != null); + } catch (Exception ex) { + logger.log(Level.WARNING, "Secure Store file not found.", ex); + } + }); tableView.setContextMenu(contextMenu); @@ -163,7 +195,7 @@ public void initialize() { tableView.getColumns().clear(); tableView.setEditable(false); tableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null && tableView.getSelectionModel().getSelectedItems().size() == 1){ + if (newValue != null && tableView.getSelectionModel().getSelectedItems().size() == 1) { selectedLogEntry = newValue.getLogEntry(); logEntryDisplayController.setLogEntry(newValue.getLogEntry()); } @@ -177,7 +209,7 @@ public void initialize() { descriptionCol = new TableColumn<>(); descriptionCol.setMaxWidth(1f * Integer.MAX_VALUE * 100); - descriptionCol.setCellValueFactory(col -> new SimpleObjectProperty(col.getValue())); + descriptionCol.setCellValueFactory(col -> new SimpleObjectProperty<>(col.getValue())); descriptionCol.setCellFactory(col -> new TableCell<>() { private final Node graphic; private final PseudoClass childlessTopLevel = @@ -253,6 +285,13 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { query.getSelectionModel().select(ologQueries.get(0)); searchParameters.setQuery(ologQueries.get(0).getQuery()); + openAdvancedSearchLabel.setOnMouseClicked(e -> resize()); + + openAdvancedSearchLabel.textProperty() + .bind(Bindings.createStringBinding(() -> advancedSearchVisibile.get() ? + Messages.AdvancedSearchHide : Messages.AdvancedSearchOpen, + advancedSearchVisibile)); + search(); } @@ -263,33 +302,31 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { @FXML public void resize() { if (!moving.compareAndExchangeAcquire(false, true)) { - if (resize.getText().equals("<")) { + Duration cycleDuration = Duration.millis(400); + Timeline timeline; + if (advancedSearchVisibile.get()) { query.disableProperty().set(false); - Duration cycleDuration = Duration.millis(400); KeyValue kv = new KeyValue(advancedSearchViewController.getPane().minWidthProperty(), 0); KeyValue kv2 = new KeyValue(advancedSearchViewController.getPane().maxWidthProperty(), 0); - Timeline timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); - timeline.play(); + timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); timeline.setOnFinished(event -> { - resize.setText(">"); + advancedSearchVisibile.set(false); moving.set(false); - //advancedSearchViewController.updateSearchParametersFromInput(); search(); }); } else { searchParameters.setQuery(query.getEditor().getText()); - Duration cycleDuration = Duration.millis(400); double width = ViewSearchPane.getWidth() / 2.5; KeyValue kv = new KeyValue(advancedSearchViewController.getPane().minWidthProperty(), width); KeyValue kv2 = new KeyValue(advancedSearchViewController.getPane().prefWidthProperty(), width); - Timeline timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); - timeline.play(); + timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); timeline.setOnFinished(event -> { - resize.setText("<"); + advancedSearchVisibile.set(true); moving.set(false); query.disableProperty().set(true); }); } + timeline.play(); } } @@ -366,9 +403,9 @@ private void refresh() { tableView.setItems(logsList); // This will ensure that if an entry was selected, it stays selected after the list has been // updated from the search result, even if it is empty. - if(selectedLogEntry != null){ - for(TableViewListItem item : tableView.getItems()){ - if(item.getLogEntry().getId().equals(selectedLogEntry.getId())){ + if (selectedLogEntry != null) { + for (TableViewListItem item : tableView.getItems()) { + if (item.getLogEntry().getId().equals(selectedLogEntry.getId())) { Platform.runLater(() -> tableView.getSelectionModel().select(item)); break; } @@ -382,7 +419,8 @@ private void createLogEntryGroup() { selectedLogEntries.stream().map(LogEntry::getId).collect(Collectors.toList()); JobManager.schedule("Group log entries", monitor -> { try { - client.groupLogEntries(logEntryIds); + LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + logClient.groupLogEntries(logEntryIds); search(); } catch (LogbookException e) { logger.log(Level.INFO, "Unable to create log entry group from selection"); @@ -459,7 +497,7 @@ public OlogQuery fromString(String s) { /** * Wrapper class for a {@link LogEntry} and a flag indicating whether details of the - * log entry meta data should be rendered in the list view. + * log entry meta-data should be rendered in the list view. */ public static class TableViewListItem { private final SimpleBooleanProperty showDetails = new SimpleBooleanProperty(true); @@ -483,15 +521,26 @@ public void setShowDetails(boolean show) { } } - public void setShowDetails(boolean show){ + public void setShowDetails(boolean show) { showDetails.set(show); } - public boolean getShowDetails(){ + public boolean getShowDetails() { return showDetails.get(); } - public void showHelp(){ + public void showHelp() { new HelpViewer(LogbookUIPreferences.search_help).show(); } + + /** + * Handler for a {@link LogEntry} change, new or updated. + * A search is triggered to make sure the result list reflects the change, and + * the detail view controller is called to refresh, if applicable. + * @param logEntry + */ + public void logEntryChanged(LogEntry logEntry){ + search(); + logEntryDisplayController.updateLogEntry(logEntry); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 01230e94f2..ad62ca354c 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -27,7 +27,7 @@ public abstract class LogbookSearchController { private Job logbookSearchJob; protected LogClient client; - private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture runningTask; protected final SimpleBooleanProperty searchInProgress = new SimpleBooleanProperty(false); private static final int SEARCH_JOB_INTERVAL = 30; // seconds @@ -62,15 +62,13 @@ public void search(Map searchParams, final Consumer searchParams, final Consumer resultHandler) { cancelPeriodSearch(); - runningTask = executor.scheduleAtFixedRate(() -> { - logbookSearchJob = LogbookSearchJob.submit(this.client, - searchParams, - resultHandler, - (url, ex) -> { - searchInProgress.set(false); - cancelPeriodSearch(); - }); - }, 0, SEARCH_JOB_INTERVAL, TimeUnit.SECONDS); + runningTask = executor.scheduleAtFixedRate(() -> logbookSearchJob = LogbookSearchJob.submit(this.client, + searchParams, + resultHandler, + (url, ex) -> { + searchInProgress.set(false); + cancelPeriodSearch(); + }), SEARCH_JOB_INTERVAL, SEARCH_JOB_INTERVAL, TimeUnit.SECONDS); } @Deprecated diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookUIPreferences.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookUIPreferences.java index 680b01b27f..9769b5a394 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookUIPreferences.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookUIPreferences.java @@ -30,6 +30,7 @@ public class LogbookUIPreferences @Preference public static String markup_help; @Preference public static String web_client_root_URL; @Preference public static boolean log_entry_groups_support; + @Preference public static boolean log_entry_update_support; @Preference public static String[] hidden_properties; @Preference public static String log_entry_table_display_name; @Preference public static String log_entry_calendar_display_name; diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/MergedLogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/MergedLogEntryDisplayController.java index ef18994684..572d2263d5 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/MergedLogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/MergedLogEntryDisplayController.java @@ -133,6 +133,9 @@ private String createSeparator(LogEntry logEntry) { stringBuilder.append(SECONDS_FORMAT.format(logEntry.getCreatedDate())).append(", "); stringBuilder.append(logEntry.getOwner()).append(", "); stringBuilder.append(logEntry.getTitle()); + if(logEntry.getModifiedDate() != null && !logEntry.getModifiedDate().equals(logEntry.getCreatedDate())){ + stringBuilder.append(" *"); + } stringBuilder.append("

").append(logEntry.getId()).append("
"); if(logEntry.getAttachments().size() > 0){ stringBuilder.append("
 
"); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java index 5b50c24aba..271cf5859f 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java @@ -11,53 +11,50 @@ public class Messages { - public static String Add_Tooltip, - Apply, - Clear, - Clear_Tooltip, - CloseRequestHeader, - CloseRequestButtonContinue, - CloseRequestButtonDiscard, - DownloadSelected, - DownloadingAttachments, - EmbedImageDialogTitle, - File, - FileSave, - FileSaveFailed, - GroupingFailed, - GroupSelectedEntries, - Level, - Logbooks, - LogbooksSearchFailTitle, - LogbookServiceUnavailableTitle, - LogbookServiceHasNoLogbooks, - LogbooksTitle, - LogbooksTooltip, - NoAttachments, - NoClipboardContent, - Normal, - NoSearchResults, - PreviewOpenErrorBody, - PreviewOpenErrorTitle, - Remove_Tooltip, - SearchAvailableItems, - SelectFile, - SelectFolder, - ServiceConnectionErrorTitle, - ServiceConnectionErrorBody, - ShowHideDetails, - Tags, - TagsTitle, - TagsTooltip, - HtmlPreview, - HtmlPreviewToolTip; + public static String + AdvancedSearchOpen, + AdvancedSearchHide, + Apply, + ArchivedDownloadFailed, + ArchivedLaunchExternalAppFailed, + ArchivedNoEntriesFound, + ArchivedSaveFailed, + CloseRequestHeader, + CloseRequestButtonContinue, + CloseRequestButtonDiscard, + DownloadSelected, + DownloadingAttachments, + EmbedImageDialogTitle, + File, + FileSave, + FileSaveFailed, + FileTooLarge, + GroupingFailed, + GroupSelectedEntries, + Level, + LogbooksSearchFailTitle, + LogbookServiceUnavailableTitle, + LogbookServiceHasNoLogbooks, + NewLogEntry, + NoAttachments, + NoClipboardContent, + NoSearchResults, + PreviewOpenErrorBody, + PreviewOpenErrorTitle, + RequestTooLarge, + SelectFile, + SelectFolder, + ShowHideDetails, + SizeLimitsText, + UpdateLogEntry; + static { // initialize resource bundle NLS.initializeMessages(Messages.class); } - private Messages() + private Messages() { // prevent instantiation } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java index 1f3a3e662c..2ef0956433 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/SingleLogEntryDisplayController.java @@ -1,8 +1,11 @@ package org.phoebus.logbook.olog.ui; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TitledPane; @@ -10,17 +13,12 @@ import javafx.scene.image.ImageView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import org.phoebus.framework.jobs.JobManager; -import org.phoebus.logbook.Attachment; -import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogEntry; -import org.phoebus.logbook.Logbook; -import org.phoebus.logbook.LogbookException; -import org.phoebus.logbook.Property; -import org.phoebus.logbook.Tag; +import org.phoebus.logbook.*; import org.phoebus.olog.es.api.model.OlogAttachment; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.web.HyperLinkRedirectListener; @@ -52,6 +50,9 @@ public class SingleLogEntryDisplayController extends HtmlAwareController { @FXML WebView webView; + @FXML + private Node updatedIndicator; + private WebEngine webEngine; @FXML @@ -84,6 +85,8 @@ public class SingleLogEntryDisplayController extends HtmlAwareController { private LogEntry logEntry; private final LogClient logClient; + private final SimpleBooleanProperty logEntryUpdated = new SimpleBooleanProperty(); + public SingleLogEntryDisplayController(LogClient logClient) { super(logClient.getServiceUrl()); this.logClient = logClient; @@ -101,6 +104,11 @@ public void initialize() { webEngine = webView.getEngine(); // This will make links clicked in the WebView to open in default browser. webEngine.getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView)); + + updatedIndicator.visibleProperty().bind(logEntryUpdated); + updatedIndicator.setOnMouseEntered(me -> updatedIndicator.setCursor(Cursor.HAND)); + updatedIndicator.setOnMouseClicked(this::handle); + } public void setLogEntry(LogEntry entry) { @@ -154,6 +162,9 @@ public void setLogEntry(LogEntry entry) { logEntryId.setText(Long.toString(entry.getId())); level.setText(entry.getLevel()); + + logEntryUpdated.set(logEntry.getModifiedDate() != null && + !logEntry.getModifiedDate().equals(logEntry.getCreatedDate())); } /** @@ -205,4 +216,8 @@ private String getFullHtml(String commonmarkString) { return stringBuffer.toString(); } + + private void handle(MouseEvent me) { + new ArchivedLogEntriesManager(logClient).handle(webView, logEntry); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java index 863c261e2f..471eb626f9 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java @@ -19,13 +19,16 @@ package org.phoebus.logbook.olog.ui.write; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.embed.swing.SwingFXUtils; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.input.Clipboard; @@ -46,10 +49,8 @@ import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.text.MessageFormat; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -82,6 +83,30 @@ public class AttachmentsEditorController { private final LogEntry logEntry; + @FXML + private Label sizeLimitsLabel; + + @FXML + private Label sizesErrorLabel; + + private final SimpleStringProperty sizesErrorMessage = new SimpleStringProperty(); + + /** + * Max size in MB of a single attachment. Default is 15. + */ + private double maxFileSize = 15.0; + /** + * Max total size in MB for log entry, attachments included. Default is 50. + */ + private double maxRequestSize = 50.0; + + /** + * Counter updated when attachments are added/removed to keep track of total attachments size. + */ + private double attachedFilesSize; + + private final SimpleStringProperty sizeLimitsText = new SimpleStringProperty(); + /** * @param logEntry The log entry template potentially holding a set of attachments. Note * that files associated with these attachments are considered temporary and @@ -89,6 +114,12 @@ public class AttachmentsEditorController { */ public AttachmentsEditorController(LogEntry logEntry) { this.logEntry = logEntry; + // If the log entry has an attachment - e.g. log entry created from display or data browser - + // then add the file size to the total attachments size + Collection attachments = logEntry.getAttachments(); + if (attachments != null && !attachments.isEmpty()) { + attachments.forEach(a -> attachedFilesSize += getFileSize(a.getFile())); + } } @FXML @@ -108,10 +139,17 @@ public void initialize() { }); embedSelectedButton.disableProperty().bind(imageAttachmentSelected.not()); + + sizeLimitsLabel.textProperty().bind(sizeLimitsText); + sizeLimitsLabel.visibleProperty().bind(Bindings.createBooleanBinding(() -> sizeLimitsText.isNotEmpty().get(), sizeLimitsText)); + + sizesErrorLabel.textProperty().bind(sizesErrorMessage); + sizesErrorLabel.visibleProperty().bind(Bindings.createBooleanBinding(() -> sizesErrorMessage.isNotEmpty().get(), sizesErrorMessage)); } @FXML public void addFiles() { + Platform.runLater(() -> sizesErrorMessage.set(null)); final FileChooser addFilesDialog = new FileChooser(); addFilesDialog.setInitialDirectory(new File(System.getProperty("user.home"))); final List files = addFilesDialog.showOpenMultipleDialog(root.getParent().getScene().getWindow()); @@ -129,6 +167,7 @@ public void addCssWindow() { @FXML public void addClipboardContent() { + Platform.runLater(() -> sizesErrorMessage.set(null)); Clipboard clipboard = Clipboard.getSystemClipboard(); if (clipboard.hasFiles()) { addFiles(clipboard.getFiles()); @@ -150,12 +189,11 @@ public void removeFiles() { attachmentsToRemove.forEach(a -> { if (a.getContentType().startsWith("image") && a.getId() != null) { // Null check on id is needed as only embedded image attachments have a non-null id. String markup = textArea.getText(); - if(markup != null){ + if (markup != null) { String newMarkup = removeImageMarkup(markup, a.getId()); textArea.textProperty().set(newMarkup); } } - //attachments.remove(a); }); attachmentsViewController.removeAttachments(attachmentsToRemove); } @@ -189,7 +227,7 @@ public void embedSelected() { public void deleteTemporaryFiles() { JobManager.schedule("Delete temporary attachment files", monitor -> filesToDeleteAfterSubmit.forEach(f -> { logger.log(Level.INFO, "Deleting temporary attachment file " + f.getAbsolutePath()); - if(!f.delete()){ + if (!f.delete()) { logger.log(Level.WARNING, "Failed to delete temporary file " + f.getAbsolutePath()); } })); @@ -232,6 +270,10 @@ public void setTextArea(TextArea textArea) { } private void addFiles(List files) { + Platform.runLater(() -> sizesErrorMessage.set(null)); + if (!checkFileSizes(files)) { + return; + } MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap(); for (File file : files) { OlogAttachment ologAttachment = new OlogAttachment(); @@ -252,10 +294,20 @@ private void addImage(Image image) { } private void addImage(Image image, String id) { + Platform.runLater(() -> sizesErrorMessage.set(null)); try { File imageFile = new File(System.getProperty("java.io.tmpdir"), id + ".png"); imageFile.deleteOnExit(); ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", imageFile); + double fileSize = getFileSize(imageFile); + if (fileSize > maxFileSize) { + showFileSizeExceedsLimit(imageFile); + return; + } + if (attachedFilesSize + fileSize > maxRequestSize) { + showTotalSizeExceedsLimit(); + return; + } OlogAttachment ologAttachment = new OlogAttachment(id); ologAttachment.setContentType("image"); ologAttachment.setFile(imageFile); @@ -268,7 +320,49 @@ private void addImage(Image image, String id) { } } - public List getAttachments(){ + public List getAttachments() { return attachmentsViewController.getAttachments(); } + + public void setSizeLimits(String maxFileSize, String maxRequestSize) { + this.maxFileSize = Double.parseDouble(maxFileSize); + this.maxRequestSize = Double.parseDouble(maxRequestSize); + Platform.runLater(() -> sizeLimitsText.set(MessageFormat.format(Messages.SizeLimitsText, maxFileSize, maxRequestSize))); + } + + /** + * Checks if files can be accepted with respect to max file size and max total (request) size. + * + * @param files List of files to be checked + * @return false when first file exceeding the max file size limit is encountered, otherwise true. + */ + private boolean checkFileSizes(List files) { + double totalSize = 0; + for (File file : files) { + double fileSize = getFileSize(file); + if (fileSize > maxFileSize) { + showFileSizeExceedsLimit(file); + return false; + } + totalSize += fileSize; + if ((attachedFilesSize + totalSize) > maxRequestSize) { + showTotalSizeExceedsLimit(); + return false; + } + } + attachedFilesSize += totalSize; + return true; + } + + private double getFileSize(File file) { + return 1.0 * file.length() / 1024 / 1024; + } + + private void showFileSizeExceedsLimit(File file) { + Platform.runLater(() -> sizesErrorMessage.set(MessageFormat.format(Messages.FileTooLarge, file.getName()))); + } + + private void showTotalSizeExceedsLimit() { + Platform.runLater(() -> sizesErrorMessage.set(Messages.RequestTooLarge)); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index 13e7924084..664ad61ef5 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -19,6 +19,8 @@ package org.phoebus.logbook.olog.ui.write; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -29,32 +31,14 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Side; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.CustomMenuItem; -import javafx.scene.control.Label; -import javafx.scene.control.MenuItem; -import javafx.scene.control.PasswordField; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextField; -import javafx.scene.control.ToggleButton; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; -import org.phoebus.logbook.LogClient; -import org.phoebus.logbook.LogEntry; -import org.phoebus.logbook.LogFactory; -import org.phoebus.logbook.LogService; -import org.phoebus.logbook.Logbook; -import org.phoebus.logbook.LogbookException; -import org.phoebus.logbook.LogbookPreferences; -import org.phoebus.logbook.Tag; +import org.phoebus.logbook.*; import org.phoebus.logbook.olog.ui.HelpViewer; import org.phoebus.logbook.olog.ui.LogbookUIPreferences; import org.phoebus.logbook.olog.ui.PreviewViewer; @@ -62,6 +46,7 @@ import org.phoebus.olog.es.api.OlogProperties; import org.phoebus.olog.es.api.model.OlogLog; import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.security.tokens.SimpleAuthenticationToken; import org.phoebus.ui.dialog.ListSelectionPopOver; @@ -69,11 +54,7 @@ import org.phoebus.util.time.TimestampFormats; import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -188,6 +169,10 @@ public class LogEntryEditorController { */ private String originalTitle = ""; + /** + * Version of remote service + */ + private String serverVersion; public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, LogEntryCompletionHandler logEntryCompletionHandler) { this.replyTo = inReplyTo; @@ -394,7 +379,7 @@ public void initialize() { }); // Note: logbooks and tags are retrieved asynchronously from service - setupLogbooksAndTags(); + getServerSideStaticData(); } /** @@ -459,7 +444,7 @@ public void submit() { try { SecureStore store = new SecureStore(); ScopedAuthenticationToken scopedAuthenticationToken = - new ScopedAuthenticationToken(LogService.AUTHENTICATION_SCOPE, usernameProperty.get(), passwordProperty.get()); + new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, usernameProperty.get(), passwordProperty.get()); store.setScopedAuthentication(scopedAuthenticationToken); } catch (Exception ex) { logger.log(Level.WARNING, "Secure Store file not found.", ex); @@ -576,8 +561,9 @@ public List getSelectedTags() { /** * Retrieves logbooks and tags from service and populates all the data structures that depend * on the result. The call to the remote service is asynchronous. + * This method also retrieves server side information, e.g. max file size and max request size. */ - private void setupLogbooksAndTags() { + private void getServerSideStaticData() { JobManager.schedule("Fetch Logbooks and Tags", monitor -> { LogClient logClient = @@ -645,6 +631,16 @@ private void setupLogbooksAndTags() { logbooksPopOver.setAvailable(availableLogbooksAsStringList, selectedLogbooks); logbooksPopOver.setSelected(selectedLogbooks); + String serverInfo = logClient.serviceInfo(); + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonNode jsonNode = objectMapper.readTree(serverInfo); + serverVersion = jsonNode.get("version").asText(); + attachmentsEditorController.setSizeLimits(jsonNode.get("serverConfig").get("maxFileSize").asText(), + jsonNode.get("serverConfig").get("maxRequestSize").asText()); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to get or parse response from server info request", e); + } }); } @@ -655,7 +651,7 @@ public void fetchStoredUserCredentials() { // Get the SecureStore. Retrieve username and password. try { SecureStore store = new SecureStore(); - ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(LogService.AUTHENTICATION_SCOPE); + ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); // Could be accessed from JavaFX Application Thread when updating, so synchronize. synchronized (usernameProperty) { usernameProperty.set(scopedAuthenticationToken == null ? "" : scopedAuthenticationToken.getUsername()); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java new file mode 100644 index 0000000000..eb7ce751f2 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.logbook.olog.ui.write; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.geometry.Side; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.framework.selection.SelectionService; +import org.phoebus.logbook.*; +import org.phoebus.logbook.olog.ui.*; +import org.phoebus.logbook.olog.ui.menu.SendToLogBookApp; +import org.phoebus.olog.es.api.OlogProperties; +import org.phoebus.olog.es.api.model.OlogAttachment; +import org.phoebus.olog.es.api.model.OlogLog; +import org.phoebus.security.store.SecureStore; +import org.phoebus.security.tokens.AuthenticationScope; +import org.phoebus.security.tokens.ScopedAuthenticationToken; +import org.phoebus.security.tokens.SimpleAuthenticationToken; +import org.phoebus.ui.dialog.ListSelectionPopOver; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.util.time.TimestampFormats; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.*; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Controller for the {@link LogEntryUpdateStage}. + */ +public class LogEntryUpdateController { + + private final LogEntryCompletionHandler completionHandler; + + private final Logger logger = Logger.getLogger(LogEntryUpdateController.class.getName()); + + @FXML + private VBox editorPane; + @FXML + private VBox errorPane; + @FXML + private Button submitButton; + @FXML + private Button cancelButton; + @FXML + private ProgressIndicator progressIndicator; + @FXML + private Label completionMessageLabel; + + @SuppressWarnings("unused") + @FXML + private AttachmentsViewController attachmentsViewController; + @SuppressWarnings("unused") + @FXML + private LogPropertiesEditorController logPropertiesEditorController; + + @FXML + private Label userFieldLabel; + @FXML + private Label passwordFieldLabel; + @FXML + private TextField userField; + @FXML + private PasswordField passwordField; + @FXML + private Label levelLabel; + @FXML + private TextField dateField; + @FXML + private ComboBox levelSelector; + @FXML + private Label titleLabel; + @FXML + private TextField titleField; + @FXML + private TextArea textArea; + @FXML + private Button addLogbooks; + @FXML + private Button addTags; + @FXML + private ToggleButton logbooksDropdownButton; + @FXML + private ToggleButton tagsDropdownButton; + @FXML + private Label logbooksLabel; + @FXML + private TextField logbooksSelection; + @FXML + private TextField tagsSelection; + + private final ContextMenu logbookDropDown = new ContextMenu(); + private final ContextMenu tagDropDown = new ContextMenu(); + + private final ObservableList selectedLogbooks = FXCollections.observableArrayList(); + private final ObservableList selectedTags = FXCollections.observableArrayList(); + + private ObservableList availableLogbooksAsStringList; + private ObservableList availableTagsAsStringList; + private Collection availableLogbooks; + private Collection availableTags; + + private ListSelectionPopOver tagsPopOver; + private ListSelectionPopOver logbooksPopOver; + + private final ObservableList availableLevels = FXCollections.observableArrayList(); + private final SimpleStringProperty titleProperty = new SimpleStringProperty(); + private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(); + private final SimpleStringProperty selectedLevelProperty = new SimpleStringProperty(); + private final SimpleStringProperty usernameProperty = new SimpleStringProperty(); + private final SimpleStringProperty passwordProperty = new SimpleStringProperty(); + + private final LogEntry logEntry; + + private final SimpleBooleanProperty updateCredentials = new SimpleBooleanProperty(); + private final ReadOnlyBooleanProperty updateCredentialsProperty; + private final SimpleBooleanProperty inputValid = new SimpleBooleanProperty(false); + + private final LogFactory logFactory; + + private final SimpleBooleanProperty submissionInProgress = + new SimpleBooleanProperty(false); + + private final Long logId; + + /** + * Indicates if user has started editing. Only title and body are used to define dirty state. + */ + private boolean isDirty = false; + + /** + * Used to determine if a log entry is dirty. For a new log entry, comparison is made to empty string, for + * the reply case the original title is copied to this field. + */ + private String originalTitle = ""; + + + public LogEntryUpdateController(LogEntry logEntry, LogEntryCompletionHandler logEntryCompletionHandler) { + this.logId = logEntry.getId(); + this.completionHandler = logEntryCompletionHandler; + this.logFactory = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory); + updateCredentialsProperty = updateCredentials; + this.logEntry = logEntry; + } + + @FXML + public void initialize() { + + // This could be configured in the fxml, but then these UI components would not be visible + // in Scene Builder. + completionMessageLabel.textProperty().set(""); + progressIndicator.visibleProperty().bind(submissionInProgress); + + // Remote log service not reachable, so show error pane. + if (!checkConnectivity()) { + errorPane.visibleProperty().set(true); + editorPane.disableProperty().set(true); + return; + } + + submitButton.disableProperty().bind(Bindings.createBooleanBinding(() -> + !inputValid.get() || submissionInProgress.get(), + inputValid, submissionInProgress)); + completionMessageLabel.visibleProperty() + .bind(Bindings.createBooleanBinding(() -> completionMessageLabel.textProperty().isNotEmpty().get() && !submissionInProgress.get(), + completionMessageLabel.textProperty(), submissionInProgress)); + + cancelButton.disableProperty().bind(submissionInProgress); + + userField.textProperty().bindBidirectional(usernameProperty); + userField.textProperty().addListener((changeListener, oldVal, newVal) -> + { + if (newVal.trim().isEmpty()) + userFieldLabel.setTextFill(Color.RED); + else + userFieldLabel.setTextFill(Color.BLACK); + }); + + passwordField.textProperty().bindBidirectional(passwordProperty); + passwordField.textProperty().addListener((changeListener, oldVal, newVal) -> + { + if (newVal.trim().isEmpty()) + passwordFieldLabel.setTextFill(Color.RED); + else + passwordFieldLabel.setTextFill(Color.BLACK); + }); + + updateCredentialsProperty.addListener((changeListener, oldVal, newVal) -> + { + // This call back should be running on a background thread. Perform contents on JavaFX application thread. + Platform.runLater(() -> + { + userField.setText(usernameProperty.get()); + passwordField.setText(passwordProperty.get()); + + // Put focus on first required field that is empty. + if (userField.getText().isEmpty()) { + userField.requestFocus(); + } else if (passwordField.getText().isEmpty()) { + passwordField.requestFocus(); + } else if (titleField.getText() == null || titleField.getText().isEmpty()) { + titleField.requestFocus(); + } else { + textArea.requestFocus(); + } + }); + }); + if (LogbookUIPreferences.save_credentials) { + fetchStoredUserCredentials(); + } + + levelLabel.setText(LogbookUIPreferences.level_field_name); + // Sites may wish to define a different meaning and name for the "level" field. + OlogProperties ologProperties = new OlogProperties(); + String[] levelList = ologProperties.getPreferenceValue("levels").split(","); + availableLevels.addAll(Arrays.asList(levelList)); + levelSelector.setItems(availableLevels); + selectedLevelProperty.set(logEntry.getLevel() != null ? logEntry.getLevel() : availableLevels.get(0)); + + levelSelector.getSelectionModel().select(selectedLevelProperty.get()); + + dateField.setText(TimestampFormats.DATE_FORMAT.format(Instant.now())); + + titleField.textProperty().bindBidirectional(titleProperty); + titleProperty.addListener((changeListener, oldVal, newVal) -> + { + if (newVal.trim().isEmpty()) { + titleLabel.setTextFill(Color.RED); + } else { + titleLabel.setTextFill(Color.BLACK); + } + if (!newVal.equals(originalTitle)) { + isDirty = true; + } + }); + titleProperty.set(logEntry.getTitle()); + + textArea.textProperty().bindBidirectional(descriptionProperty); + descriptionProperty.set(logEntry.getDescription() != null ? logEntry.getDescription() : ""); + descriptionProperty.addListener((observable, oldValue, newValue) -> isDirty = true); + + Image tagIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/add_tag.png"); + Image logbookIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/logbook-16.png"); + Image downIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/down_triangle.png"); + + addLogbooks.setGraphic(new ImageView(logbookIcon)); + addTags.setGraphic(new ImageView(tagIcon)); + logbooksDropdownButton.setGraphic(new ImageView(downIcon)); + tagsDropdownButton.setGraphic(new ImageView(downIcon)); + + logbooksSelection.textProperty().addListener((changeListener, oldVal, newVal) -> + { + if (newVal.trim().isEmpty()) + logbooksLabel.setTextFill(Color.RED); + else + logbooksLabel.setTextFill(Color.BLACK); + }); + + logbooksSelection.textProperty().bind(Bindings.createStringBinding(() -> { + if (selectedLogbooks.isEmpty()) { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + selectedLogbooks.forEach(l -> stringBuilder.append(l).append(", ")); + String text = stringBuilder.toString(); + return text.substring(0, text.length() - 2); + }, selectedLogbooks)); + + tagsSelection.textProperty().bind(Bindings.createStringBinding(() -> { + if (selectedTags.isEmpty()) { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + selectedTags.forEach(l -> stringBuilder.append(l).append(", ")); + String text = stringBuilder.toString(); + return text.substring(0, text.length() - 2); + }, selectedTags)); + + logbooksDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> + { + if (!newVal && !tagDropDown.isShowing() && !logbookDropDown.isShowing()) + logbooksDropdownButton.setSelected(false); + }); + + tagsDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> + { + if (!newVal && !tagDropDown.isShowing() && !tagDropDown.isShowing()) + tagsDropdownButton.setSelected(false); + }); + + inputValid.bind(Bindings.createBooleanBinding(() -> titleProperty.get() != null && !titleProperty.get().isEmpty() && + usernameProperty.get() != null && !usernameProperty.get().isEmpty() && + passwordProperty.get() != null && !passwordProperty.get().isEmpty() && + !selectedLogbooks.isEmpty(), + titleProperty, usernameProperty, passwordProperty, selectedLogbooks)); + + tagsPopOver = ListSelectionPopOver.create( + (tags, popOver) -> { + setSelectedTags(tags, selectedTags); + if (popOver.isShowing()) { + popOver.hide(); + } + }, + (tags, popOver) -> popOver.hide() + ); + logbooksPopOver = ListSelectionPopOver.create( + (logbooks, popOver) -> { + setSelectedLogbooks(logbooks, selectedLogbooks); + if (popOver.isShowing()) { + popOver.hide(); + } + }, + (logbooks, popOver) -> popOver.hide() + ); + + selectedTags.addListener((ListChangeListener) change -> { + List newSelection = new ArrayList<>(change.getList()); + tagsPopOver.setAvailable(availableTagsAsStringList, newSelection); + tagsPopOver.setSelected(newSelection); + }); + + selectedLogbooks.addListener((ListChangeListener) change -> { + List newSelection = new ArrayList<>(change.getList()); + logbooksPopOver.setAvailable(availableLogbooksAsStringList, newSelection); + logbooksPopOver.setSelected(newSelection); + }); + + // Note: logbooks and tags are retrieved asynchronously from service + setupLogbooksAndTags(); + retrieveAttachments(); + } + + /** + * Handler for Cancel button. Note that any selections in the {@link SelectionService} are + * cleared to prevent next launch of {@link SendToLogBookApp} + * to pick them up. + */ + @FXML + public void cancel() { + // Need to clear selections. + SelectionService.getInstance().clearSelection(""); + ((LogEntryUpdateStage) cancelButton.getScene().getWindow()).handleCloseEditor(isDirty, editorPane); + } + + @FXML + public void showHelp() { + new HelpViewer(LogbookUIPreferences.markup_help).show(); + } + + /** + * Handler for HTML preview button + */ + @FXML + public void showHtmlPreview() { + new PreviewViewer(getDescription(), attachmentsViewController.getAttachments()).show(); + } + + + @FXML + public void submit() { + + submissionInProgress.set(true); + + JobManager.schedule("Submit Log Entry Update", monitor -> { + OlogLog ologLog = new OlogLog(); + ologLog.setId(logId); + ologLog.setTitle(getTitle()); + ologLog.setDescription(getDescription()); + ologLog.setLevel(selectedLevelProperty.get()); + ologLog.setLogbooks(getSelectedLogbooks()); + ologLog.setTags(getSelectedTags()); +// ologLog.setAttachments(attachmentsViewController.getAttachments()); + ologLog.setProperties(logPropertiesEditorController.getProperties()); + + LogClient logClient = + logFactory.getLogClient(new SimpleAuthenticationToken(usernameProperty.get(), passwordProperty.get())); + LogEntry result; + try { + result = logClient.update(ologLog); + // Not dirty any more... + isDirty = false; + if (result != null) { + if (completionHandler != null) { + completionHandler.handleResult(result); + } + // Set username and password in secure store if submission of log entry completes successfully + if (LogbookUIPreferences.save_credentials) { + // Get the SecureStore. Store username and password. + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = + new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, usernameProperty.get(), passwordProperty.get()); + store.setScopedAuthentication(scopedAuthenticationToken); + } catch (Exception ex) { + logger.log(Level.WARNING, "Secure Store file not found.", ex); + } + } + // This will close the editor + Platform.runLater(this::cancel); + } + } catch (LogbookException e) { + logger.log(Level.WARNING, "Unable to submit log entry", e); + Platform.runLater(() -> { + if (e.getCause() != null && e.getCause().getMessage() != null) { + completionMessageLabel.textProperty().setValue(e.getCause().getMessage()); + } else if (e.getMessage() != null) { + completionMessageLabel.textProperty().setValue(e.getMessage()); + } else { + completionMessageLabel.textProperty().setValue(org.phoebus.logbook.Messages.SubmissionFailed); + } + }); + } + submissionInProgress.set(false); + }); + } + + @FXML + public void setLevel() { + selectedLevelProperty.set(levelSelector.getSelectionModel().getSelectedItem()); + } + + public String getTitle() { + return titleProperty.get(); + } + + public String getDescription() { + return descriptionProperty.get(); + } + + @FXML + public void addLogbooks() { + logbooksPopOver.show(addLogbooks); + } + + private void addSelectedLogbook(String logbookName) { + selectedLogbooks.add(logbookName); + updateDropDown(logbookDropDown, logbookName, true); + } + + private void removeSelectedLogbook(String logbookName) { + selectedLogbooks.remove(logbookName); + updateDropDown(logbookDropDown, logbookName, false); + } + + private void setSelectedLogbooks(List proposedLogbooks, List existingLogbooks) { + setSelected(proposedLogbooks, existingLogbooks, this::addSelectedLogbook, this::removeSelectedLogbook); + } + + private void setSelected(List proposed, List existing, Consumer addFunction, Consumer removeFunction) { + List addedTags = proposed.stream() + .filter(tag -> !existing.contains(tag)) + .collect(Collectors.toList()); + List removedTags = existing.stream() + .filter(tag -> !proposed.contains(tag)) + .collect(Collectors.toList()); + addedTags.forEach(addFunction); + removedTags.forEach(removeFunction); + } + + @FXML + public void selectLogbooks() { + if (logbooksDropdownButton.isSelected()) { + logbookDropDown.show(logbooksSelection, Side.BOTTOM, 0, 0); + } else { + logbookDropDown.hide(); + } + } + + @FXML + public void addTags() { + tagsPopOver.show(addTags); + } + + private void addSelectedTag(String tagName) { + selectedTags.add(tagName); + updateDropDown(tagDropDown, tagName, true); + } + + private void removeSelectedTag(String tagName) { + selectedTags.remove(tagName); + updateDropDown(tagDropDown, tagName, false); + } + + private void setSelectedTags(List proposedTags, List existingTags) { + setSelected(proposedTags, existingTags, this::addSelectedTag, this::removeSelectedTag); + } + + @FXML + public void selectTags() { + if (tagsDropdownButton.isSelected()) { + tagDropDown.show(tagsSelection, Side.BOTTOM, 0, 0); + } else { + tagDropDown.hide(); + } + } + + public List getSelectedLogbooks() { + return availableLogbooks.stream().filter(l -> selectedLogbooks.contains(l.getName())).collect(Collectors.toList()); + } + + public List getSelectedTags() { + return availableTags.stream().filter(t -> selectedTags.contains(t.getName())).collect(Collectors.toList()); + } + + /** + * Retrieves logbooks and tags from service and populates all the data structures that depend + * on the result. The call to the remote service is asynchronous. + */ + private void setupLogbooksAndTags() { + JobManager.schedule("Fetch Logbooks and Tags", monitor -> + { + LogClient logClient = + LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + availableLogbooks = logClient.listLogbooks(); + availableLogbooksAsStringList = + FXCollections.observableArrayList(availableLogbooks.stream().map(Logbook::getName).collect(Collectors.toList())); + Collections.sort(availableLogbooksAsStringList); + + List preSelectedLogbooks = + logEntry.getLogbooks().stream().map(Logbook::getName).collect(Collectors.toList()); + List defaultLogbooks = Arrays.asList(LogbookUIPreferences.default_logbooks); + availableLogbooksAsStringList.forEach(logbook -> { + CheckBox checkBox = new CheckBox(logbook); + CustomMenuItem newLogbook = new CustomMenuItem(checkBox); + newLogbook.setHideOnClick(false); + checkBox.setOnAction(e -> { + CheckBox source = (CheckBox) e.getSource(); + String text = source.getText(); + if (source.isSelected()) { + selectedLogbooks.add(text); + } else { + selectedLogbooks.remove(text); + } + }); + if (!preSelectedLogbooks.isEmpty() && preSelectedLogbooks.contains(logbook)) { + checkBox.setSelected(preSelectedLogbooks.contains(logbook)); + selectedLogbooks.add(logbook); + } else if (defaultLogbooks.contains(logbook) && selectedLogbooks.isEmpty()) { + checkBox.setSelected(defaultLogbooks.contains(logbook)); + selectedLogbooks.add(logbook); + } + logbookDropDown.getItems().add(newLogbook); + }); + + availableTags = logClient.listTags(); + availableTagsAsStringList = + FXCollections.observableArrayList(availableTags.stream().map(Tag::getName).collect(Collectors.toList())); + Collections.sort(availableLogbooksAsStringList); + + List preSelectedTags = + logEntry.getTags().stream().map(Tag::getName).collect(Collectors.toList()); + availableTagsAsStringList.forEach(tag -> { + CheckBox checkBox = new CheckBox(tag); + CustomMenuItem newTag = new CustomMenuItem(checkBox); + newTag.setHideOnClick(false); + checkBox.setOnAction(e -> { + CheckBox source = (CheckBox) e.getSource(); + String text = source.getText(); + if (source.isSelected()) { + selectedTags.add(text); + } else { + selectedTags.remove(text); + } + }); + checkBox.setSelected(preSelectedTags.contains(tag)); + if (preSelectedTags.contains(tag)) { + selectedTags.add(tag); + } + tagDropDown.getItems().add(newTag); + }); + + tagsPopOver.setAvailable(availableTagsAsStringList, selectedTags); + tagsPopOver.setSelected(selectedTags); + logbooksPopOver.setAvailable(availableLogbooksAsStringList, selectedLogbooks); + logbooksPopOver.setSelected(selectedLogbooks); + + }); + } + + private void retrieveAttachments() { + JobManager.schedule("Fetch attachment data", monitor -> { + + LogClient logClient = + LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + Collection attachments = logEntry.getAttachments().stream() + .filter((attachment) -> attachment.getName() != null && !attachment.getName().isEmpty()) + .map((attachment) -> { + OlogAttachment fileAttachment = new OlogAttachment(); + fileAttachment.setContentType(attachment.getContentType()); + fileAttachment.setThumbnail(false); + fileAttachment.setFileName(attachment.getName()); + try { + Path temp = Files.createTempFile("phoebus", attachment.getName()); + Files.copy(logClient.getAttachment(logEntry.getId(), attachment.getName()), temp, StandardCopyOption.REPLACE_EXISTING); + fileAttachment.setFile(temp.toFile()); + temp.toFile().deleteOnExit(); + } catch (LogbookException | IOException e) { + Logger.getLogger(SingleLogEntryDisplayController.class.getName()) + .log(Level.WARNING, "Failed to retrieve attachment " + fileAttachment.getFileName(), e); + } + return fileAttachment; + }).collect(Collectors.toList()); + // Update UI + Platform.runLater(() -> { + attachmentsViewController.setAttachments(attachments); + }); + }); + } + + public void fetchStoredUserCredentials() { + // Perform file IO on background thread. + JobManager.schedule("Access Secure Store", monitor -> + { + // Get the SecureStore. Retrieve username and password. + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); + // Could be accessed from JavaFX Application Thread when updating, so synchronize. + synchronized (usernameProperty) { + usernameProperty.set(scopedAuthenticationToken == null ? "" : scopedAuthenticationToken.getUsername()); + } + synchronized (passwordProperty) { + passwordProperty.set(scopedAuthenticationToken == null ? "" : scopedAuthenticationToken.getPassword()); + } + // Let anyone listening know that their credentials are now out of date. + updateCredentials.set(true); + } catch (Exception ex) { + logger.log(Level.WARNING, "Secure Store file not found.", ex); + } + }); + } + + /** + * Updates the logbooks or tags context menu to reflect the state of selected logbooks and tags. + * + * @param contextMenu The context menu to update + * @param itemName The logbook or tag name identifying to a context menu item + * @param itemSelected Indicates whether to select or deselect. + */ + private void updateDropDown(ContextMenu contextMenu, String itemName, boolean itemSelected) { + for (MenuItem menuItem : contextMenu.getItems()) { + CustomMenuItem custom = (CustomMenuItem) menuItem; + CheckBox check = (CheckBox) custom.getContent(); + if (check.getText().equals(itemName)) { + check.setSelected(itemSelected); + break; + } + } + } + + public boolean isDirty() { + return isDirty; + } + + /** + * Checks connectivity to remote service by querying the info end-point. If connection fails, + * connectionError property is set to true, which should set opacity of the editor pane, and + * set visibility of error pane. + */ + private boolean checkConnectivity() { + LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + try { + logClient.serviceInfo(); + return true; + } catch (Exception e) { + Logger.getLogger(SendToLogBookApp.class.getName()).warning("Failed to query logbook service, it may be off-line."); + return false; + } + } +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java new file mode 100644 index 0000000000..9e69bb0899 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.logbook.olog.ui.write; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.phoebus.framework.nls.NLS; +import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.olog.ui.AttachmentsViewController; +import org.phoebus.logbook.olog.ui.Messages; +import org.phoebus.ui.dialog.DialogHelper; + +import java.util.Collection; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LogEntryUpdateStage extends Stage { + private LogEntryUpdateController logEntryUpdateController; + + /** + * A stand-alone window containing components needed to create a logbook entry. + * + * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. + */ + public LogEntryUpdateStage(LogEntry logEntry) { + this(logEntry, null); + } + + /** + * A stand-alone window containing components needed to create a logbook entry. + * + * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. + * @param completionHandler A completion handler called when service call completes. + */ + public LogEntryUpdateStage(LogEntry logEntry, LogEntryCompletionHandler completionHandler) { + + initModality(Modality.WINDOW_MODAL); + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("LogEntryUpdate.fxml"), resourceBundle); + fxmlLoader.setControllerFactory(clazz -> { + try { + if (clazz.isAssignableFrom(LogEntryUpdateController.class)) { + logEntryUpdateController = (LogEntryUpdateController) clazz.getConstructor(LogEntry.class, LogEntryCompletionHandler.class) + .newInstance(logEntry, completionHandler); + return logEntryUpdateController; + } else if (clazz.isAssignableFrom(AttachmentsEditorController.class)) { + return clazz.getConstructor(LogEntry.class).newInstance(logEntry); + } else if (clazz.isAssignableFrom(AttachmentsViewController.class)) { + return clazz.getConstructor().newInstance(); + } else if (clazz.isAssignableFrom(LogPropertiesEditorController.class)) { + return clazz.getConstructor(Collection.class).newInstance(logEntry.getProperties()); + } + } catch (Exception e) { + Logger.getLogger(LogEntryUpdateStage.class.getName()).log(Level.SEVERE, "Failed to construct controller for log editor UI", e); + } + return null; + }); + + try { + fxmlLoader.load(); + } catch ( + Exception exception) { + Logger.getLogger(LogEntryUpdateStage.class.getName()).log(Level.WARNING, "Unable to load fxml for log entry editor UI", exception); + } + + Scene scene = new Scene(fxmlLoader.getRoot()); + setScene(scene); + scene.getWindow().setOnCloseRequest(we -> { + we.consume(); + handleCloseEditor(logEntryUpdateController.isDirty(), fxmlLoader.getRoot()); + }); + } + + /** + * Helper method to show a confirmation dialog if user closes/cancels log entry editor with "dirty" data. + * + * @param entryIsDirty Indicates if the log entry content (title or body, or both) have been changed. + * @param parent The {@link Node} used to determine the position of the dialog. + */ + public void handleCloseEditor(boolean entryIsDirty, Node parent) { + if (entryIsDirty) { + ButtonType discardChanges = new ButtonType(Messages.CloseRequestButtonDiscard, ButtonBar.ButtonData.OK_DONE); + ButtonType continueEditing = new ButtonType(Messages.CloseRequestButtonContinue, ButtonBar.ButtonData.CANCEL_CLOSE); + Alert alert = new Alert(AlertType.CONFIRMATION, + null, + discardChanges, + continueEditing); + alert.setHeaderText(Messages.CloseRequestHeader); + DialogHelper.positionDialog(alert, parent, -200, -300); + if (alert.showAndWait().get().getButtonData().equals(ButtonBar.ButtonData.OK_DONE)) { + close(); + } + } else { + close(); + } + } +} diff --git a/app/logbook/olog/ui/src/main/resources/META-INF/services/org.phoebus.logbook.LogEntryChangeHandler b/app/logbook/olog/ui/src/main/resources/META-INF/services/org.phoebus.logbook.LogEntryChangeHandler new file mode 100644 index 0000000000..5a00a9c1e7 --- /dev/null +++ b/app/logbook/olog/ui/src/main/resources/META-INF/services/org.phoebus.logbook.LogEntryChangeHandler @@ -0,0 +1,20 @@ +# +# Copyright (C) 2023 European Spallation Source ERIC. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# + +org.phoebus.logbook.olog.ui.LogEntryChangeHandlerImpl \ No newline at end of file diff --git a/app/logbook/olog/ui/src/main/resources/detail_log_webview.css b/app/logbook/olog/ui/src/main/resources/detail_log_webview.css index ba3ccf9f8b..535b6a6026 100644 --- a/app/logbook/olog/ui/src/main/resources/detail_log_webview.css +++ b/app/logbook/olog/ui/src/main/resources/detail_log_webview.css @@ -85,3 +85,12 @@ background: float: right; padding-right: 10px } + +.updated-indicator { + font-size: 20px; + text-decoration: none; +} + +a{ + text-decoration: none; +} diff --git a/app/logbook/olog/ui/src/main/resources/log_olog_ui_preferences.properties b/app/logbook/olog/ui/src/main/resources/log_olog_ui_preferences.properties index 3d8562a0f3..527edeeb2f 100644 --- a/app/logbook/olog/ui/src/main/resources/log_olog_ui_preferences.properties +++ b/app/logbook/olog/ui/src/main/resources/log_olog_ui_preferences.properties @@ -30,6 +30,11 @@ web_client_root_URL= # groups will not be shown. log_entry_groups_support=false +# Log entry update support. If set to false user will not be able to update log entries +# , and consequently UI elements and views related to updating log entry and viewing log history +# will not be displayed. +log_entry_update_support=true + # Comma separated list of "hidden" properties. For instance, properties that serve internal # business logic, but should not be rendered in the properties view. hidden_properties=Log Entry Group diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/AdvancedSearchView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/AdvancedSearchView.fxml index b994cdf45f..c1e8ceb922 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/AdvancedSearchView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/AdvancedSearchView.fxml @@ -3,6 +3,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsView.fxml new file mode 100644 index 0000000000..d855662974 --- /dev/null +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsView.fxml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableView.fxml new file mode 100644 index 0000000000..bc59f31280 --- /dev/null +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableView.fxml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotView.fxml index d0fdde295d..cf6ddfb202 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotView.fxml @@ -5,99 +5,22 @@ This is the contents of the top portion of a snapshot tab. Since tabs are added save-and-restore UI, this fxml does not make use of TabPane and Tab elements. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +
+ +
+
+ + +
diff --git a/app/save-and-restore/app/src/main/resources/style.css b/app/save-and-restore/app/src/main/resources/save-and-restore-style.css similarity index 76% rename from app/save-and-restore/app/src/main/resources/style.css rename to app/save-and-restore/app/src/main/resources/save-and-restore-style.css index 05f69aba3c..f01406d2cf 100644 --- a/app/save-and-restore/app/src/main/resources/style.css +++ b/app/save-and-restore/app/src/main/resources/save-and-restore-style.css @@ -11,48 +11,53 @@ -fx-text-fill: -fx-selection-bar-text; } -.table-view .column-header.toplevel .label { +.table-view .column-header.toplevel > .label { -fx-text-fill: #595959FF; -fx-font-weight: 700; -fx-font-family: 'Liberation Sans'; -fx-font-size: 18px; } -.table-view .column-header.second-level .label { +.table-view .column-header.second-level > .label { -fx-text-fill: #595959FF; -fx-font-weight: 700; -fx-font-family: 'Liberation Sans'; } -.table-cell.divider{ - -fx-background-color: #f2f2f2FF; -} - -.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .diff-cell, -.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:selected, -.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:hover:selected, -.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:focused:selected:hover { - -fx-background-color: -fx-selection-bar; - -fx-text-fill: -fx-selection-bar-text; +.snapshot-table-left-aligned.column-header > .label{ + -fx-text-fill: #595959FF; + -fx-font-weight: 700; + -fx-font-family: 'Liberation Sans'; + -fx-alignment: center-left; + -fx-padding: 0 0 0 2; } -.tree-table-view .column-header.toplevel .label { - -fx-text-fill: #595959FF; - -fx-font-weight: 700; - -fx-font-family: 'Liberation Sans'; - -fx-font-size: 18px; +.snapshot-table-right-aligned.column-header > .label{ + -fx-text-fill: #595959FF; + -fx-font-weight: 700; + -fx-font-family: 'Liberation Sans'; + -fx-alignment: center-right; + -fx-padding: 0 2 0 0; } -.tree-table-view .column-header.second-level .label { +.snapshot-table-centered.column-header > .label{ -fx-text-fill: #595959FF; -fx-font-weight: 700; -fx-font-family: 'Liberation Sans'; } -.tree-table-cell.divider{ +.table-cell.divider{ -fx-background-color: #f2f2f2FF; } +.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected > .diff-cell, +.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:selected, +.tree-table-view:cell-selection > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:hover:selected, +.tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled > .diff-cell:focused:selected:hover { + -fx-background-color: -fx-selection-bar; + -fx-text-fill: -fx-selection-bar-text; +} + .separator:vertical .line { -fx-border-style: dashed; -fx-padding: 0.0em 0.0em 0.0em 0.0em; @@ -65,18 +70,10 @@ -fx-prompt-text-fill: transparent; } -.chart-plot-background { - -fx-background-color: #ffffff; -} - .tab-pane .tab-header-area .tab-header-background { -fx-opacity: 0; } -.check-box-table-cell-disabled > .check-box > .box { - -fx-background-color: #E0E0E0; -} - .pv-name-check-box-disabled > .box { -fx-background-color: #E0E0E0; } @@ -118,6 +115,12 @@ -fx-font-family: "Liberation Mono"; } +.id-column.table-cell{ + -fx-font-family: "Liberation Mono"; + -fx-alignment: CENTER-RIGHT; + -fx-padding: 0 4 2 0; +} + .filter-match { -fx-background-color: rgb(242, 242, 242); } @@ -126,3 +129,13 @@ .tree-cell:selected{ -fx-background-color: rgb(0, 150, 201); } + +.column-header.rightAlignedTableColumnHeader > .label { + -fx-alignment: center-right; + -fx-padding: 0 2 0 0; +} + +.column-header.leftAlignedTableColumnHeader > .label { + -fx-alignment: center-left; + -fx-padding: 0 0 0 2; +} \ No newline at end of file diff --git a/app/save-and-restore/app/src/main/resources/save_and_restore_client_preferences.properties b/app/save-and-restore/app/src/main/resources/save_and_restore_client_preferences.properties new file mode 100644 index 0000000000..56138ca424 --- /dev/null +++ b/app/save-and-restore/app/src/main/resources/save_and_restore_client_preferences.properties @@ -0,0 +1,12 @@ +# ------------------------------------------------------ +# Package org.phoebus.applications.saveandrestore.client +# ------------------------------------------------------ + +# The URL to the save-and-restore service +jmasar.service.url=http://localhost:8080/save-restore + +# Read timeout (in ms) used by the Jersey client +httpClient.readTimeout=5000 + +# Connect timeout in (ms) used by the Jersey client +httpClient.connectTimeout=5000 diff --git a/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties b/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties index 94a80013be..35c193d8b3 100644 --- a/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties +++ b/app/save-and-restore/app/src/main/resources/save_and_restore_preferences.properties @@ -5,14 +5,6 @@ # Sort snapshots in reverse order of created time. Last item comes first. sortSnapshotsTimeReversed=false -# Specify hierarchy parser class to enable TreeTableView in snapshot -# Hierarchy parser class should be in ui/snapshot/hierarchyparser -# RegexHierarchyParser is provided for convenience. Use , as separator for each regex pattern. -# First matched pattern is used to create its hierarchy. -tree_tableview_enable=true -treeTableView.hierarchyParser=RegexHierarchyParser -regexHierarchyParser.regexList=(\\w+)_(\\w+):(\\w+)_(\\w+):(.*),(\\w+)_(\\w+):(\\w+)_(.*),(\\w+)_(\\w+):(.*),(\\w+):(.*) - # Read timeout (in ms) when taking snapshot readTimeout=5000 @@ -21,3 +13,8 @@ search_result_page_size=30 # Default save-and-restore search query. Used unless a saved query is located. default_search_query=tags=golden + +# If declared add a date automatically in the name of the snapshot "Take Snapshot" +#default_snapshot_name_date_format=yyyy-MM-dd HH:mm:ss + +this.is.a.test=MUSIGNY \ No newline at end of file diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/SafeMultiplyTest.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/SafeMultiplyTest.java index d8691da496..1b2b6a2eef 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/SafeMultiplyTest.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/SafeMultiplyTest.java @@ -64,7 +64,8 @@ import org.epics.vtype.VUShortArray; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.phoebus.applications.saveandrestore.common.Utilities; +import org.phoebus.applications.saveandrestore.ui.Utilities; + import java.math.BigInteger; import java.time.Instant; diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/DragNDropUtilTest.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/DragNDropUtilTest.java new file mode 100644 index 0000000000..458d0b19e3 --- /dev/null +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/DragNDropUtilTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.applications.saveandrestore.ui; + +import javafx.scene.input.TransferMode; +import org.junit.jupiter.api.Test; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class DragNDropUtilTest { + + private final Node folderTargetNode = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.FOLDER).build(); + private final Node compositeSnapshotTargetNode = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); + private final Node snapshotTargetNode = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.SNAPSHOT).build(); + private final Node configurationTargetNode = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.CONFIGURATION).build(); + + + @Test + public void testMayDropFoldersOnFolder(){ + Node f1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.FOLDER).build(); + Node f2 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.FOLDER).build(); + + assertTrue(DragNDropUtil.mayDrop(TransferMode.MOVE, folderTargetNode, Arrays.asList(f1, f2))); + assertTrue(DragNDropUtil.mayDrop(TransferMode.COPY, folderTargetNode, Arrays.asList(f1, f2))); + } + + @Test + public void testMayNotDropFoldersOnNonFolder(){ + Node f1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.FOLDER).build(); + Node f2 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.FOLDER).build(); + + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, compositeSnapshotTargetNode, Arrays.asList(f1, f2))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, compositeSnapshotTargetNode, Arrays.asList(f1, f2))); + + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, configurationTargetNode, Arrays.asList(f1, f2))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, configurationTargetNode, Arrays.asList(f1, f2))); + + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, snapshotTargetNode, Arrays.asList(f1, f2))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, snapshotTargetNode, Arrays.asList(f1, f2))); + } + + @Test + public void testMayDropConfigurationOnFolder(){ + Node c1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.CONFIGURATION).build(); + assertTrue(DragNDropUtil.mayDrop(TransferMode.MOVE, folderTargetNode, Collections.singletonList(c1))); + assertTrue(DragNDropUtil.mayDrop(TransferMode.COPY, folderTargetNode, Collections.singletonList(c1))); + } + + @Test + public void testMayNotDropConfigurationOnNonFolder(){ + Node c1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.CONFIGURATION).build(); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, configurationTargetNode, Collections.singletonList(c1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, configurationTargetNode, Collections.singletonList(c1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, snapshotTargetNode, Collections.singletonList(c1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, snapshotTargetNode, Collections.singletonList(c1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, compositeSnapshotTargetNode, Collections.singletonList(c1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, compositeSnapshotTargetNode, Collections.singletonList(c1))); + } + + @Test + public void testMayDropSnapshotsAndCompositeSnapshotsOnCompositeSnapshot(){ + Node cs1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); + Node s1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.SNAPSHOT).build(); + assertTrue(DragNDropUtil.mayDrop(TransferMode.MOVE, compositeSnapshotTargetNode, Arrays.asList(cs1, s1))); + assertTrue(DragNDropUtil.mayDrop(TransferMode.COPY, compositeSnapshotTargetNode, Arrays.asList(cs1, s1))); + } + + @Test + public void testMayNotDropSnapshotOnNonCompositeSnapshot(){ + Node s1 = Node.builder().uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.SNAPSHOT).build(); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, configurationTargetNode, Collections.singletonList(s1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, configurationTargetNode, Collections.singletonList(s1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, folderTargetNode, Collections.singletonList(s1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, folderTargetNode, Collections.singletonList(s1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.MOVE, snapshotTargetNode, Collections.singletonList(s1))); + assertFalse(DragNDropUtil.mayDrop(TransferMode.COPY, snapshotTargetNode, Collections.singletonList(s1))); + } +} diff --git a/app/save-and-restore/common/src/test/java/org/phoebus/applications/saceandrestore/common/UtilitiesTest.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/UtilitiesTest.java similarity index 85% rename from app/save-and-restore/common/src/test/java/org/phoebus/applications/saceandrestore/common/UtilitiesTest.java rename to app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/UtilitiesTest.java index f0051b9040..9109c45b78 100644 --- a/app/save-and-restore/common/src/test/java/org/phoebus/applications/saceandrestore/common/UtilitiesTest.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/UtilitiesTest.java @@ -16,71 +16,22 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package org.phoebus.applications.saceandrestore.common; - -import org.epics.util.array.ArrayBoolean; -import org.epics.util.array.ArrayByte; -import org.epics.util.array.ArrayDouble; -import org.epics.util.array.ArrayFloat; -import org.epics.util.array.ArrayInteger; -import org.epics.util.array.ArrayLong; -import org.epics.util.array.ArrayShort; -import org.epics.util.array.ArrayUByte; -import org.epics.util.array.ArrayUInteger; -import org.epics.util.array.ArrayULong; -import org.epics.util.array.ArrayUShort; -import org.epics.util.array.ListBoolean; -import org.epics.util.array.ListLong; -import org.epics.vtype.Alarm; -import org.epics.vtype.AlarmSeverity; -import org.epics.vtype.AlarmStatus; -import org.epics.vtype.Display; -import org.epics.vtype.EnumDisplay; -import org.epics.vtype.Time; -import org.epics.vtype.VBoolean; -import org.epics.vtype.VBooleanArray; -import org.epics.vtype.VByte; -import org.epics.vtype.VByteArray; -import org.epics.vtype.VDouble; -import org.epics.vtype.VDoubleArray; -import org.epics.vtype.VEnum; -import org.epics.vtype.VEnumArray; -import org.epics.vtype.VFloat; -import org.epics.vtype.VFloatArray; -import org.epics.vtype.VInt; -import org.epics.vtype.VIntArray; -import org.epics.vtype.VLong; -import org.epics.vtype.VLongArray; -import org.epics.vtype.VShort; -import org.epics.vtype.VShortArray; -import org.epics.vtype.VString; -import org.epics.vtype.VStringArray; -import org.epics.vtype.VType; -import org.epics.vtype.VUByte; -import org.epics.vtype.VUByteArray; -import org.epics.vtype.VUInt; -import org.epics.vtype.VUIntArray; -import org.epics.vtype.VULong; -import org.epics.vtype.VULongArray; -import org.epics.vtype.VUShort; -import org.epics.vtype.VUShortArray; +package org.phoebus.applications.saveandrestore.ui; + +import org.epics.pva.data.*; +import org.epics.util.array.*; +import org.epics.vtype.*; import org.junit.jupiter.api.Test; -import org.phoebus.applications.saveandrestore.common.Threshold; -import org.phoebus.applications.saveandrestore.common.Utilities; -import org.phoebus.applications.saveandrestore.common.VDisconnectedData; +import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.core.vtypes.VTypeHelper; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; public class UtilitiesTest { @@ -268,7 +219,7 @@ public void testValueFromString() { Utilities.valueFromString("invalid", val); fail("Should throw exception"); } catch (IllegalArgumentException e) { - e.printStackTrace(); + // Ignore } val = VBoolean.of(false, alarm, time); @@ -334,12 +285,12 @@ public void testValueFromString() { val = VLongArray.of(ArrayLong.of(1, 2, 3, 4, 5), alarm, time, display); result = Utilities.valueFromString("1, 2, 3, 4, 5", val); assertTrue(result instanceof VLongArray); - assertTrue(((VLongArray) result).getData() instanceof ListLong); + assertNotNull(((VLongArray) result).getData()); val = VBooleanArray.of(ArrayBoolean.of(true, true, false, true), alarm, time); result = Utilities.valueFromString("[1, 1, 0, 2]", val); assertTrue(result instanceof VBooleanArray); - assertTrue(((VBooleanArray) result).getData() instanceof ListBoolean); + assertNotNull(((VBooleanArray) result).getData()); val = VDisconnectedData.INSTANCE; result = Utilities.valueFromString("5", val); @@ -355,99 +306,6 @@ public void testValueFromString() { assertEquals("string", ((VString) result).getValue()); } - /** - * Tests {@link Utilities#toRawValue(VType)}. - */ - @Test - public void testToRawValue() { - Alarm alarm = Alarm.none(); - Display display = Display.none(); - Time time = Time.now(); - - assertNull(Utilities.toRawValue(null)); - - VType val = VDouble.of(5d, alarm, time, display); - Object d = Utilities.toRawValue(val); - assertTrue(d instanceof Double); - assertEquals(5.0, d); - - val = VFloat.of(5f, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Float); - assertEquals(5.0f, d); - - val = VLong.of(5L, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Long); - assertEquals(5L, d); - - val = VInt.of(5, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Integer); - assertEquals(5, d); - - val = VShort.of((short) 5, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Short); - assertEquals((short) 5, d); - - val = VByte.of((byte) 5, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Byte); - assertEquals((byte) 5, d); - - val = VEnum.of(1, EnumDisplay.of("first", "second", "third"), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String); - assertEquals("second", d); - - val = VEnum.of(1, EnumDisplay.of("", "", ""), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String); - assertEquals("1", d); - - val = VEnum.of(1, EnumDisplay.of("a", "", ""), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String); - assertEquals("", d); - - val = VString.of("third", alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String); - assertEquals("third", d); - - ArrayDouble arrayDouble = ArrayDouble.of(1, 2, 3, 4, 5); - val = VDoubleArray.of(arrayDouble, alarm, time, display); - d = Utilities.toRawValue(val); - assertTrue(d instanceof double[]); - for (int i = 0; i < ((double[]) d).length; i++) { - assertEquals(arrayDouble.getDouble(i), ((double[]) d)[i], 0); - } - - val = VStringArray.of(Arrays.asList("a", "b", "c"), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String[]); - - val = VBooleanArray.of(ArrayBoolean.of(true, false, true), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof boolean[]); - assertTrue(((boolean[]) d)[0]); - assertFalse(((boolean[]) d)[1]); - - val = VEnumArray.of(ArrayInteger.of(0, 1, 2, 3, 4), EnumDisplay.of("a", "b", "c", "d", "e"), alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof String[]); - assertEquals("a", ((String[]) d)[0]); - assertEquals("e", ((String[]) d)[4]); - - val = VBoolean.of(true, alarm, time); - d = Utilities.toRawValue(val); - assertTrue(d instanceof Boolean); - assertTrue(((Boolean) d)); - - assertNull(Utilities.toRawValue(VDisconnectedData.INSTANCE)); - } - /** * Tests {@link Utilities#valueToCompareString(VType, VType, Optional)}. The test doesn't cover all possible @@ -498,7 +356,7 @@ public void testValueToCompareString() { val1 = VDouble.of(5d, alarm, time, display); VType val2 = VDouble.of(6d, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1.0", result.getString()); + assertEquals("5 Δ-1.0", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -506,7 +364,7 @@ public void testValueToCompareString() { val1 = VDouble.of(15d, alarm, time, display); val2 = VDouble.of(6d, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9.0", result.getString()); + assertEquals("15 Δ+9.0", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -514,7 +372,7 @@ public void testValueToCompareString() { val1 = VFloat.of(15f, alarm, time, display); val2 = VFloat.of(6f, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9.0", result.getString()); + assertEquals("15 Δ+9.0", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -522,7 +380,7 @@ public void testValueToCompareString() { val1 = VDouble.of(6d, alarm, time, display); val2 = VDouble.of(6d, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u03940.0", result.getString()); + assertEquals("6 Δ0.0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); assertEquals(0, result.getAbsoluteDelta(), 0.0); @@ -530,7 +388,7 @@ public void testValueToCompareString() { val1 = VFloat.of(5f, alarm, time, display); val2 = VFloat.of(6f, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1.0", result.getString()); + assertEquals("5 Δ-1.0", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -538,7 +396,7 @@ public void testValueToCompareString() { val1 = VFloat.of(5f, alarm, time, display); val2 = VFloat.of(6f, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("5 \u0394-1.0", result.getString()); + assertEquals("5 Δ-1.0", result.getString()); assertTrue(result.getValuesEqual() < 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -546,7 +404,7 @@ public void testValueToCompareString() { val1 = VFloat.of(5f, alarm, time, display); val2 = VFloat.of(6f, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("5 \u0394-1.0", result.getString()); + assertEquals("5 Δ-1.0", result.getString()); assertTrue(result.getValuesEqual() < 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -555,14 +413,14 @@ public void testValueToCompareString() { val1 = VLong.of(15L, alarm, time, display); val2 = VDouble.of(6d, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); val1 = VLong.of(15L, alarm, time, display); val2 = VDouble.of(6d, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -570,7 +428,7 @@ public void testValueToCompareString() { val1 = VULong.of(15L, alarm, time, display); val2 = VULong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -578,7 +436,7 @@ public void testValueToCompareString() { val1 = VULong.of(5L, alarm, time, display); val2 = VULong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -586,7 +444,7 @@ public void testValueToCompareString() { val1 = VUInt.of(15, alarm, time, display); val2 = VUInt.of(6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -594,7 +452,7 @@ public void testValueToCompareString() { val1 = VUInt.of(15, alarm, time, display); val2 = VUInt.of(6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -602,7 +460,7 @@ public void testValueToCompareString() { val1 = VInt.of(15, alarm, time, display); val2 = VInt.of(6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("15 \u0394+9", result.getString()); + assertEquals("15 Δ+9", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -610,7 +468,7 @@ public void testValueToCompareString() { val1 = VDouble.of(15d, alarm, time, display); val2 = VLong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("15 \u0394+9.0", result.getString()); + assertEquals("15 Δ+9.0", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -618,7 +476,7 @@ public void testValueToCompareString() { val1 = VDouble.of(15d, alarm, time, display); val2 = VLong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("15 \u0394+9.0", result.getString()); + assertEquals("15 Δ+9.0", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(9, result.getAbsoluteDelta(), 0.0); @@ -626,7 +484,7 @@ public void testValueToCompareString() { val1 = VDouble.of(15d, alarm, time, display); val2 = VLong.of(15L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("15 \u03940.0", result.getString()); + assertEquals("15 Δ0.0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); assertEquals(0, result.getAbsoluteDelta(), 0.0); @@ -659,7 +517,7 @@ public void testValueToCompareString() { val1 = VLong.of(6L, alarm, time, display); val2 = VLong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u03940", result.getString()); + assertEquals("6 Δ0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); assertEquals(0, result.getAbsoluteDelta(), 0.0); @@ -667,7 +525,7 @@ public void testValueToCompareString() { val1 = VLong.of(5L, alarm, time, display); val2 = VLong.of(6L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -675,7 +533,7 @@ public void testValueToCompareString() { val1 = VLong.of(6L, alarm, time, display); val2 = VLong.of(5L, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -684,7 +542,7 @@ public void testValueToCompareString() { val1 = VInt.of(6, alarm, time, display); val2 = VInt.of(6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u03940", result.getString()); + assertEquals("6 Δ0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); assertEquals(0, result.getAbsoluteDelta(), 0.0); @@ -692,7 +550,7 @@ public void testValueToCompareString() { val1 = VInt.of(5, alarm, time, display); val2 = VInt.of(6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -700,7 +558,7 @@ public void testValueToCompareString() { val1 = VInt.of(6, alarm, time, display); val2 = VInt.of(5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); @@ -708,14 +566,14 @@ public void testValueToCompareString() { val1 = VShort.of((short) 6, alarm, time, display); val2 = VShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u03940", result.getString()); + assertEquals("6 Δ0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); val1 = VShort.of((short) 5, alarm, time, display); val2 = VShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -723,7 +581,7 @@ public void testValueToCompareString() { val1 = VShort.of((short) 6, alarm, time, display); val2 = VShort.of((short) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -731,7 +589,7 @@ public void testValueToCompareString() { val1 = VUShort.of((short) 6, alarm, time, display); val2 = VUShort.of((short) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -739,7 +597,7 @@ public void testValueToCompareString() { val1 = VUShort.of((short) 6, alarm, time, display); val2 = VUShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u03940", result.getString()); + assertEquals("6 Δ0", result.getString()); assertEquals(0, result.getValuesEqual()); assertTrue(result.isWithinThreshold()); assertEquals(0, result.getAbsoluteDelta(), 0.0); @@ -747,7 +605,7 @@ public void testValueToCompareString() { val1 = VUShort.of((short) 5, alarm, time, display); val2 = VUShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -755,7 +613,7 @@ public void testValueToCompareString() { val1 = VShort.of((short) 5, alarm, time, display); val2 = VShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -763,7 +621,7 @@ public void testValueToCompareString() { val1 = VUShort.of((short) 5, alarm, time, display); val2 = VUShort.of((short) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -798,7 +656,7 @@ public void testValueToCompareString() { val1 = VByte.of((byte) 5, alarm, time, display); val2 = VByte.of((byte) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -806,7 +664,7 @@ public void testValueToCompareString() { val1 = VByte.of((byte) 6, alarm, time, display); val2 = VByte.of((byte) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -814,7 +672,7 @@ public void testValueToCompareString() { val1 = VByte.of((byte) 6, alarm, time, display); val2 = VByte.of((byte) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -822,7 +680,7 @@ public void testValueToCompareString() { val1 = VUByte.of((byte) 5, alarm, time, display); val2 = VUByte.of((byte) 6, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("5 \u0394-1", result.getString()); + assertEquals("5 Δ-1", result.getString()); assertTrue(result.getValuesEqual() < 0); assertTrue(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -830,14 +688,14 @@ public void testValueToCompareString() { val1 = VUByte.of((byte) 6, alarm, time, display); val2 = VUByte.of((byte) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, threshold); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertTrue(result.isWithinThreshold()); val1 = VUByte.of((byte) 6, alarm, time, display); val2 = VUByte.of((byte) 5, alarm, time, display); result = Utilities.valueToCompareString(val1, val2, Optional.empty()); - assertEquals("6 \u0394+1", result.getString()); + assertEquals("6 Δ+1", result.getString()); assertTrue(result.getValuesEqual() > 0); assertFalse(result.isWithinThreshold()); assertEquals(1, result.getAbsoluteDelta(), 0.0); @@ -1555,4 +1413,157 @@ public void testAreValuesEqual() { val2 = VStringArray.of(Arrays.asList("a", "b", "c"), alarm, time); assertTrue(Utilities.areValuesEqual(val1, val2, Optional.empty())); } + + @Test + public void testAreVTablesEqual() { + + VTable vTable1 = VTable.of(List.of(Integer.TYPE, Double.TYPE), + List.of("a", "b"), + List.of(ArrayInteger.of(1, 2, 3), ArrayDouble.of(7.0, 8.0, 9.0))); + VTable vTable2 = VTable.of(List.of(Integer.TYPE, Double.TYPE), + List.of("a", "b"), + List.of(ArrayInteger.of(1, 2, 3), ArrayDouble.of(7.0, 8.0, 9.0))); + + assertTrue(Utilities.areValuesEqual(vTable1, vTable2, Optional.empty())); + + vTable2 = VTable.of(List.of(Integer.TYPE, Integer.TYPE), + List.of("a", "b"), + List.of(ArrayInteger.of(1, 2, 3), ArrayInteger.of(7, 8, 9))); + + assertFalse(Utilities.areValuesEqual(vTable1, vTable2, Optional.empty())); + + vTable2 = VTable.of(List.of(Integer.TYPE, Double.TYPE), + List.of("a", "b"), + List.of(ArrayInteger.of(1, 2, 3), ArrayDouble.of(7.0, 8.0))); + + assertFalse(Utilities.areValuesEqual(vTable1, vTable2, Optional.empty())); + + vTable1 = VTable.of(List.of(String.class, Double.TYPE), + List.of("a", "b"), + List.of(List.of("AAA", "BBB", "CCC"), ArrayDouble.of(7.0, 8.0, 9.0))); + + vTable2 = VTable.of(List.of(String.class, Double.TYPE), + List.of("a", "b"), + List.of(List.of("AAA", "BBB", "CCC"), ArrayDouble.of(7.0, 8.0, 9.0))); + + assertTrue(Utilities.areValuesEqual(vTable1, vTable2, Optional.empty())); + } + + @Test + public void testAreVTypeArraysEqual() { + assertTrue(Utilities.areVTypeArraysEqual(Integer.TYPE, ArrayInteger.of(1, 2, 3), ArrayInteger.of(1, 2, 3))); + assertTrue(Utilities.areVTypeArraysEqual(Integer.TYPE, ArrayUInteger.of(1, 2, 3), ArrayUInteger.of(1, 2, 3))); + assertTrue(Utilities.areVTypeArraysEqual(Long.TYPE, ArrayLong.of(1L, 2L, 3L), ArrayLong.of(1L, 2L, 3L))); + assertTrue(Utilities.areVTypeArraysEqual(Long.TYPE, ArrayULong.of(1L, 2L, 3L), ArrayULong.of(1L, 2L, 3L))); + assertTrue(Utilities.areVTypeArraysEqual(Double.TYPE, ArrayDouble.of(7.0, 8.0, 9.0), ArrayDouble.of(7.0, 8.0, 9.0))); + assertTrue(Utilities.areVTypeArraysEqual(Float.TYPE, ArrayFloat.of(7.0f, 8.0f, 9.0f), ArrayFloat.of(7.0f, 8.0f, 9.0f))); + assertTrue(Utilities.areVTypeArraysEqual(Short.TYPE, ArrayShort.of((short) 7.0, (short) 8.0, (short) 9.0), ArrayShort.of((short) 7.0, (short) 8.0, (short) 9.0))); + assertTrue(Utilities.areVTypeArraysEqual(Short.TYPE, ArrayUShort.of((short) 7.0, (short) 8.0, (short) 9.0), ArrayUShort.of((short) 7.0, (short) 8.0, (short) 9.0))); + assertTrue(Utilities.areVTypeArraysEqual(Byte.TYPE, ArrayByte.of((byte) 7, (byte) 8, (byte) 9), ArrayByte.of((byte) 7, (byte) 8, (byte) 9))); + assertTrue(Utilities.areVTypeArraysEqual(Byte.TYPE, ArrayUByte.of((byte) 7, (byte) 8, (byte) 9), ArrayUByte.of((byte) 7, (byte) 8, (byte) 9))); + assertTrue(Utilities.areVTypeArraysEqual(Boolean.TYPE, ArrayBoolean.of(true, false), ArrayBoolean.of(true, false))); + + assertTrue(Utilities.areVTypeArraysEqual(String.class, List.of("AAA", "BBB", "CCC"), List.of("AAA", "BBB", "CCC"))); + + assertFalse(Utilities.areVTypeArraysEqual(Byte.TYPE, ArrayShort.of((byte) 7, (byte) 8, (byte) 9), ArrayShort.of((byte) 7, (byte) 8, (byte) 10))); + } + + @Test + public void testToPVArrayType() { + + boolean[] bools = new boolean[]{true, false, true}; + Object converted = VTypeHelper.toPVArrayType("bools", ArrayBoolean.of(bools)); + assertInstanceOf(PVABoolArray.class, converted); + assertEquals(3, ((PVABoolArray) converted).get().length); + assertArrayEquals(bools, ((PVABoolArray) converted).get()); + assertEquals("bools", ((PVABoolArray) converted).getName()); + + byte[] bytes = new byte[]{(byte) -1, (byte) 2, (byte) 3}; + converted = VTypeHelper.toPVArrayType("bytes", ArrayByte.of(bytes)); + assertInstanceOf(PVAByteArray.class, converted); + assertEquals(3, ((PVAByteArray) converted).get().length); + assertArrayEquals(bytes, ((PVAByteArray) converted).get()); + assertEquals("bytes", ((PVAByteArray) converted).getName()); + assertFalse(((PVAByteArray) converted).isUnsigned()); + + bytes = new byte[]{(byte) 1, (byte) 2, (byte) 3}; + converted = VTypeHelper.toPVArrayType("ubytes", ArrayUByte.of(bytes)); + assertInstanceOf(PVAByteArray.class, converted); + assertEquals(3, ((PVAByteArray) converted).get().length); + assertArrayEquals(bytes, ((PVAByteArray) converted).get()); + assertEquals("ubytes", ((PVAByteArray) converted).getName()); + assertTrue(((PVAByteArray) converted).isUnsigned()); + + short[] shorts = new short[]{(short) -1, (short) 2, (short) 3}; + converted = VTypeHelper.toPVArrayType("shorts", ArrayShort.of(shorts)); + assertInstanceOf(PVAShortArray.class, converted); + assertEquals(3, ((PVAShortArray) converted).get().length); + assertArrayEquals(shorts, ((PVAShortArray) converted).get()); + assertEquals("shorts", ((PVAShortArray) converted).getName()); + assertFalse(((PVAShortArray) converted).isUnsigned()); + + shorts = new short[]{(short) 1, (short) 2, (short) 3}; + converted = VTypeHelper.toPVArrayType("ushorts", ArrayUShort.of(shorts)); + assertInstanceOf(PVAShortArray.class, converted); + assertEquals(3, ((PVAShortArray) converted).get().length); + assertArrayEquals(shorts, ((PVAShortArray) converted).get()); + assertEquals("ushorts", ((PVAShortArray) converted).getName()); + assertTrue(((PVAShortArray) converted).isUnsigned()); + + int[] ints = new int[]{-1, 2, 3}; + converted = VTypeHelper.toPVArrayType("ints", ArrayInteger.of(ints)); + assertInstanceOf(PVAIntArray.class, converted); + assertEquals(3, ((PVAIntArray) converted).get().length); + assertArrayEquals(ints, ((PVAIntArray) converted).get()); + assertEquals("ints", ((PVAIntArray) converted).getName()); + assertFalse(((PVAIntArray) converted).isUnsigned()); + + ints = new int[]{1, 2, 3}; + converted = VTypeHelper.toPVArrayType("uints", ArrayUInteger.of(ints)); + assertInstanceOf(PVAIntArray.class, converted); + assertEquals(3, ((PVAIntArray) converted).get().length); + assertArrayEquals(ints, ((PVAIntArray) converted).get()); + assertEquals("uints", ((PVAIntArray) converted).getName()); + assertTrue(((PVAIntArray) converted).isUnsigned()); + + long[] longs = new long[]{-1L, 2L, 3L}; + converted = VTypeHelper.toPVArrayType("longs", ArrayLong.of(longs)); + assertInstanceOf(PVALongArray.class, converted); + assertEquals(3, ((PVALongArray) converted).get().length); + assertArrayEquals(longs, ((PVALongArray) converted).get()); + assertEquals("longs", ((PVALongArray) converted).getName()); + assertFalse(((PVALongArray) converted).isUnsigned()); + + longs = new long[]{1L, 2L, 3L}; + converted = VTypeHelper.toPVArrayType("ulongs", ArrayULong.of(longs)); + assertInstanceOf(PVALongArray.class, converted); + assertEquals(3, ((PVALongArray) converted).get().length); + assertArrayEquals(longs, ((PVALongArray) converted).get()); + assertEquals("ulongs", ((PVALongArray) converted).getName()); + assertTrue(((PVALongArray) converted).isUnsigned()); + + float[] floats = new float[]{-1.0f, 2.0f, 3.0f}; + converted = VTypeHelper.toPVArrayType("floats", ArrayFloat.of(floats)); + assertInstanceOf(PVAFloatArray.class, converted); + assertEquals(3, ((PVAFloatArray) converted).get().length); + assertArrayEquals(floats, ((PVAFloatArray) converted).get()); + assertEquals("floats", ((PVAFloatArray) converted).getName()); + + double[] doubles = new double[]{-1.0, 2.0, 3.0}; + converted = VTypeHelper.toPVArrayType("doubles", ArrayDouble.of(doubles)); + assertInstanceOf(PVADoubleArray.class, converted); + assertEquals(3, ((PVADoubleArray) converted).get().length); + assertArrayEquals(doubles, ((PVADoubleArray) converted).get()); + assertEquals("doubles", ((PVADoubleArray) converted).getName()); + + List strings = new ArrayList<>(); + strings.add("a"); + strings.add("b"); + strings.add("c"); + converted = VTypeHelper.toPVArrayType("strings", strings); + assertInstanceOf(PVAStringArray.class, converted); + assertEquals(3, ((PVAStringArray) converted).get().length); + assertArrayEquals(new String[]{"a", "b", "c"}, ((PVAStringArray) converted).get()); + assertEquals("strings", ((PVAStringArray) converted).getName()); + } } diff --git a/app/save-and-restore/common/pom.xml b/app/save-and-restore/common/pom.xml deleted file mode 100644 index fd0275f75e..0000000000 --- a/app/save-and-restore/common/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - 4.0.0 - - - org.phoebus - app-save-and-restore - 4.7.2-SNAPSHOT - - - save-and-restore-common - - - - - org.phoebus - save-and-restore-model - 4.7.2-SNAPSHOT - - - - org.phoebus - core-vtype - 4.7.2-SNAPSHOT - - - - org.epics - vtype - ${vtype.version} - - - - org.epics - vtype-json - ${vtype.version} - - - - org.epics - vtype-gson - ${vtype.version} - - - - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - org.mockito - mockito-core - ${mockito.version} - test - - - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.5 - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - - attach-javadocs - - jar - - - - - - - diff --git a/app/save-and-restore/logging/pom.xml b/app/save-and-restore/logging/pom.xml index 9e95b42221..ca4210816d 100644 --- a/app/save-and-restore/logging/pom.xml +++ b/app/save-and-restore/logging/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-save-and-restore - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT save-and-restore-logging @@ -12,17 +12,17 @@ org.phoebus save-and-restore-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.openjfx @@ -32,7 +32,7 @@ org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/save-and-restore/model/pom.xml b/app/save-and-restore/model/pom.xml index 482f51a41d..94cf9c26e8 100644 --- a/app/save-and-restore/model/pom.xml +++ b/app/save-and-restore/model/pom.xml @@ -5,7 +5,7 @@ org.phoebus app-save-and-restore - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT save-and-restore-model @@ -57,6 +57,13 @@ javax.json 1.1.4 + + + diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshot.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshot.java index 02246a8cf0..90db4a3b15 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshot.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshot.java @@ -45,6 +45,19 @@ public void setCompositeSnapshotData(CompositeSnapshotData compositeSnapshotData this.compositeSnapshotData = compositeSnapshotData; } + @Override + public boolean equals(Object other){ + if(!(other instanceof CompositeSnapshot)){ + return false; + } + return compositeSnapshotNode.equals(((CompositeSnapshot) other).getCompositeSnapshotNode()); + } + + @Override + public int hashCode(){ + return compositeSnapshotNode.hashCode(); + } + public static CompositeSnapshot.Builder builder(){ return new CompositeSnapshot.Builder(); } diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Configuration.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Configuration.java index fd5b2f7b88..f756a3edb6 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Configuration.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Configuration.java @@ -41,6 +41,19 @@ public void setConfigurationData(ConfigurationData configurationData) { this.configurationData = configurationData; } + @Override + public boolean equals(Object other){ + if(!(other instanceof Configuration)){ + return false; + } + return configurationNode.equals(((Configuration) other).getConfigurationNode()); + } + + @Override + public int hashCode(){ + return configurationNode.hashCode(); + } + public static Builder builder(){ return new Builder(); } diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Node.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Node.java index 728c952076..8cf1f27fab 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Node.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Node.java @@ -158,7 +158,7 @@ public void removeTag(Tag tag) { * Implements strategy where the node type ordinal is considered first, then * name in lower case. * - * @param other The tree item to compare to + * @param other The {@link Node} item to compare to * @return -1 if this item is a folder and the other item is a configuration, * 1 if vice versa, and result of name comparison if node types are equal. */ @@ -175,6 +175,9 @@ public boolean equals(Object other) { } if(other instanceof Node) { Node otherNode = (Node)other; + if(uniqueId == null || otherNode.getUniqueId() == null){ + return false; + } return uniqueId.equals(otherNode.getUniqueId()); } return false; diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Snapshot.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Snapshot.java index 5555c5292b..3fa214a795 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Snapshot.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/Snapshot.java @@ -43,4 +43,17 @@ public SnapshotData getSnapshotData() { public void setSnapshotData(SnapshotData snapshotData) { this.snapshotData = snapshotData; } + + @Override + public boolean equals(Object other){ + if(!(other instanceof Snapshot)){ + return false; + } + return snapshotNode.equals(((Snapshot) other).getSnapshotNode()); + } + + @Override + public int hashCode(){ + return snapshotNode.hashCode(); + } } diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotData.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotData.java index 2b59431266..519cf368ea 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotData.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/SnapshotData.java @@ -45,13 +45,7 @@ public List getSnapshotItems() { return snapshotItems; } - public void setSnasphotItems(List snapshotItems) { + public void setSnapshotItems(List snapshotItems) { this.snapshotItems = snapshotItems; } - - public static SnapshotData clone(SnapshotData snapshotDataToClone){ - SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(snapshotDataToClone.getSnapshotItems()); - return snapshotData; - } } diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/UserData.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/UserData.java new file mode 100644 index 0000000000..97c5cfd91c --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/UserData.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.phoebus.applications.saveandrestore.model; + +import java.util.List; + +/** + * Simple pojo used to convey username and list of roles to a client upon + * login or explicit request. + */ +public class UserData { + + private String userName; + private List roles; + + public UserData(){ + + } + + public UserData(String userName, List roles){ + this.userName = userName; + this.roles = roles; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } +} diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/json/VTypeSerializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/json/VTypeSerializer.java index 16b22018c5..28ab8f3a79 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/json/VTypeSerializer.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/json/VTypeSerializer.java @@ -36,6 +36,7 @@ public class VTypeSerializer extends JsonSerializer { @Override public void serialize(VType vType, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeRawValue(VTypeToJson.toJson(vType).toString()); + String s = VTypeToJson.toJson(vType).toString(); + gen.writeRawValue(s); } } diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/SearchQueryUtil.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/SearchQueryUtil.java index 03e0928f3a..b7b933310d 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/SearchQueryUtil.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/SearchQueryUtil.java @@ -40,7 +40,8 @@ public enum Keys { ENDTIME("end"), FROM("from"), SIZE("size"), - GOLDEN("golden"); + GOLDEN("golden"), + PVS("pvs"); private final String name; @@ -68,6 +69,7 @@ public String toString() { lookupTable.put("user", Keys.USER); lookupTable.put("type", Keys.TYPE); lookupTable.put("golden", Keys.GOLDEN); + lookupTable.put("pvs", Keys.PVS); } public static Keys findKey(String keyName) { diff --git a/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshotDataTest.java b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshotDataTest.java new file mode 100644 index 0000000000..5cd592f428 --- /dev/null +++ b/app/save-and-restore/model/src/test/java/org/phoebus/applications/saveandrestore/model/CompositeSnapshotDataTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.applications.saveandrestore.model; + +import org.junit.jupiter.api.Test; + +public class CompositeSnapshotDataTest { + + @Test + public void testClone(){ + + } +} diff --git a/app/save-and-restore/pom.xml b/app/save-and-restore/pom.xml index e6807c2e5e..d2da448a85 100644 --- a/app/save-and-restore/pom.xml +++ b/app/save-and-restore/pom.xml @@ -5,13 +5,11 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT model app - service - common logging diff --git a/app/save-and-restore/service/pom.xml b/app/save-and-restore/service/pom.xml deleted file mode 100644 index 214f33a8ea..0000000000 --- a/app/save-and-restore/service/pom.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - 4.0.0 - - - org.phoebus - app-save-and-restore - 4.7.2-SNAPSHOT - - - save-and-restore-service - - - - - org.phoebus - save-and-restore-model - 4.7.2-SNAPSHOT - - - - org.phoebus - save-and-restore-common - 4.7.2-SNAPSHOT - - - - org.phoebus - core-pv - 4.7.2-SNAPSHOT - - - - org.epics - vtype - ${vtype.version} - - - - org.epics - vtype-json - ${vtype.version} - - - - org.epics - vtype-gson - ${vtype.version} - - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - - com.fasterxml.jackson.jaxrs - jackson-jaxrs-json-provider - ${jackson.version} - - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - - - com.sun.jersey - jersey-core - 1.19 - - - com.sun.jersey - jersey-client - 1.19 - - - - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - org.mockito - mockito-core - ${mockito.version} - test - - - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.5 - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - - attach-javadocs - - jar - - - - - - - diff --git a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/RestoreReport.java b/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/RestoreReport.java deleted file mode 100644 index 46c54be015..0000000000 --- a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/RestoreReport.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -package org.phoebus.applications.saveandrestore.script; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * A wrapper for various items describing the outcome of a restore operation. - */ -public class RestoreReport { - - /** - * Unique id of a snaphsot - */ - private String snapshotId; - /** - * "Path" if a snapshot, e.g. /folder1/folder2/saveset1/snapshot1 - */ - private String snapshotPath; - /** - * Date when the snapshot was restored - */ - private Date restoreDate; - /** - * Map of PV names and the values to which they were restored. - */ - private Map restoredPVs; - /** - * List of PVs that failed to restore. May be empty. - */ - private List nonRestoredPVs; - /** - * List of PVs that were restored in case one or several write operations fail and if rollback was requested, - * see {@link org.phoebus.applications.saveandrestore.script.SaveAndRestoreScriptUtil}. May be null. - */ - private Map rolledBackPVs; - - public RestoreReport(Date restoreDate, String snapshotId, String snapshotPath) { - this.restoreDate = restoreDate; - this.snapshotId = snapshotId; - this.snapshotPath = snapshotPath; - } - - public void setRolledBackPVs(Map rolledBackPVs) { - this.rolledBackPVs = rolledBackPVs; - } - - public void setRestoreDate(Date restoreDate) { - this.restoreDate = restoreDate; - } - - public Map getRestoredPVs() { - return restoredPVs; - } - - public void setRestoredPVs(Map restoredPVs) { - this.restoredPVs = restoredPVs; - } - - public List getNonRestoredPVs() { - return nonRestoredPVs; - } - - public void setNonRestoredPVs(List nonRestoredPVs) { - this.nonRestoredPVs = nonRestoredPVs; - } - - @Override - public String toString() { - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("SnapshotData id: ").append(snapshotId).append(System.lineSeparator()); - stringBuilder.append("SnapshotData path: ").append(snapshotPath).append(System.lineSeparator()); - stringBuilder.append("Restore date: ").append(restoreDate).append(System.lineSeparator()); - stringBuilder.append("Restored PVs: ").append(System.lineSeparator()); - restoredPVs.entrySet().stream().forEach(entry -> { - stringBuilder.append(entry.getKey()).append(" : ").append(entry.getValue()).append(System.lineSeparator()); - }); - if (!nonRestoredPVs.isEmpty()) { - stringBuilder.append("Non-restored PVs:").append(System.lineSeparator()); - nonRestoredPVs.stream().forEach(pvName -> { - stringBuilder.append(pvName).append(System.lineSeparator()); - }); - } - if (rolledBackPVs != null && !rolledBackPVs.isEmpty()) { - stringBuilder.append("Rolled-back PVs:").append(System.lineSeparator()); - rolledBackPVs.entrySet().stream().forEach(entry -> { - stringBuilder.append(entry.getKey()).append(" : ").append(entry.getValue()).append(System.lineSeparator()); - }); - } - - return stringBuilder.toString(); - } -} diff --git a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/SaveAndRestoreScriptUtil.java b/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/SaveAndRestoreScriptUtil.java deleted file mode 100644 index c20668a2a5..0000000000 --- a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/script/SaveAndRestoreScriptUtil.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -package org.phoebus.applications.saveandrestore.script; - -import org.epics.vtype.VType; -import org.phoebus.applications.saveandrestore.SaveAndRestoreClient; -import org.phoebus.applications.saveandrestore.common.Utilities; -import org.phoebus.applications.saveandrestore.impl.SaveAndRestoreJerseyClient; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.pv.PV; -import org.phoebus.pv.PVPool; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -@SuppressWarnings("unused") -public class SaveAndRestoreScriptUtil { - - private static SaveAndRestoreClient saveAndRestoreClient; - private static final Logger logger = Logger.getLogger(SaveAndRestoreScriptUtil.class.getName()); - - /** - * Useful in case a mock client is needed. - * - * @param client The client used to interact with the remote service - */ - public static void setSaveAndRestoreClient(SaveAndRestoreClient client) { - saveAndRestoreClient = client; - } - - /** - * Should be called before a call to the service is invoked. - */ - private static void ensureClientSet() { - if (saveAndRestoreClient == null) { - saveAndRestoreClient = new SaveAndRestoreJerseyClient(); - } - } - - public static List getChildNodes(String nodeId) { - ensureClientSet(); - return saveAndRestoreClient.getChildNodes(nodeId); - } - - public static List getSnapshotItems(String snapshotId) { - ensureClientSet(); - return saveAndRestoreClient.getSnapshotData(snapshotId).getSnapshotItems(); - } - - /** - * Restores PV values from a given snapshot. Before values are written to the PVs, this method will first - * connect to all of the PVs. If any of the PVs fails to connect, an exception is thrown, i.e. the restore - * operation is aborted. - *

- * Once all PVs have been successfully connected, the persisted values in the snapshot are written in an - * synchronous manner, i.e. the call to the write operation on a PV will wait for completion before a write - * operation on the next PV is invoked. - *

- * - * @param snapshotId The unique id of a snapshot, which can be copied to the clipboard in the save-and-restore UI. - * @param connectTimeout The timeout in ms when connecting to the PVs. If not all PVs are connected after - * connectTimeout ms, an exception is thrown. - * @param writeTimeout The timeout i ms to wait for a single write operation to complete. - * @param abortOnFail Determines if write of PV values should be aborted when a write failure occurs, e.g. - * PV is disconnected or read-only. - * @param rollBack Determines if restored PVs should be restored to the original state if a write failure occurs. - * @return A {@link RestoreReport} holding data that can be used to analyze the outcome of the process. - * @throws Exception In either of these following cases: - *
    - *
  • The remote save-and-restore service is unavailable.
  • - *
  • No snapshot identified by the snapshot id exists.
  • - *
  • The snapshot is not associated with any persisted PV values. This is a corner case...
  • - *
  • If any of the PVs fails to connect.
  • - *
- */ - public static RestoreReport restore(String snapshotId, int connectTimeout, int writeTimeout, boolean abortOnFail, boolean rollBack) throws Exception { - ensureClientSet(); - saveAndRestoreClient.getNode(snapshotId); // This will throw an exception if the snapshot does not exist. - List snapshotItems = saveAndRestoreClient.getSnapshotData(snapshotId).getSnapshotItems(); - List restorableItems = - snapshotItems.stream().filter(item -> !item.getConfigPv().isReadOnly()).collect(Collectors.toList()); - if (restorableItems.isEmpty()) { // Should really not happen. - throw new Exception("No restorable PVs found in snapshot id " + snapshotId); - } - - Map savedValues = new HashMap<>(); - Map pvs = new HashMap<>(); - List> latest = new ArrayList<>(); - boolean allConnected = true; - try { - // First connect to all PVs and read values. - for (SnapshotItem item : restorableItems) { - final CompletableFuture done = new CompletableFuture<>(); - latest.add(done); - String pvName = item.getConfigPv().getPvName(); - final org.phoebus.pv.PV pv = PVPool.getPV(pvName); - pvs.put(pvName, pv); - pv.onValueEvent().subscribe(value -> { - if (!PV.isDisconnected(value)) { - savedValues.put(pvName, value); - done.complete(value); - } - }); - } - CompletableFuture.allOf(latest.toArray(new CompletableFuture[latest.size()])) - .get(connectTimeout, TimeUnit.MILLISECONDS); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to connect and read all PVs", e); - allConnected = false; - } - - // If any of the PVs fails to connect, abort the restore process. - if (!allConnected) { - pvs.forEach((key, value) -> PVPool.releasePV(value)); - throw new Exception("Failed to connect to all PVs within " + connectTimeout + " ms."); - } - String path = saveAndRestoreClient.getFullPath(snapshotId); - RestoreReport restoreReport = new RestoreReport(new Date(), snapshotId, path); - Map restoredPVs = new HashMap<>(); - List nonRestoredPvs = new ArrayList<>(); - List rollBackItems = new ArrayList<>(); - // All connected, now write values - boolean writeFailureDetected = false; - for (SnapshotItem item : restorableItems) { - String pvName = item.getConfigPv().getPvName(); - PV pv = pvs.get(pvName); - try { - Object vType = Utilities.toRawValue(item.getValue()); - Future result = pv.asyncWrite(vType); - result.get(1000, TimeUnit.MILLISECONDS); - restoredPVs.put(pvName, vType); - // A restored PV may be subject to rollback, so add it here - rollBackItems.add(new RollBackItem(pv, savedValues.get(pvName))); - } catch (Exception exception) { - logger.log(Level.WARNING, "Failed to restore/write PV " + pvName); - writeFailureDetected = true; - if (abortOnFail) { - break; - } - } - } - if (writeFailureDetected && rollBack) { - Map rolledBackPVs = rollback(rollBackItems, writeTimeout); - restoreReport.setRolledBackPVs(rolledBackPVs); - } - // Determine the list of PVs that were not restored. - List restorablePVNames = pvs.values().stream().map(PV::getName).collect(Collectors.toList()); - List restoredPVNames = new ArrayList<>(restoredPVs.keySet()); - restorablePVNames.removeAll(restoredPVNames); - restoreReport.setNonRestoredPVs(restorablePVNames); - - restoreReport.setRestoredPVs(restoredPVs); - - pvs.forEach((key, value) -> PVPool.releasePV(value)); - - return restoreReport; - } - - private static Map rollback(List rollBackItems, int writeTimeout) { - Map rolledBackPVs = new HashMap<>(); - rollBackItems.forEach(item -> { - try { - logger.log(Level.INFO, "Rollback of PV " + item.pv.getName() + " to value " + item.value); - Future result = item.pv.asyncWrite(Utilities.toRawValue(item.value)); - result.get(writeTimeout, TimeUnit.MILLISECONDS); - rolledBackPVs.put(item.pv.getName(), item.value); - } catch (Exception exception) { - logger.log(Level.WARNING, "Failed to rollback PV " + item.pv.getName(), exception); - } - }); - return rolledBackPVs; - } - - private static class RollBackItem { - private final VType value; - private final PV pv; - - public RollBackItem(PV pv, VType value) { - this.pv = pv; - this.value = value; - } - } -} diff --git a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/service/Messages.java b/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/service/Messages.java deleted file mode 100644 index 6c93843f82..0000000000 --- a/app/save-and-restore/service/src/main/java/org/phoebus/applications/saveandrestore/service/Messages.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (C) 2019 European Spallation Source ERIC. - *

- * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package org.phoebus.applications.saveandrestore.service; - -import org.phoebus.framework.nls.NLS; - -public class Messages { - - public static String compositeSnapshotConsistencyCheckFailed; - public static String copyOrMoveNotAllowedBody; - public static String createNodeFailed; - public static String createCompositeSnapshotFailed; - public static String createConfigurationFailed; - public static String deleteFilterFailed; - public static String updateConfigurationFailed; - public static String updateCompositeSnapshotFailed; - public static String saveFilterFailed; - public static String searchFailed; - public static String saveSnapshotFailed; - public static String tagAddFailed; - public static String updateNodeFailed; - - static - { - NLS.initializeMessages(Messages.class); - } - - private Messages() - { - // Prevent instantiation - } - -} diff --git a/app/save-and-restore/service/src/main/resources/org/phoebus/applications/saveandrestore/service/messages.properties b/app/save-and-restore/service/src/main/resources/org/phoebus/applications/saveandrestore/service/messages.properties deleted file mode 100644 index 4eef2ff376..0000000000 --- a/app/save-and-restore/service/src/main/resources/org/phoebus/applications/saveandrestore/service/messages.properties +++ /dev/null @@ -1,19 +0,0 @@ -compositeSnapshotConsistencyCheckFailed=Failed to check consistency for composite snapshot -copyOrMoveNotAllowedBody=Selection cannot be moved/copied to the specified target node. -createCompositeSnapshotFailed=Failed to create composite snapshot -createConfigurationFailed=Failed to create configuration -createNodeFailed=Failed to create new node -deleteFilterFailed=Failed to delete filter -saveFilterFailed=Failed to save filter -saveSnapshotFailed=Failed to save snapshot -searchFailed=Search failed -tagAddFailed=Failed to add tag -tagDeleteFailed=Failed to delete tag -updateConfigurationFailed=Failed to update configuration -updateCompositeSnapshotFailed=Failed to update composite snapshot -updateNodeFailed=Failed to update node - - - - - diff --git a/app/scan/client/pom.xml b/app/scan/client/pom.xml index ea970d2a8a..ffadc1d322 100644 --- a/app/scan/client/pom.xml +++ b/app/scan/client/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-scan - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-scan-client @@ -22,12 +22,12 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-scan-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/scan/model/pom.xml b/app/scan/model/pom.xml index abb9eb01c1..b9412fb718 100644 --- a/app/scan/model/pom.xml +++ b/app/scan/model/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-scan - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-scan-model @@ -22,17 +22,17 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/scan/model/src/main/java/org/csstudio/ndarray/NDCompare.java b/app/scan/model/src/main/java/org/csstudio/ndarray/NDCompare.java index 27f5c56207..bb1a721053 100644 --- a/app/scan/model/src/main/java/org/csstudio/ndarray/NDCompare.java +++ b/app/scan/model/src/main/java/org/csstudio/ndarray/NDCompare.java @@ -183,9 +183,9 @@ private static NDArray binary_operation(final NDArray a, final NDArray b, * @param other N-dim array * @return Bool array */ - public static NDArray equal_to(final NDArray a, final NDArray b) + public static NDArray equal_to(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_eq); + return binary_operation(array, other, op_eq); } @@ -194,9 +194,9 @@ public static NDArray equal_to(final NDArray a, final NDArray b) * @param other N-dim array * @return Bool array */ - public static NDArray not_equal_to(final NDArray a, final NDArray b) + public static NDArray not_equal_to(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_ne); + return binary_operation(array, other, op_ne); } /** Element-by-element comparison @@ -204,9 +204,9 @@ public static NDArray not_equal_to(final NDArray a, final NDArray b) * @param other N-dim array * @return Bool array */ - public static NDArray less_than(final NDArray a, final NDArray b) + public static NDArray less_than(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_lt); + return binary_operation(array, other, op_lt); } /** Element-by-element comparison @@ -214,9 +214,9 @@ public static NDArray less_than(final NDArray a, final NDArray b) * @param other N-dim array * @return Bool array */ - public static NDArray less_equal(final NDArray a, final NDArray b) + public static NDArray less_equal(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_le); + return binary_operation(array,other, op_le); } /** Element-by-element comparison @@ -224,9 +224,9 @@ public static NDArray less_equal(final NDArray a, final NDArray b) * @param other N-dim array * @return Bool array */ - public static NDArray greater_than(final NDArray a, final NDArray b) + public static NDArray greater_than(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_gt); + return binary_operation(array, other, op_gt); } /** Element-by-element comparison @@ -234,8 +234,8 @@ public static NDArray greater_than(final NDArray a, final NDArray b) * @param other N-dim array * @return Bool array */ - public static NDArray greater_equal(final NDArray a, final NDArray b) + public static NDArray greater_equal(final NDArray array, final NDArray other) { - return binary_operation(a, b, op_ge); + return binary_operation(array, other, op_ge); } } diff --git a/app/scan/model/src/main/java/org/csstudio/ndarray/NDMatrix.java b/app/scan/model/src/main/java/org/csstudio/ndarray/NDMatrix.java index ff779c2adf..2061a37ce0 100644 --- a/app/scan/model/src/main/java/org/csstudio/ndarray/NDMatrix.java +++ b/app/scan/model/src/main/java/org/csstudio/ndarray/NDMatrix.java @@ -101,7 +101,7 @@ public static NDArray linspace(final double start, final double end, final int c * @param shape Desired shape * @return Array view with new shape * @throws IllegalArgumentException if new shape conflicts with existing size - * @see #reshape(NDShape) + * @see NDShape #reshape */ public static NDArray reshape(final NDArray array, final int... shape) { @@ -274,9 +274,6 @@ private static NDArray dot2x1(final NDArray a, final NDArray b, /** Perform matrix multiplication of arrays 1x1 arrays * @param a 1-dim array * @param b 1-dim array - * @param len_a Length of a - * @param len_b Length of b - * * @return Result a * b in the matrix sense */ public static NDArray inner(final NDArray a, final NDArray b) diff --git a/app/scan/model/src/main/java/org/csstudio/ndarray/NDShape.java b/app/scan/model/src/main/java/org/csstudio/ndarray/NDShape.java index 57a6e07a45..b1cfa017eb 100644 --- a/app/scan/model/src/main/java/org/csstudio/ndarray/NDShape.java +++ b/app/scan/model/src/main/java/org/csstudio/ndarray/NDShape.java @@ -150,7 +150,7 @@ public static boolean haveEqualEnds(final NDShape a, final NDShape b) * @param a One shape * @param b Other shape * @return Combined shape. null if shapes don't match at the end. - * @see #hasEqualEnd(NDShape) + * @see NDShape #hasEqualEnd() */ public static NDShape combine(final NDShape a, final NDShape b) { diff --git a/app/scan/model/src/main/java/org/csstudio/ndarray/NDStrides.java b/app/scan/model/src/main/java/org/csstudio/ndarray/NDStrides.java index a906af8c7b..d8fd7272fb 100644 --- a/app/scan/model/src/main/java/org/csstudio/ndarray/NDStrides.java +++ b/app/scan/model/src/main/java/org/csstudio/ndarray/NDStrides.java @@ -37,7 +37,7 @@ public NDStrides(final int... strides) } /** Initialize - * @param shape Array strides, e.g. [1, 2] + * @param strides Array strides, e.g. [1, 2] */ public NDStrides(final List strides) { diff --git a/app/scan/model/src/main/java/org/csstudio/scan/command/Comparison.java b/app/scan/model/src/main/java/org/csstudio/scan/command/Comparison.java index 337c0181b3..305b1f14d2 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/command/Comparison.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/command/Comparison.java @@ -33,10 +33,10 @@ public enum Comparison /** Value at or above desired value, '>=' */ AT_LEAST(">="), - /** Value below desired value, '<' */ + /** Value below desired value, {@literal '<'} */ BELOW("<"), - /** Value at or below desired value, '<=' */ + /** Value at or below desired value, {@literal '<='} */ AT_MOST("<="), /** Value has increased by some amount */ diff --git a/app/scan/model/src/main/java/org/csstudio/scan/command/IncludeCommand.java b/app/scan/model/src/main/java/org/csstudio/scan/command/IncludeCommand.java index fec80f64c1..c2840e815c 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/command/IncludeCommand.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/command/IncludeCommand.java @@ -37,7 +37,8 @@ public IncludeCommand() } /** Initialize - * @param comment Comment + * @param scan_file + * @param macros */ public IncludeCommand(final String scan_file, final String macros) { diff --git a/app/scan/model/src/main/java/org/csstudio/scan/command/LoopCommand.java b/app/scan/model/src/main/java/org/csstudio/scan/command/LoopCommand.java index 9f041bb2f4..22f046589a 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/command/LoopCommand.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/command/LoopCommand.java @@ -196,7 +196,7 @@ public boolean getCompletion() return completion; } - /** @param wait Wait for write completion? */ + /** @param completion Wait for write completion ? */ public void setCompletion(final Boolean completion) { this.completion = completion; diff --git a/app/scan/model/src/main/java/org/csstudio/scan/command/SetCommand.java b/app/scan/model/src/main/java/org/csstudio/scan/command/SetCommand.java index ad0030b185..d90c45c9ce 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/command/SetCommand.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/command/SetCommand.java @@ -149,7 +149,7 @@ public boolean getCompletion() return completion; } - /** @param wait Wait for write completion? */ + /** @param completion Wait for write completion ? */ public void setCompletion(final Boolean completion) { this.completion = completion; diff --git a/app/scan/model/src/main/java/org/csstudio/scan/command/XMLCommandReader.java b/app/scan/model/src/main/java/org/csstudio/scan/command/XMLCommandReader.java index f0129aff36..1f37822eb7 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/command/XMLCommandReader.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/command/XMLCommandReader.java @@ -19,12 +19,12 @@ /** Read {@link ScanCommand}s from XML stream. * - *

Depends on a {@link SimpleScanCommandFactory} to + *

Depends on a see SimpleScanCommandFactory to * create the {@link ScanCommand} instances. * * @author Kay Kasemir */ -@SuppressWarnings("nls") + public class XMLCommandReader { /** Read scan commands from XML string diff --git a/app/scan/model/src/main/java/org/csstudio/scan/data/NumberScanSample.java b/app/scan/model/src/main/java/org/csstudio/scan/data/NumberScanSample.java index c625f3ca14..18c7147d58 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/data/NumberScanSample.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/data/NumberScanSample.java @@ -21,7 +21,7 @@ /** Scan sample for numbers * @author Kay Kasemir */ -@SuppressWarnings("nls") + public class NumberScanSample extends ScanSample { final private Number[] values; @@ -29,7 +29,7 @@ public class NumberScanSample extends ScanSample /** Initialize * @param timestamp Time stamp * @param serial Serial to identify when the sample was taken - * @param number Number + * @param values Number */ public NumberScanSample(final Instant timestamp, final long serial, final Number... values) diff --git a/app/scan/pom.xml b/app/scan/pom.xml index 68a1ad98aa..b1cff3fe6b 100644 --- a/app/scan/pom.xml +++ b/app/scan/pom.xml @@ -5,7 +5,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT model diff --git a/app/scan/ui/pom.xml b/app/scan/ui/pom.xml index 8968809c6b..947c963758 100644 --- a/app/scan/ui/pom.xml +++ b/app/scan/ui/pom.xml @@ -3,7 +3,7 @@ org.phoebus app-scan - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-scan-ui @@ -22,27 +22,27 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-rtplot - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-scan-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-scan-client - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java index 55aab2f5cf..7135c45bb2 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java @@ -423,7 +423,7 @@ void updateScanInfo(final ScanInfo info) /** Change a command's property on the scan server, i.e. for a 'live' scan * @param command Command to change - * @param property_id Property to change + * @param property Property to change * @param value New value * @throws Exception on error */ diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/actions/AddCommands.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/actions/AddCommands.java index dee31bb298..fb9612f4a9 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/actions/AddCommands.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/actions/AddCommands.java @@ -36,7 +36,7 @@ public AddCommands(final Model model, final ScanCommand location, final List org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT rich-adapters diff --git a/app/trends/rich-adapters/pom.xml b/app/trends/rich-adapters/pom.xml index 0efa5eed74..9ac8be7ab6 100644 --- a/app/trends/rich-adapters/pom.xml +++ b/app/trends/rich-adapters/pom.xml @@ -3,29 +3,29 @@ org.phoebus app-trends - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-trends-rich-adapters org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-databrowser - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-email-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/app/trends/rich-adapters/src/main/java/org/phoebus/apps/trends/rich/adapters/DatabrowserAdapterFactory.java b/app/trends/rich-adapters/src/main/java/org/phoebus/apps/trends/rich/adapters/DatabrowserAdapterFactory.java index 2df5ecfc7d..50827f2c77 100644 --- a/app/trends/rich-adapters/src/main/java/org/phoebus/apps/trends/rich/adapters/DatabrowserAdapterFactory.java +++ b/app/trends/rich-adapters/src/main/java/org/phoebus/apps/trends/rich/adapters/DatabrowserAdapterFactory.java @@ -74,6 +74,8 @@ else if (adapterType.isAssignableFrom(LogEntry.class)) .appendDescription(getBody(databrowserSelection)); try { + databrowserSelection.makeTimeRangeAbsolute(); + final File image_file = databrowserSelection.getPlot() == null ? null : new Screenshot(databrowserSelection.getPlot()).writeToTempfile("image"); if(image_file != null){ log.attach(AttachmentImpl.of(image_file)); diff --git a/app/trends/simple-adapters/pom.xml b/app/trends/simple-adapters/pom.xml index a0722c44cb..bd4ad28cfb 100644 --- a/app/trends/simple-adapters/pom.xml +++ b/app/trends/simple-adapters/pom.xml @@ -3,29 +3,29 @@ org.phoebus app-trends - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT app-trends-simple-adapters org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-databrowser - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-email-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-logbook - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/app/update/mk_update_settings.sh b/app/update/mk_update_settings.sh index d4af1a09c6..e3752c025a 100755 --- a/app/update/mk_update_settings.sh +++ b/app/update/mk_update_settings.sh @@ -6,7 +6,7 @@ then echo '' echo 'URL, for example http://your.update.site/path/to/product-$(arch).zip,' echo 'is where the update process will check for a new version.' - echo 'It may contain $(arch), which gets replaced by "linux", "mac" or "win"' + echo 'It may contain $(arch), which gets replaced by "linux", "mac", "mac-aarch64" or "win"' echo 'depending on the OS.' echo '' echo 'Redirect the output into a file settings.ini' diff --git a/app/update/pom.xml b/app/update/pom.xml index 1af33b562f..fb2603d643 100644 --- a/app/update/pom.xml +++ b/app/update/pom.xml @@ -4,7 +4,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -22,17 +22,17 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT javax.jms diff --git a/app/update/src/main/java/org/phoebus/applications/update/Update.java b/app/update/src/main/java/org/phoebus/applications/update/Update.java index fbc93c1f51..320a3b1a24 100644 --- a/app/update/src/main/java/org/phoebus/applications/update/Update.java +++ b/app/update/src/main/java/org/phoebus/applications/update/Update.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -130,7 +130,12 @@ static public String replace_arch(final String text) if (PlatformInfo.is_linux) return text.replace("$(arch)", "linux"); else if (PlatformInfo.is_mac_os_x) - return text.replace("$(arch)", "mac"); + { + if ("aarch64".equalsIgnoreCase(System.getProperty("os.arch"))) + return text.replace("$(arch)", "mac-aarch64"); + else + return text.replace("$(arch)", "mac"); + } else return text.replace("$(arch)", "win"); } @@ -320,7 +325,7 @@ public Instant checkForUpdate(final JobMonitor monitor) throws Exception /** Perform update * - *

Get & unpack the update file into the current installation. + * Get and unpack the update file into the current installation. * * @param monitor {@link JobMonitor} * @param install_location Existing {@link Locations#install()} diff --git a/app/update/src/main/java/org/phoebus/applications/update/UpdateProvider.java b/app/update/src/main/java/org/phoebus/applications/update/UpdateProvider.java index 0e37326a36..661580102b 100644 --- a/app/update/src/main/java/org/phoebus/applications/update/UpdateProvider.java +++ b/app/update/src/main/java/org/phoebus/applications/update/UpdateProvider.java @@ -21,7 +21,7 @@ public interface UpdateProvider /** Perform update * - *

Get & unpack the update file into the current installation. + * Get and unpack the update file into the current installation. * * @param monitor {@link JobMonitor} * @param install_location Existing {@link Locations#install()} diff --git a/app/utility/pom.xml b/app/utility/pom.xml index f63d10305f..8648a2840d 100644 --- a/app/utility/pom.xml +++ b/app/utility/pom.xml @@ -4,7 +4,7 @@ org.phoebus app - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 diff --git a/app/utility/preference-manager/pom.xml b/app/utility/preference-manager/pom.xml index e20811fc0e..7f8cce5319 100644 --- a/app/utility/preference-manager/pom.xml +++ b/app/utility/preference-manager/pom.xml @@ -3,7 +3,7 @@ app-utility org.phoebus - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 @@ -12,12 +12,12 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT \ No newline at end of file diff --git a/core/email/pom.xml b/core/email/pom.xml index 622071ad1b..662c7b30cf 100644 --- a/core/email/pom.xml +++ b/core/email/pom.xml @@ -3,7 +3,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-email @@ -15,7 +15,7 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/formula/doc/index.rst b/core/formula/doc/index.rst index 892a3976df..d0a83eea43 100644 --- a/core/formula/doc/index.rst +++ b/core/formula/doc/index.rst @@ -1,12 +1,108 @@ Formulas ======== +Several types of formulas are supported. +A prefix of the form "=xx(..)" is typically used to select the formula. -Function categories -___________________ -*Arrays* +Operators +---- +You can use the following operators : +| **Arithmetic and maths operators :** +| " **+** " - Addition. +| " **-** " - Subtraction. +| " **\*** " - Multiplication. +| " **/** " - Division. +| " **^** " - Power. +| **rnd(x)** - Return a floating point number between 0 and x. +| **max(expression...)** - Return the maximum between all expressions. +| **min(expression...)** - Return the minimum between all expressions. + +| **Logical and comparison operators :** +| " **&** " / " **&&** " - AND. +| " **|** " / " **||** " - OR. +| " **!(...)** " - NOT. +| " **==** " - Equals. +| " **!=** " - Not equals. +| " **>=** " - Greater than or equal. +| " **>** " - Greater than. +| " **<=** " - Lesser than or equal. +| " **<** " - Lesser than. + +| **Conditional operator :** +| ** ? : ** - Return a first expression if condition is true or a second expression if it's false. + + +Maths +----- +**abs(double value)** - Returns the absolute value of a double value. + +**acos(double value)** - Returns the inverse cosine of a double value. The returned angle is in the +range 0.0 through pi. + +**asin(double value)** - Returns the inverse sine of a double value. The returned angle is in the +range -pi/2 through pi/2. + +**atan(double value)** - Returns the inverse tangent of a double value. the returned angle is in the +range -pi/2 through pi/2. + +**atan2(double y, double x)** - Returns the angle theta from the conversion of rectangular coordinates (x, y) +to polar coordinates (r, theta). This method computes the phase theta by computing an arc tangent +of y/x in the range of -pi to pi. + +**ceil(double value)** - Returns the smallest double value that is greater than or equal to the argument +and is equal to a mathematical integer. + +**cos(double value)** - Returns the trigonometric cosine of an angle. + +**cosh(double value)** - Returns the hyperbolic cosine of a double value. The hyperbolic cosine +of x is defined to be (e^x + e^-x)/2 where e is Euler's number. + +**exp(double value)** - Returns Euler's number e raised to the power of a double value. + +**expm1(double value)** - Returns (e^x) -1. Note that for values of x near 0, the exact sum of expm1(x) + 1 +is much closer to the true result of e^x than exp(x). + +**floor(double value)** - Returns the largest double value that is less than or equal to the +argument and is equal to a mathematical integer. + +**hypot(double x, double y)** - Returns hypotenuse : sqrt(x² +y²) without intermediate overflow or underflow. + +**log(double value)** - Returns the natural logarithm (base e) of a double value. + +**log10(double value)** - Returns the base 10 logarithm of a double value. + +**pow(double base, double exponent)** - Returns the value of the first argument raised +to the power of the second argument. + +**round(double value)** - Returns the closest long to the argument, with ties rounding to positive infinity. + +**sin(double value)** - Returns the trigonometric sine of an angle. + +**sinh(double value)** - Returns the hyperbolic sine of a double value. The hyperbolic sine +of x is defined to be (e^x - e^-x)/2 where e is Euler's number. + +**sqrt(double value)** - Returns the correctly rounded positive square root of a double value. + +**tan(double value)** - Returns the trigonometric tangent of an angle. + +**tanh(double value)** - Returns the hyperbolic tangent of a double value. The hyperbolic tangent +of x is defined to be (e^x - e^-x)/(e^x + e^-x), in other words, sinh(x)/cosh(x). +Note that the absolute value of the exact tanh is always less than 1. + +**toDegrees(double value)** - Converts an angle measured in radians to an approximately equivalent +angle measured in degrees. The conversion from radians to degrees is generally inexact; +users should not expect cos(toRadians(90.0)) to exactly equal 0.0. + +**toRadians(double value)** - Converts an angle measured in degrees to an approximately equivalent +angle measured in radians. The conversion from degrees to radians is generally inexact. + +**fac(double value)** - Calculate the factorial of the given number. + + +Arrays +------ **arrayDiv(VNumberArray array1, VNumberArray array2)** - Returns a VNumberarray where each element is defined as array1[x] / array2[x]. The input arrays must be of equal length. @@ -52,4 +148,62 @@ This includes the average, min, max, and element count **arrayMax(VNumberArray array)** - Returns a VDouble with the greatest value of the given array -**arrayMin(VNumberArray array)** - Returns a VDouble with the smallest value of the given array \ No newline at end of file +**arrayMin(VNumberArray array)** - Returns a VDouble with the smallest value of the given array + + +String +------ +**concat(String s...)** - Concatenate a list of strings of a string array. + +**strEqual(String s1, String s2)** - Compare value of 2 strings. Return true if s1 equals s2, else false. + + +Enum +---- +**enumOf(VNumber value, VNumberArray intervals, VStringArray labels)** - Creates a VEnum based a value and a set of intervals. + +**indexOf(Enum e)** - Return the index of the enum value. + + +Alarm +----- +**highestSeverity(String s...)** - Returns the highest severity. + +**majorAlarm(Boolean condition, String s)** - Returns a string with major severity when the given condition is true. + +**minorAlarm(Boolean condition, String s)** - Returns a string with minor severity when the given condition is true. + + +Bitwise operation +----------------- +**bitAND(long a, long b)** - Bitwise AND, operation "a & b". Throw an exception if 'a' or 'b' are floating-point numbers. + +**bitOR(long a, long b)** - Bitwise OR, operation "a | b". Throw an exception if 'a' or 'b' are floating-point numbers. + +**bitXOR(long a, long b)** - Bitwise XOR, operation "a ^ b". Throw an exception if 'a' or 'b' are floating-point numbers. + +**bitLeftShift(long a, long b)** - Bitwise Left Shift, operation "a << b". Throw an exception if 'a' or 'b' are floating-point numbers. + +**bitRightShift(long a, long b)** - Bitwise Right Shift, operation "a >> b". Throw an exception if 'a' or 'b' are floating-point numbers. + +**bitNOT(long a)** - Bitwise NOT, operation "~a". Throw an exception if 'a' is a floating-point number. + + +Area detector +------------- +**adData(VNumberArray data, String type)** - Map the area detector data to the specified type, +i.e. [Int8, UInt8, Int16, UInt16, Int32, UInt32, Float32, Float64]. + +**imageHeight(VImage image)** - Fetch height of image. + +**imageWidth(VImage image)** - Fetch width of image. + +**imageValue(VImage image)** - Fetch array data of image. + +**imageXOffset(VImage image)** - Fetch horizontal offset of image. + +**imageXReversed(VImage image)** - Fetch horizontal reversal of image. + +**imageYOffset(VImage image)** - Fetch vertical offset of image. + +**imageYReversed(VImage image)** - Fetch vertical reversal of image. \ No newline at end of file diff --git a/core/formula/pom.xml b/core/formula/pom.xml index fe027c3aaa..4c72940b7f 100644 --- a/core/formula/pom.xml +++ b/core/formula/pom.xml @@ -4,7 +4,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -16,7 +16,7 @@ org.phoebus core-vtype - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/Formula.java b/core/formula/src/main/java/org/csstudio/apputil/formula/Formula.java index f7b1c2d69f..2b5b8671f0 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/Formula.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/Formula.java @@ -48,8 +48,8 @@ * min(a, b, ...), max(a, b, ...). *

  • *, /, ^ *
  • +, - - *
  • comparisons <, >, >=, <=, ==, != - *
  • boolean logic !, &, |, .. ? .. : .. + *
  • comparisons {@literal <, >, >=, <=, ==, !=} + *
  • boolean logic {@literal !, &, |, .. ? .. : ..} * * * diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/Node.java b/core/formula/src/main/java/org/csstudio/apputil/formula/Node.java index 6c56b560eb..481e24aebe 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/Node.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/Node.java @@ -16,16 +16,18 @@ public interface Node { /** Evaluate the node, i.e. compute its value. * @return The value of the node. - * @exception on error + * Throws Exception on error */ public VType eval(); /** Check if this node has given node as a subnode + * @param node , the test node * @return true if given node was found under this one. */ public boolean hasSubnode(Node node); /** Check if this node has a sub-node with the name + * @param name, node name * @return true if given node name was found under this one. */ public boolean hasSubnode(String name); diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/VariableNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/VariableNode.java index 2a05ab9986..b5826a7c3d 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/VariableNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/VariableNode.java @@ -24,19 +24,31 @@ public class VariableNode implements Node /** Current value of the variable. */ private volatile VType value; - /** Create Variable with given name. */ + /** + * Create Variable with given name. + * @param name , the name of the variable + */ public VariableNode(final String name) { this(name, Double.NaN); } - /** Create Variable with given name and value. */ + /** */ + /** + * Create Variable with given name and value. + * @param name , the name of the variable + * @param value, the value of the variable + */ public VariableNode(final String name, final double value) { this(name, VDouble.of(value, Alarm.none(), Time.now(), Display.none())); } - /** Create Variable with given name and value. */ + /** + * Create Variable with given name and value. + * @param name , the name of the variable + * @param value, the value of the variable see @VType + */ public VariableNode(final String name, final VType value) { this.name = name; @@ -49,13 +61,17 @@ final public String getName() return name; } - /** @param New value of variable. */ + /** + * @param value, new value of variable + */ public void setValue(final double value) { setValue(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); } - /** @param New value of variable. */ + /** + * @param value , new value of variable see @VType + */ public void setValue(final VType value) { this.value = value; diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/array/ArrayRangeOfFunction.java b/core/formula/src/main/java/org/csstudio/apputil/formula/array/ArrayRangeOfFunction.java index d4e4900489..e1c80df8c7 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/array/ArrayRangeOfFunction.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/array/ArrayRangeOfFunction.java @@ -29,7 +29,9 @@ import java.util.List; - +/** + * ArrayRangeOfFunction class + */ public class ArrayRangeOfFunction extends BaseArrayFunction { @Override diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/array/ScaleArrayFormulaFunction.java b/core/formula/src/main/java/org/csstudio/apputil/formula/array/ScaleArrayFormulaFunction.java index 5cfe319e87..6345958fc4 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/array/ScaleArrayFormulaFunction.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/array/ScaleArrayFormulaFunction.java @@ -62,7 +62,7 @@ public boolean isVarArgs() { * factor specified as the second argument. Optionally the result is offset by * the third argument, which may be positive or negative. If the input array is * not numerical, {@link BaseArrayFunction#DEFAULT_NAN_DOUBLE_ARRAY} is returned. - * @throws Exception + * @throws Exception on error */ @Override public VType compute(VType... args) throws Exception{ diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitAND.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitAND.java new file mode 100644 index 0000000000..4c02ebb87e --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitAND.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitAND extends TwoArgBitwiseOperation +{ + public BitAND() + { + super("bitAND", "Bitwise AND (x, y)", (a, b) -> a & b); + } +} \ No newline at end of file diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitLeftShift.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitLeftShift.java new file mode 100644 index 0000000000..5e7d5e4fa4 --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitLeftShift.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitLeftShift extends TwoArgBitwiseOperation +{ + public BitLeftShift() + { + super("bitLeftShift", "Bitwise Left Shift (x, y)", (a, b) -> a << b); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitNOT.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitNOT.java new file mode 100644 index 0000000000..4008ecdb70 --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitNOT.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitNOT extends OneArgBitwiseOperation +{ + public BitNOT() + { + super("bitNOT", "Bitwise NOT (x)", a -> ~a); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitOR.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitOR.java new file mode 100644 index 0000000000..e9b8b0ee99 --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitOR.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitOR extends TwoArgBitwiseOperation +{ + public BitOR() + { + super("bitOR", "Bitwise OR (x, y)", (a, b) -> a | b); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitRightShift.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitRightShift.java new file mode 100644 index 0000000000..a8e548b90c --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitRightShift.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitRightShift extends TwoArgBitwiseOperation +{ + public BitRightShift() + { + super("bitRightShift", "Bitwise Right Shift (x, y)", (a, b) -> a >> b); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitXOR.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitXOR.java new file mode 100644 index 0000000000..6b3fea709e --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/BitXOR.java @@ -0,0 +1,9 @@ +package org.csstudio.apputil.formula.bitwise; + +public class BitXOR extends TwoArgBitwiseOperation +{ + public BitXOR() + { + super("bitXOR", "Bitwise XOR (x, y)", (a, b) -> a ^ b); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/OneArgBitwiseOperation.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/OneArgBitwiseOperation.java new file mode 100644 index 0000000000..3a3556873f --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/OneArgBitwiseOperation.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.csstudio.apputil.formula.bitwise; + +import java.util.List; + +import org.csstudio.apputil.formula.spi.FormulaFunction; +import org.epics.vtype.Alarm; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VDouble; +import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VTypeHelper; + +/** Helper for SPI-provided `long operation(long)` + * @author Mathis Huriez + */ +@SuppressWarnings("nls") +class OneArgBitwiseOperation implements FormulaFunction +{ + @FunctionalInterface + public interface OneArgOperation + { + long calc(long arg); + } + + private final String name; + private final String description; + private final OneArgOperation operation; + + protected OneArgBitwiseOperation(final String name, final String description, final OneArgOperation operation) + { + this.name = name; + this.description = description; + this.operation = operation; + } + + @Override + public String getCategory() { + return "bitwise"; + } + + @Override + public String getName() + { + return name; + } + + @Override + public String getDescription() + { + return description; + } + + @Override + public List getArguments() + { + return List.of("x"); + } + + @Override + public VType compute(final VType... args) throws Exception + { + final double arg = VTypeHelper.toDouble(args[0]); + final long a = (long) arg; + if((double) a != arg) + throw new Exception("Operation " + getName() + + " takes integer type but received floating-point type"); + final long value = operation.calc(a); + return VDouble.of(value, Alarm.none(), Time.now(), Display.none()); + } + +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/TwoArgBitwiseOperation.java b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/TwoArgBitwiseOperation.java new file mode 100644 index 0000000000..58b4ef7f74 --- /dev/null +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/bitwise/TwoArgBitwiseOperation.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2019-2020 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.csstudio.apputil.formula.bitwise; + +import java.util.List; + +import org.csstudio.apputil.formula.spi.FormulaFunction; +import org.epics.vtype.Alarm; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VDouble; +import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VTypeHelper; + +/** Helper for SPI-provided `long operation(long, long)` + * @author Mathis Huriez + */ +@SuppressWarnings("nls") +class TwoArgBitwiseOperation implements FormulaFunction +{ + @FunctionalInterface + public interface TwoArgOperation + { + long calc(long a, long b); + } + + private final String name; + private final String description; + private final TwoArgOperation operation; + + protected TwoArgBitwiseOperation(final String name, final String description, final TwoArgOperation operation) + { + this.name = name; + this.description = description; + this.operation = operation; + } + + @Override + public String getCategory() { + return "bitwise"; + } + + @Override + public String getName() + { + return name; + } + + @Override + public String getDescription() + { + return description; + } + + @Override + public List getArguments() + { + return List.of("x", "y"); + } + + @Override + public VType compute(final VType... args) throws Exception + { + final double arg_a = VTypeHelper.toDouble(args[0]); + final double arg_b = VTypeHelper.toDouble(args[1]); + final long a = (long) arg_a, b = (long) arg_b; + // Check if the conversion is accurate, else, send an exception + if((double) a != arg_a || (double) b != arg_b) + throw new Exception("Operation " + getName() + + " takes integer types but received floating-point types"); + final long value = operation.calc(a, b); + return VDouble.of(value, Alarm.none(), Time.now(), Display.none()); + } +} diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ACos.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ACos.java index e02627a9ef..997c90c40e 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ACos.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ACos.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Acos class see @Math acos function + */ public class ACos extends OneArgMathFunction { - public ACos() + /** + * Constructor + */ + public ACos() { super("acos", "Inverse cosine", Math::acos); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ASin.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ASin.java index 03f3ca00ba..e003a06e89 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ASin.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ASin.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * ASin class + */ public class ASin extends OneArgMathFunction { - public ASin() + /** + * Constructor + */ + public ASin() { super("asin", "Inverse sine", Math::asin); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan.java index a141d97645..645900e724 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan.java @@ -1,8 +1,13 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * ATan class see @Math atan function + */ public class ATan extends OneArgMathFunction { + /** + * Constructor + */ public ATan() { super("atan", "Inverse tangent", Math::atan); diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan2.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan2.java index 2814a2d44c..b392cf3898 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan2.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ATan2.java @@ -2,10 +2,15 @@ import java.util.List; -@SuppressWarnings("nls") +/** + * ATan2 class see @Math atan2 function + */ public class ATan2 extends TwoArgMathFunction { - public ATan2() + /** + * Constructor + */ + public ATan2() { super("atan2", "Inverse tangent (y, x)", Math::atan2); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Abs.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Abs.java index edcceba76f..fca71714cc 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Abs.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Abs.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Abs class see @Math abs function + */ public class Abs extends OneArgMathFunction { - public Abs() + /** + * Constructor + */ + public Abs() { super("abs", "Absolute value", Math::abs); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Ceil.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Ceil.java index 6a4d1e9053..5ec5e53a6f 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Ceil.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Ceil.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Ceil class see @Math ceil function + */ public class Ceil extends OneArgMathFunction { - public Ceil() + /** + * Constructor + */ + public Ceil() { super("ceil", "Ceiling", Math::ceil); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Cos.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Cos.java index 94932fa7ec..aab02a6913 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Cos.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Cos.java @@ -2,10 +2,15 @@ import java.util.List; -@SuppressWarnings("nls") +/** + * Cos class see @Math cos function + */ public class Cos extends OneArgMathFunction { - public Cos() + /** + * Constructor + */ + public Cos() { super("cos", "Cosine", Math::cos); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/CosH.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/CosH.java index b6581fd128..27620f0742 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/CosH.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/CosH.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * CosH class see @Math cosh function + */ public class CosH extends OneArgMathFunction { - public CosH() + /** + * Constructor + */ + public CosH() { super("cosh", "Hyperbolic cosine", Math::cosh); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Exp.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Exp.java index 45f5dfb69c..0e23e32ecb 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Exp.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Exp.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Exp class see @Math exp function + */ public class Exp extends OneArgMathFunction { - public Exp() + /** + * Constructor + */ + public Exp() { super("exp", "Exponential", Math::exp); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ExpM1.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ExpM1.java index 67e60cd6bb..ffd6b4ea40 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ExpM1.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ExpM1.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * ExpM1 class see @Math expm1 function + */ public class ExpM1 extends OneArgMathFunction { - public ExpM1() + /** + * Constructor + */ + public ExpM1() { super("expm1", "exp(x)-1", Math::expm1); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Floor.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Floor.java index 405198a120..cb48e1ba7a 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Floor.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Floor.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Floor class see @Math floor function + */ public class Floor extends OneArgMathFunction { - public Floor() + /** + * Constructor + */ + public Floor() { super("floor", "Floor", Math::floor); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Hypot.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Hypot.java index 4eeec9d04b..875e1af6f4 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Hypot.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Hypot.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Hypot class see @Math hypot function + */ public class Hypot extends TwoArgMathFunction { - public Hypot() + /** + * Constructor + */ + public Hypot() { super("hypot", "Hypotenuse (x, y)", Math::hypot); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log.java index fadee5606b..e92719fe2f 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Log class see @Math log function + */ public class Log extends OneArgMathFunction { - public Log() + /** + * Constructor + */ + public Log() { super("log", "Natural logarithm", Math::log); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log10.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log10.java index 54d929ef00..ebc738c9e6 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log10.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Log10.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Log10 class see @Math log10 function + */ public class Log10 extends OneArgMathFunction { - public Log10() + /** + * Constructor + */ + public Log10() { super("log10", "Decadic logarithm", Math::log10); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Pow.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Pow.java index a11ff582d9..e3ec56936d 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Pow.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Pow.java @@ -2,10 +2,15 @@ import java.util.List; -@SuppressWarnings("nls") +/** + * Pow class see @Math pow function + */ public class Pow extends TwoArgMathFunction { - public Pow() + /** + * Constructor + */ + public Pow() { super("pow", "Power (base, exponent)", Math::pow); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Round.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Round.java index 88300b6f9e..7401d8b3c1 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Round.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Round.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Round class see @Math round function + */ public class Round extends OneArgMathFunction { - public Round() + /** + * Constructor + */ + public Round() { super("round", "Round", Math::round); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sin.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sin.java index a21c4b186a..abfaeec1fc 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sin.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sin.java @@ -2,10 +2,15 @@ import java.util.List; -@SuppressWarnings("nls") +/** + * Sin class see @Math sin function + */ public class Sin extends OneArgMathFunction { - public Sin() + /** + * Constructor + */ + public Sin() { super("sin", "Sine", Math::sin); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/SinH.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/SinH.java index 6db01f5b81..a75cc9f10c 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/SinH.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/SinH.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * SinH class see @Math sinh function + */ public class SinH extends OneArgMathFunction { - public SinH() + /** + * Constructor + */ + public SinH() { super("sinh", "Hyperbolic sine", Math::sinh); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sqrt.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sqrt.java index 0cc0e3ba88..f5e6847c99 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sqrt.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Sqrt.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Sqrt class see @Math sqrt function + */ public class Sqrt extends OneArgMathFunction { - public Sqrt() + /** + * Constructor + */ + public Sqrt() { super("sqrt", "Square Root", Math::sqrt); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Tan.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Tan.java index ccf6cbf088..1ddbfcd204 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/Tan.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/Tan.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * Tan class see @Math tan function + */ public class Tan extends OneArgMathFunction { - public Tan() + /** + * Constructor + */ + public Tan() { super("tan", "Tangent", Math::tan); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/TanH.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/TanH.java index bcc9e373ff..5767be4cdd 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/TanH.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/TanH.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * TanH class see @Math tanh function + */ public class TanH extends OneArgMathFunction { - public TanH() + /** + * Constructor + */ + public TanH() { super("tanh", "Hyperbolic tangent", Math::tanh); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToDegrees.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToDegrees.java index 0cb80de4f4..57cbeb6fbe 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToDegrees.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToDegrees.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * ToDegrees class see @Math toDegrees function + */ public class ToDegrees extends OneArgMathFunction { - public ToDegrees() + /** + * Constructor + */ + public ToDegrees() { super("toDegrees", "Radians to degrees", Math::toDegrees); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToRadians.java b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToRadians.java index 6695b4b8c8..7bcfe6fe96 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToRadians.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/math/ToRadians.java @@ -1,9 +1,14 @@ package org.csstudio.apputil.formula.math; -@SuppressWarnings("nls") +/** + * ToRadians class see @Math toRadians function + */ public class ToRadians extends OneArgMathFunction { - public ToRadians() + /** + * Constructor + */ + public ToRadians() { super("toRadians", "Degrees to radians", Math::toRadians); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/AddNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/AddNode.java index 0e5484de2c..e89c72d0f3 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/AddNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/AddNode.java @@ -19,7 +19,12 @@ */ public class AddNode extends AbstractBinaryNode { - public AddNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right, right node + */ + public AddNode(final Node left, final Node right) { super(left, right); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/AndNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/AndNode.java index 929f0af14c..6404740d47 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/AndNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/AndNode.java @@ -14,7 +14,12 @@ */ public class AndNode extends AbstractBinaryNode { - public AndNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public AndNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a != 0.0 && b != 0.0 ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/ConstantNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/ConstantNode.java index ed3008e8ff..ec7e99c907 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/ConstantNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/ConstantNode.java @@ -19,16 +19,23 @@ /** One computational node. * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ConstantNode implements Node { final VType value; + /** + * Constructor + * @param value , value of constant in double + */ public ConstantNode(final double value) { this.value = VDouble.of(value, Alarm.none(), Time.now(), Display.none()); } + /** + * Constructor + * @param value , value of constant in String + */ public ConstantNode(final String value) { this.value = VString.of(value, Alarm.none(), Time.now()); diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/DivNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/DivNode.java index d4e0de30ed..ff5e6dd9f0 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/DivNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/DivNode.java @@ -14,7 +14,12 @@ */ public class DivNode extends AbstractBinaryNode { - public DivNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public DivNode(final Node left, final Node right) { super(left, right); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/EqualNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/EqualNode.java index 81431db45b..b75af929bd 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/EqualNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/EqualNode.java @@ -14,7 +14,12 @@ */ public class EqualNode extends AbstractBinaryNode { - public EqualNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public EqualNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a == b ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterEqualNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterEqualNode.java index ba1eaeff6c..03c13a6997 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterEqualNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterEqualNode.java @@ -14,7 +14,12 @@ */ public class GreaterEqualNode extends AbstractBinaryNode { - public GreaterEqualNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public GreaterEqualNode(final Node left, final Node right) { super(left, right); } diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterThanNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterThanNode.java index 76d1010895..6b3c60ab45 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterThanNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/GreaterThanNode.java @@ -14,7 +14,12 @@ */ public class GreaterThanNode extends AbstractBinaryNode { - public GreaterThanNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public GreaterThanNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a > b ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/IfNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/IfNode.java index cd38d3587e..17a5ac5e0b 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/IfNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/IfNode.java @@ -20,6 +20,12 @@ public class IfNode implements Node private final Node yes; private final Node no; + /** + * Constructor + * @param cond , condition Node + * @param yes , True Node + * @param no , False Node + */ public IfNode(final Node cond, final Node yes, final Node no) { this.cond = cond; @@ -65,7 +71,6 @@ public boolean hasSubnode(final String name) no.hasSubnode(name); } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessEqualNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessEqualNode.java index c6387ca387..265d7aad38 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessEqualNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessEqualNode.java @@ -14,7 +14,12 @@ */ public class LessEqualNode extends AbstractBinaryNode { - public LessEqualNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public LessEqualNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a <= b ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessThanNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessThanNode.java index 0fd9226410..7185731597 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessThanNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/LessThanNode.java @@ -14,7 +14,12 @@ */ public class LessThanNode extends AbstractBinaryNode { - public LessThanNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public LessThanNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a < b ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MaxNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MaxNode.java index f24e172ef1..e4f56c91c6 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MaxNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MaxNode.java @@ -22,9 +22,13 @@ public class MaxNode implements Node { private final Node args[]; - public MaxNode(final Node args[]) + /** + * Constructor + * @param nodeArray , arrays of nodes + */ + public MaxNode(final Node nodeArray[]) { - this.args = args; + this.args = nodeArray; } @Override @@ -63,7 +67,6 @@ public boolean hasSubnode(final String name) return false; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MinNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MinNode.java index 685ce5b97d..d0991d8433 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MinNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MinNode.java @@ -22,9 +22,13 @@ public class MinNode implements Node { private final Node args[]; - public MinNode(final Node args[]) + /** + * Constructor + * @param nodeArray , arrays of nodes + */ + public MinNode(final Node nodeArray[]) { - this.args = args; + this.args = nodeArray; } @Override diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MulNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MulNode.java index fe2045d966..238cdb06e7 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/MulNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/MulNode.java @@ -14,7 +14,12 @@ */ public class MulNode extends AbstractBinaryNode { - public MulNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public MulNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a*b; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotEqualNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotEqualNode.java index 9ee5557297..2bd478701d 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotEqualNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotEqualNode.java @@ -14,7 +14,12 @@ */ public class NotEqualNode extends AbstractBinaryNode { - public NotEqualNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public NotEqualNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a != b ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotNode.java index e20f921759..3d7edd1cff 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/NotNode.java @@ -14,7 +14,11 @@ */ public class NotNode extends AbstractUnaryNode { - public NotNode(final Node n) + /** + * Constructor + * @param n , node to invert + */ + public NotNode(final Node n) { super(n); } @@ -25,7 +29,6 @@ protected double calc(final double a) return a != 0.0 ? 0.0 : 1.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/OrNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/OrNode.java index 70563a5eb7..7c9a0a85c5 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/OrNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/OrNode.java @@ -14,7 +14,12 @@ */ public class OrNode extends AbstractBinaryNode { - public OrNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public OrNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a != 0.0 || b != 0.0 ? 1.0 : 0.0; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/PwrNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/PwrNode.java index 9a5bd78a2b..132068726e 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/PwrNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/PwrNode.java @@ -14,7 +14,12 @@ */ public class PwrNode extends AbstractBinaryNode { - public PwrNode(final Node left, final Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public PwrNode(final Node left, final Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return Math.pow(a, b); } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/RndNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/RndNode.java index 353d621f85..280636fe9b 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/RndNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/RndNode.java @@ -14,7 +14,11 @@ */ public class RndNode extends AbstractUnaryNode { - public RndNode(Node n) + /** + * Constructor + * @param n , the node to random + */ + public RndNode(Node n) { super(n); } @@ -25,7 +29,6 @@ protected double calc(final double a) return a * Math.random(); } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/SPIFuncNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/SPIFuncNode.java index 94f5f79d34..058144e746 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/SPIFuncNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/SPIFuncNode.java @@ -22,7 +22,6 @@ /** Node for evaluating an SPI-provided function * @author Kay Kasemir */ -@SuppressWarnings("nls") public class SPIFuncNode implements Node { final private FormulaFunction function; @@ -31,12 +30,12 @@ public class SPIFuncNode implements Node /** Construct node for SPI function. * * @param function {@link FormulaFunction} - * @param n Argument node + * @param nodeArray Argument node array */ - public SPIFuncNode(final FormulaFunction function, final Node args[]) + public SPIFuncNode(final FormulaFunction function, final Node nodeArray[]) { this.function = function; - this.args = args; + this.args = nodeArray; // Should be called with the correct number of arguments if (!function.isVarArgs() && args.length != function.getArguments().size()) throw new IllegalStateException("Wrong number of arguments for " + function.getSignature()); diff --git a/core/formula/src/main/java/org/csstudio/apputil/formula/node/SubNode.java b/core/formula/src/main/java/org/csstudio/apputil/formula/node/SubNode.java index 6893f4e1dc..a659b02537 100644 --- a/core/formula/src/main/java/org/csstudio/apputil/formula/node/SubNode.java +++ b/core/formula/src/main/java/org/csstudio/apputil/formula/node/SubNode.java @@ -14,7 +14,12 @@ */ public class SubNode extends AbstractBinaryNode { - public SubNode(Node left, Node right) + /** + * Constructor + * @param left , left node + * @param right , right node + */ + public SubNode(Node left, Node right) { super(left, right); } @@ -25,7 +30,6 @@ protected double calc(final double a, final double b) return a-b; } - @SuppressWarnings("nls") @Override public String toString() { diff --git a/core/formula/src/main/resources/META-INF/services/org.csstudio.apputil.formula.spi.FormulaFunction b/core/formula/src/main/resources/META-INF/services/org.csstudio.apputil.formula.spi.FormulaFunction index 767b83f54e..7ac24eb076 100644 --- a/core/formula/src/main/resources/META-INF/services/org.csstudio.apputil.formula.spi.FormulaFunction +++ b/core/formula/src/main/resources/META-INF/services/org.csstudio.apputil.formula.spi.FormulaFunction @@ -11,6 +11,7 @@ org.csstudio.apputil.formula.math.ExpM1 org.csstudio.apputil.formula.math.Floor org.csstudio.apputil.formula.math.Log org.csstudio.apputil.formula.math.Log10 +org.csstudio.apputil.formula.math.Round org.csstudio.apputil.formula.math.Sin org.csstudio.apputil.formula.math.SinH org.csstudio.apputil.formula.math.Sqrt @@ -55,6 +56,14 @@ org.csstudio.apputil.formula.array.ArrayStatsFunction org.csstudio.apputil.formula.array.ArrayMaxFunction org.csstudio.apputil.formula.array.ArrayMinFunction +# Bitwise Operation +org.csstudio.apputil.formula.bitwise.BitAND +org.csstudio.apputil.formula.bitwise.BitOR +org.csstudio.apputil.formula.bitwise.BitXOR +org.csstudio.apputil.formula.bitwise.BitNOT +org.csstudio.apputil.formula.bitwise.BitLeftShift +org.csstudio.apputil.formula.bitwise.BitRightShift + # Alarm org.csstudio.apputil.formula.alarm.HighestSeverityFunction org.csstudio.apputil.formula.alarm.MajorAlarmFunction diff --git a/core/formula/src/test/java/org/csstudio/apputil/formula/FormulaUnitTest.java b/core/formula/src/test/java/org/csstudio/apputil/formula/FormulaUnitTest.java index c142b74abe..34fadd7872 100644 --- a/core/formula/src/test/java/org/csstudio/apputil/formula/FormulaUnitTest.java +++ b/core/formula/src/test/java/org/csstudio/apputil/formula/FormulaUnitTest.java @@ -194,6 +194,24 @@ public void testFunctions() throws Exception { // usually, should NOT get the same number twice... assertTrue(rnd != rnd2); } + + f = new Formula("round(12.3)"); + assertEquals(12, VTypeHelper.toDouble(f.eval()), epsilon); + + f = new Formula("bitOR(5, 7)"); + assertEquals(7, VTypeHelper.toDouble(f.eval()), epsilon); + + f = new Formula("bitXOR(5, 7)"); + assertEquals(2, VTypeHelper.toDouble(f.eval()), epsilon); + + f = new Formula("bitRightShift(2, 4)"); + assertEquals(0, VTypeHelper.toDouble(f.eval()), epsilon); + + f = new Formula("bitRightShift(4, 2)"); + assertEquals(1, VTypeHelper.toDouble(f.eval()), epsilon); + + f = new Formula("bitNOT(5)"); + assertEquals(-6, VTypeHelper.toDouble(f.eval()), epsilon); } @Test @@ -257,6 +275,9 @@ public void testErrors() throws Exception { f = new Formula("sqrt(-1)"); assertTrue(Double.isNaN(VTypeHelper.toDouble(f.eval()))); + + f = new Formula("bitXOR(5.3, 7)"); + assertTrue(Double.isNaN(VTypeHelper.toDouble(f.eval()))); } @Test diff --git a/core/framework/.classpath b/core/framework/.classpath index 7eb3aa5218..08fd8814e6 100644 --- a/core/framework/.classpath +++ b/core/framework/.classpath @@ -6,6 +6,6 @@ - + diff --git a/core/framework/pom.xml b/core/framework/pom.xml index 47df3a25f5..b0b3076dd1 100644 --- a/core/framework/pom.xml +++ b/core/framework/pom.xml @@ -4,13 +4,13 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-formula - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT compile diff --git a/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterFactory.java b/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterFactory.java index 08cd7da118..352672a36f 100644 --- a/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterFactory.java +++ b/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterFactory.java @@ -25,8 +25,9 @@ public interface AdapterFactory { /** * The function to adapt an object adaptableObject to type adapterType - * @param adaptableObject - * @param adapterType + * @param , the type to convert + * @param adaptableObject , the object to adapt + * @param adapterType , the target type * @return the adapted object */ public Optional adapt(Object adaptableObject, Class adapterType); diff --git a/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterService.java b/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterService.java index f05b387c6c..956298b2f8 100644 --- a/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterService.java +++ b/core/framework/src/main/java/org/phoebus/framework/adapter/AdapterService.java @@ -89,9 +89,9 @@ public static List getAdaptersforAdaptable(Class cls) /** * Adapts the adaptableObject to type adapterType using registered adaptor factories. - * - * @param adaptableObject - * @param adapterType + * @param , the type to convert + * @param adaptableObject , the object to adapt + * @param adapterType , the target type * @return an {@link Optional} with the the adapted object or empty */ public static Optional adapt(Object adaptableObject, Class adapterType) diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaFunctionProposal.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaFunctionProposal.java index d3e0a3f555..4ab4581280 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaFunctionProposal.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaFunctionProposal.java @@ -85,7 +85,7 @@ public List getMatch(final String text) { // Have text for this argument. segs.add(MatchSegment.match(text.substring(pos, end), - function.getArguments().get(i))); + function.getArguments().get(i))); if (text.charAt(end) == ')') { segs.add(MatchSegment.match(")")); @@ -101,7 +101,7 @@ public List getMatch(final String text) else { segs.add(MatchSegment.comment(text.substring(pos), - function.getArguments().get(i))); + function.getArguments().get(i))); pos = end = -1; } } @@ -130,4 +130,4 @@ public List getMatch(final String text) return segs; } -} +} \ No newline at end of file diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaProposalProvider.java index c2ccb49bbb..d33eddb34b 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/FormulaProposalProvider.java @@ -18,10 +18,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class FormulaProposalProvider implements PVProposalProvider { - public static final FormulaProposalProvider INSTANCE = new FormulaProposalProvider(); + /** + * Singleton pattern + */ + public static final FormulaProposalProvider INSTANCE = new FormulaProposalProvider(); private final List generic = List.of(new Proposal("=2*`pv_name`")); @@ -73,4 +75,4 @@ public List lookup(final String text) return result; } -} +} \ No newline at end of file diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/History.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/History.java index c2c64265b1..ddb7f79339 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/History.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/History.java @@ -23,12 +23,14 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class History implements ProposalProvider { private final LinkedList history = new LinkedList<>(); private final int max_size; + /** + * Constructor + */ public History() { this(10); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposal.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposal.java index 1e20f5787e..25ca4e7dc4 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposal.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposal.java @@ -13,7 +13,7 @@ /** Proposal for "loc://..." PVs * *

    Description includes the optional type and initial value, - * which are shown as {@link MatchSegment#COMMENT} + * which are shown as {@link MatchSegment #COMMENT} * until the user provides parameters for them. * *

    When applied to user text, @@ -22,12 +22,17 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class LocProposal extends Proposal { private final String type; private final String[] initial_values; + /** + * Constructor + * @param name , name + * @param type , type + * @param initial_values , initial value + */ public LocProposal(final String name, final String type, final String... initial_values) { super(name); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposalProvider.java index 1eb7bd6781..b1c3f75a6c 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/LocProposalProvider.java @@ -16,10 +16,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class LocProposalProvider implements PVProposalProvider { - public static final LocProposalProvider INSTANCE = new LocProposalProvider(); + /** + * Singleton + */ + public static final LocProposalProvider INSTANCE = new LocProposalProvider(); private static final List generic = List.of(new LocProposal("loc://name", "VType", "initial value...")); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MatchSegment.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MatchSegment.java index 9cb7e483c3..5daf0a6e8d 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MatchSegment.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MatchSegment.java @@ -10,10 +10,12 @@ /** Description of a match between entered text and a {@link Proposal} * @author Kay Kasemir */ -@SuppressWarnings("nls") public class MatchSegment { - public static enum Type + /** + * Type enumeration + */ + public static enum Type { /** Text from proposal which does not appear in entered text. * If proposal is applied, this text will be used. diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposal.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposal.java index 8f68d645dd..e74f382b4e 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposal.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposal.java @@ -13,16 +13,20 @@ /** Proposal for "mqtt://..." PVs * *

    Description includes the optional type, - * which are shown as {@link MatchSegment#COMMENT} + * which are shown as {@link MatchSegment #COMMENT} * until the user provides parameters for them. * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class MqttProposal extends Proposal { private final String type; + /** + * Constructor + * @param path mqtt path + * @param type type + */ public MqttProposal(final String path, final String type) { super(path); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposalProvider.java index 2c1b027403..1fb9f174d2 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/MqttProposalProvider.java @@ -16,10 +16,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class MqttProposalProvider implements PVProposalProvider { - public static final MqttProposalProvider INSTANCE = new MqttProposalProvider(); + /** + * Singleton + */ + public static final MqttProposalProvider INSTANCE = new MqttProposalProvider(); private static final List generic = List.of(new MqttProposal("mqtt://path", "VType")); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/PVProposalService.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/PVProposalService.java index 6818f88bb3..7e885a86da 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/PVProposalService.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/PVProposalService.java @@ -15,10 +15,12 @@ /** Autocompletion Service for PVs * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PVProposalService extends ProposalService { - public static final PVProposalService INSTANCE = new PVProposalService(); + /** + * Singleton + */ + public static final PVProposalService INSTANCE = new PVProposalService(); private PVProposalService() { diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/ProposalService.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/ProposalService.java index a849e96af6..79a62836be 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/ProposalService.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/ProposalService.java @@ -26,12 +26,14 @@ * @see PVProposalService * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ProposalService { /** Logger for autocompletion */ public static final Logger logger = Logger.getLogger(ProposalService.class.getPackageName()); + /** + * functional interface Handler + */ @FunctionalInterface public interface Handler { @@ -49,6 +51,10 @@ public interface Handler protected final List providers; private final List> submitted = new ArrayList<>(); + /** + * Constructor + * @param providers the list of provider + */ public ProposalService(final ProposalProvider... providers) { this.providers = new ArrayList<>(providers.length + 1); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/PvaProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/PvaProposalProvider.java index ff09b6d65b..a188de70f6 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/PvaProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/PvaProposalProvider.java @@ -16,10 +16,12 @@ /** {@link ProposalProvider} for PVA * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PvaProposalProvider implements PVProposalProvider { - public static final PvaProposalProvider INSTANCE = new PvaProposalProvider(); + /** + * Singleton + */ + public static final PvaProposalProvider INSTANCE = new PvaProposalProvider(); private static final Pattern PLAIN_NAME = Pattern.compile("[a-zA-Z]\\w*"); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposal.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposal.java index 3fae81ecef..c5082ec4cd 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposal.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposal.java @@ -13,7 +13,7 @@ /** Proposal for "sim://..." PVs * *

    Description includes the optional parameters, - * which are shown as {@link MatchSegment#COMMENT} + * which are shown as {@link MatchSegment #COMMENT} * until the user provides a value for a parameter. * *

    When applied to user text, @@ -22,11 +22,15 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class SimProposal extends Proposal { private final String[] arguments; + /** + * Constructor + * @param name value of parameter + * @param arguments list of arguments + */ public SimProposal(final String name, final String... arguments) { super(name); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposalProvider.java index 419d1ee069..ebce10e246 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SimProposalProvider.java @@ -16,10 +16,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class SimProposalProvider implements PVProposalProvider { - public static final SimProposalProvider INSTANCE = new SimProposalProvider(); + /** + * Singleton + */ + public static final SimProposalProvider INSTANCE = new SimProposalProvider(); private static final List generic = List.of( new SimProposal("sim://name", "parameters...")); diff --git a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SysProposalProvider.java b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SysProposalProvider.java index df78f4118e..940471e2a7 100644 --- a/core/framework/src/main/java/org/phoebus/framework/autocomplete/SysProposalProvider.java +++ b/core/framework/src/main/java/org/phoebus/framework/autocomplete/SysProposalProvider.java @@ -16,10 +16,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class SysProposalProvider implements PVProposalProvider { - public static final SysProposalProvider INSTANCE = new SysProposalProvider(); + /** + * Singleton + */ + public static final SysProposalProvider INSTANCE = new SysProposalProvider(); private static final List generic = List.of( new Proposal("sys://name")); diff --git a/core/framework/src/main/java/org/phoebus/framework/jobs/CommandExecutor.java b/core/framework/src/main/java/org/phoebus/framework/jobs/CommandExecutor.java index 0d8d4472bb..57795ae98c 100644 --- a/core/framework/src/main/java/org/phoebus/framework/jobs/CommandExecutor.java +++ b/core/framework/src/main/java/org/phoebus/framework/jobs/CommandExecutor.java @@ -29,7 +29,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class CommandExecutor implements Callable { /** Seconds to wait for a launched program */ @@ -38,6 +37,11 @@ public class CommandExecutor implements Callable private final ProcessBuilder process_builder; private volatile Process process; + /** + * Constructor + * @param cmd command line + * @param directory execution + */ public CommandExecutor(final String cmd, final File directory) { final List cmd_parts = splitCmd(cmd); diff --git a/core/framework/src/main/java/org/phoebus/framework/jobs/JobRunnableWithCancel.java b/core/framework/src/main/java/org/phoebus/framework/jobs/JobRunnableWithCancel.java index 645b00b647..6a6438dab7 100644 --- a/core/framework/src/main/java/org/phoebus/framework/jobs/JobRunnableWithCancel.java +++ b/core/framework/src/main/java/org/phoebus/framework/jobs/JobRunnableWithCancel.java @@ -16,7 +16,7 @@ * JobManager.schedule("Demo", monitor -> * { * monitor.beginTask("What I do", 100); - * for (int step=0; step < 100; ++step) + * {@literal for (int step=0; step < 100; ++step)} * { * // Allow cancellation * if (monitor.isCanceled()) diff --git a/core/framework/src/main/java/org/phoebus/framework/jobs/LogWriter.java b/core/framework/src/main/java/org/phoebus/framework/jobs/LogWriter.java index b3abb358ff..54d3d74214 100644 --- a/core/framework/src/main/java/org/phoebus/framework/jobs/LogWriter.java +++ b/core/framework/src/main/java/org/phoebus/framework/jobs/LogWriter.java @@ -17,13 +17,18 @@ /** Thread that writes data from stream to log * @author Kay Kasemir */ -@SuppressWarnings("nls") public class LogWriter extends Thread { private final BufferedReader reader; private final String cmd; private Level level; + /** + * Constructor + * @param stream input + * @param cmd command + * @param level logging level see @Level + */ public LogWriter(final InputStream stream, final String cmd, final Level level) { super("LogWriter " + level.getName() + " " + cmd); diff --git a/core/framework/src/main/java/org/phoebus/framework/jobs/NamedThreadFactory.java b/core/framework/src/main/java/org/phoebus/framework/jobs/NamedThreadFactory.java index d30a066ec4..06c585214e 100644 --- a/core/framework/src/main/java/org/phoebus/framework/jobs/NamedThreadFactory.java +++ b/core/framework/src/main/java/org/phoebus/framework/jobs/NamedThreadFactory.java @@ -23,6 +23,10 @@ public class NamedThreadFactory implements ThreadFactory final private String name; final private AtomicInteger count = new AtomicInteger(1); + /** + * Constructor + * @param name of factory + */ public NamedThreadFactory(final String name) { this.name = name; diff --git a/core/framework/src/main/java/org/phoebus/framework/macros/Macros.java b/core/framework/src/main/java/org/phoebus/framework/macros/Macros.java index e620bcd7b2..f71f041827 100644 --- a/core/framework/src/main/java/org/phoebus/framework/macros/Macros.java +++ b/core/framework/src/main/java/org/phoebus/framework/macros/Macros.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2018 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,43 +7,73 @@ *******************************************************************************/ package org.phoebus.framework.macros; +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleImmutableEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; -/** Macro information +/** Macro specifications and expanded runtime values + * + *

    Macro specifications are an ordered list of name-value-pairs: + *

    + *  A=a
    + *  B=b
    + *  X=$(A)
    + *  A=$(B)
    + *  B=$(X)
    + *  
    + * + *

    Macro specifications are used to configure for example + * display widgets. + * At runtime, they are expanded, typically based on macros + * passed in from a container. + * Assuming that a container provides + *

    COUNT=42
    + * the above specification will result in + *
    + *  A=b
    + *  B=a
    + *  COUNT=42
    + *  X=a
    + *  
    * - *

    Holds macros and their value * @author Kay Kasemir */ -@SuppressWarnings("nls") public class Macros implements MacroValueProvider { /** Logger for macro related messages */ public static final Logger logger = Logger.getLogger(Macros.class.getPackageName()); - // Using linked map for predictable order. - // - // Example, a tool that tries to first "save" a current macro value in another macro, - // then set it to a new value like this depends on the order of macros: - // SAVE = $(M), M = "new value" - // - // (At the same time, getNames() always sorts alphabetically, so does order still matter?) - // - // SYNC on access - private final Map macros = new LinkedHashMap<>(); - /** Regular expression for valid macro names */ public final static Pattern MACRO_NAME_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_.\\-\\[\\]]*"); + /** List of name-value pairs for macro specification. + * Order of macro definitions is preserved. + * Multiple specifications for the same macro will re-define its value. + * Thread-safe using COWAL assuming that specifications are usually short, + * between none and just a few per display widget. + */ + private final CopyOnWriteArrayList> specs = new CopyOnWriteArrayList<>(); + + /** Map of macro name to (expanded) macro value + * Order of macro definitions is not preserved, + * emphasis is on fast lookup of values by name. + * Each macro has exactly one value, holding the + * last one from the specification in case specification + * re-defines it. + * Thread-safe. + */ + private final ConcurrentHashMap values = new ConcurrentHashMap<>(); + /** Check macro name * *

    Permitted macro names are a subset of valid XML names: @@ -158,19 +188,14 @@ public static Macros fromSimpleSpec(final String names_and_values) throws Except if (value == null) throw new Exception("Error parsing '" + names_and_values + "': Missing value"); - // Legacy tools like EDM would allow "S=$(S)" to pass existing macros, - // which we now consider a recursive macro error, so ignore. - if (value.equals("$(" + name + ")")) - logger.log(Level.WARNING, "Ignoring recursive macro " + name + " = " + value); - else - macros.add(name, value); + macros.add(name, value); pos = end; } return macros; } - /** Create empty macro map */ + /** Create empty macros */ public Macros() { } @@ -181,158 +206,166 @@ public Macros() public Macros(final Macros other) { if (other != null) - synchronized (other.macros) - { - synchronized (macros) - { - macros.putAll(other.macros); - } - } - } - - /** @return Are the macros empty? */ - public boolean isEmpty() - { - synchronized (macros) { - return macros.isEmpty(); + specs.addAll(other.specs); + values.putAll(other.values); } } - /** Merge two macro maps - * - *

    Optimized for cases where base or addition are empty, - * but will never _change_ any macros. - * If a merge is necessary, it returns a new Macros instance. - * - * @param base Base macros - * @param addition Additional macros that may override 'base' - * @return Merged macros - */ - public static Macros merge(final Macros base, final Macros addition) + /** @return Are there any macro specifications? */ + public boolean isEmpty() { - // Optimize if one is empty - if (addition == null || addition.isEmpty()) - return base; - if (base == null || base.isEmpty()) - return addition; - // Construct new macros - final Macros merged = new Macros(); - synchronized (base.macros) - { - merged.macros.putAll(base.macros); - } - synchronized (addition.macros) - { - merged.macros.putAll(addition.macros); - } - return merged; + return specs.isEmpty(); } - /** Add a macro + /** Add a macro specification * @param name Name of the macro - * @param value Value of the macro + * @param spec Specification of the macro, that is value that might contain "$(NAME)" * @throws IllegalArgumentException for illegal macro name * @see #checkMacroName(String) */ - public void add(final String name, final String value) + public void add(final String name, final String spec) { final String error = checkMacroName(name); if (error != null) throw new IllegalArgumentException(error); - synchronized (macros) + specs.add(new SimpleImmutableEntry<>(name, spec)); + // Expand this name=spec right away so getValue() won't return null. + // Complete expansion typically requires calling expand(base) + expandSpec(name, spec); + } + + /** Expand macro specs into values + * + *

    The macro specifications remain unchanged, + * while the values are set to the input's values, + * then adding the expanded specs. + * @param base Base values (already expanded) to import before expanding specs + */ + public void expandValues(final Macros base) + { + values.clear(); + + // Add all base values (already expanded) + if (base != null) + values.putAll(base.values); + + // Add expanded specs + for (AbstractMap.SimpleImmutableEntry spec : specs) { - macros.put(name, value); + final String name = spec.getKey(); + final String expanded = expandSpec(name, spec.getValue()); + if (MacroHandler.containsMacros(expanded)) + { + // Not fatal, in fact common when creating displays, + // but log with exception to get stack trace in case + // origin of macro needs to be debugged + logger.log(Level.WARNING, "Incomplete macro expansion " + name + "='" + expanded + "'", + new Exception("Macro spec " + name + "='" + spec + "' does not fully resolve")); + } } } - /** @return Macro names, sorted alphabetically */ - public Collection getNames() + /** Expand a macro spec + * @param name Name of the macro + * @param spec Specification of the macro, may contain macros "$(NAME)" or "${NAME}" which will be expanded + * @return Expanded value + */ + private String expandSpec(final String name, final String spec) { - final List names; - synchronized (macros) + String expanded; + try + { + expanded = MacroHandler.replace(this, spec); + } + catch (Exception ex) { - names = new ArrayList<>(macros.keySet()); + logger.log(Level.WARNING, "Failed to expand " + name + "='" + spec + "'", ex); + expanded = spec; } + values.put(name, expanded); + return expanded; + } + + /** @return Names of expanded macros, sorted alphabetically */ + public Collection getNames() + { + final List names = new ArrayList<>(values.keySet()); Collections.sort(names); return names; } - /** Perform given action for each name/value (names are not sorted) - * @param action Invoked with each name/value + /** Get value for macro + * @param name Name of the macro + * @return Expanded value of the macro or null if not defined or macro specs have not been expanded */ - public void forEach(final BiConsumer action) + @Override + public String getValue(final String name) { - synchronized (macros) - { - macros.forEach(action); - } + return values.get(name); } - /** Expand values of all macros - * @param input Value provider, usually from the 'parent' widget - * @throws Exception on error + // Note that there is no "getSpecNames()" and "getSpec(name)" + // because for specifications, the same name might appear more than once + // and order matters. So "getSpecNames()" would have to return + // the same name multiple times, and "getSpec(name)" wouldn't know + // which definition to return. + // forEachSpec() iterates over all specs in the correct order + + /** Visit a thread-safe snapshot of all macro specifications + * @param action Invoked with each macro name and value specification */ - public void expandValues(final MacroValueProvider input) throws Exception + public void forEachSpec(final BiConsumer action) { - synchronized (macros) - { - for (String name : macros.keySet()) - { - final String orig = macros.get(name); - final String expanded = MacroHandler.replace(input, orig); - if (! expanded.equals(orig)) - macros.put(name, expanded); - } - } + specs.forEach(entry -> action.accept(entry.getKey(), entry.getValue())); } - /** {@inheritDoc} */ - @Override - public String getValue(final String name) + /** Visit a thread-safe snapshot of all expanded macro values + * @param action Invoked with each macro name and expanded value + */ + public void forEach(final BiConsumer action) { - synchronized (macros) - { - return macros.get(name); - } + values.forEach(action); } - // Hash based on content + // Hash based on specs @Override public int hashCode() { - synchronized (macros) - { - return macros.hashCode(); - } + return specs.hashCode(); } - // Compare based on content + // Compare based on specs @Override public boolean equals(final Object obj) { if (! (obj instanceof Macros)) return false; final Macros other = (Macros) obj; - synchronized (other.macros) - { - synchronized (macros) - { - return other.macros.equals(macros); - } - } + return other.specs.equals(specs); } - /** @return String representation for debugging */ + /** @return String representation of macro specs for debugging + * @see #toExpandedString() + */ @Override public String toString() { - synchronized (macros) - { - return "[" + getNames().stream() - .map((macro) -> macro + " = '" + macros.get(macro) + "'") - .collect(Collectors.joining(", ")) + - "]"; - } + return "[" + specs.stream() + .map(entry -> entry.getKey() + "='" + entry.getValue() + "'") + .collect(Collectors.joining(",")) + + "]"; } + + /** @return String representation of expanded macros for debugging + * @see #toString() + */ + public String toExpandedString() + { + return "[" + getNames().stream() + .map((macro) -> macro + "='" + getValue(macro) + "'") + .collect(Collectors.joining(",")) + + "]"; + } + } diff --git a/core/framework/src/main/java/org/phoebus/framework/persistence/XMLMementoTree.java b/core/framework/src/main/java/org/phoebus/framework/persistence/XMLMementoTree.java index 2fab22d7cc..96644fdb99 100644 --- a/core/framework/src/main/java/org/phoebus/framework/persistence/XMLMementoTree.java +++ b/core/framework/src/main/java/org/phoebus/framework/persistence/XMLMementoTree.java @@ -30,7 +30,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class XMLMementoTree implements MementoTree { // Basic implementation, using the attributes of elements for the data, @@ -57,8 +56,11 @@ public static XMLMementoTree create() throws Exception return new XMLMementoTree(document, root); } - /** @param in Stream to which memento is written - * @throws Exception on error + /** + * read InputStrem + * @param in Stream to which memento is written + * @return XMLMementoTree + * @throws Exception on error */ public static XMLMementoTree read(final InputStream in) throws Exception { diff --git a/core/framework/src/main/java/org/phoebus/framework/persistence/XMLUtil.java b/core/framework/src/main/java/org/phoebus/framework/persistence/XMLUtil.java index 38ea84edfc..84ef788d4e 100644 --- a/core/framework/src/main/java/org/phoebus/framework/persistence/XMLUtil.java +++ b/core/framework/src/main/java/org/phoebus/framework/persistence/XMLUtil.java @@ -27,10 +27,12 @@ /** XML Helper * @author Kay Kasemir */ -@SuppressWarnings("nls") public class XMLUtil { - public static final String ENCODING = "UTF-8"; + /** + * Encoding type constant + */ + public static final String ENCODING = "UTF-8"; /** Open XML document with line-number-aware reader and locate root element * @param stream XML stream @@ -163,6 +165,13 @@ public static void writeDocument(Node node, OutputStream stream) throws Exceptio transformer.transform(new DOMSource(node), new StreamResult(stream)); } + /** + * Create a text Element + * @param doc xml document + * @param name of element + * @param value of element + * @return Element + */ public static Element createTextElement(final Document doc, final String name, final String value) { final Element el = doc.createElement(name); @@ -358,9 +367,10 @@ public static boolean parseBoolean(final String text, final boolean default_valu return Boolean.parseBoolean(text); } - /** Transform xml element and children into a string - * + /** + * Transform xml element and children into a string * @param nd Node root of elements to transform + * @param add_newlines add new line if true * @return String representation of xml */ public static String elementToString(Node nd, boolean add_newlines) @@ -423,6 +433,12 @@ else if ((text != null) && (text.length() > 0)) return ret; } + /** + * Transform xml element and children into a string + * @param nls Node root of elements to transform + * @param add_newlines add new line if true + * @return String representation of xml + */ public static String elementsToString(NodeList nls, boolean add_newlines) { String ret = ""; diff --git a/core/framework/src/main/java/org/phoebus/framework/preferences/Preference.java b/core/framework/src/main/java/org/phoebus/framework/preferences/Preference.java index 47baceac30..bf050d16ef 100644 --- a/core/framework/src/main/java/org/phoebus/framework/preferences/Preference.java +++ b/core/framework/src/main/java/org/phoebus/framework/preferences/Preference.java @@ -42,5 +42,9 @@ @Target(ElementType.FIELD) public @interface Preference { - String name() default ""; + /** + * default name + * @return empty string + */ + String name() default ""; } diff --git a/core/framework/src/main/java/org/phoebus/framework/rdb/RDBConnectionPool.java b/core/framework/src/main/java/org/phoebus/framework/rdb/RDBConnectionPool.java index 19ff9b4f07..3353e4f571 100644 --- a/core/framework/src/main/java/org/phoebus/framework/rdb/RDBConnectionPool.java +++ b/core/framework/src/main/java/org/phoebus/framework/rdb/RDBConnectionPool.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2021 Oak Ridge National Laboratory. + * Copyright (c) 2017-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -33,7 +33,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class RDBConnectionPool { /** Logger for the package */ @@ -56,8 +55,12 @@ public class RDBConnectionPool private volatile boolean closed = false; private final int instance; private final RDBInfo info; + /** All open connections */ + private final List connections = new ArrayList<>(); + /** Idle connections, kept open for re-use */ private final List pool = new ArrayList<>(); + /** Future for clear_timer, used to update/cancel */ private final AtomicReference> cleanup = new AtomicReference<>(); private volatile int timeout = 10; @@ -67,10 +70,10 @@ public class RDBConnectionPool *

    URL format depends on the database dialect. * *

    For MySQL resp. Oracle, the formats are: - *

    +     *  
    {@code
          *     jdbc:mysql://[host]:[port]/[database]?user=[user]&password=[password]
          *     jdbc:oracle:thin:[user]/[password]@//[host]:[port]/[database]
    -     *  
    + * }
    * * For Oracle, the port is usually 1521. * @@ -135,6 +138,7 @@ public Connection getConnection() throws Exception // No suitable existing connection, create new one connection = info.connect(); + connections.add(connection); if (total_connections != null) { total_connections.put(connection, new Exception("Open connection " + this)); @@ -157,6 +161,7 @@ public void releaseConnection(final Connection connection) { // Ignore, closing anyway } + connections.remove(connection); logger.log(Level.INFO, this + " is closed", new Exception("Call stack")); } push(connection); @@ -187,13 +192,23 @@ public void clear() closed = true; closeIdleConnections(); - + logger.log(Level.INFO, () -> "Cleared " + this); // In case a timer was running, cancel final Future previous = cleanup.getAndSet(null); if (previous != null) previous.cancel(false); + + for (Connection c : connections) + try + { + c.close(); + } + catch (Exception ex) + { + // Ignore, closing anyway + } } private void closeIdleConnections() @@ -217,6 +232,7 @@ private void close(final Connection connection) { logger.log(Level.FINE, () -> "Closing connection of " + this); connection.close(); + connections.remove(connection); } catch (Exception ex) { diff --git a/core/framework/src/main/java/org/phoebus/framework/rdb/RDBInfo.java b/core/framework/src/main/java/org/phoebus/framework/rdb/RDBInfo.java index 32e3053a6f..71dbb37335 100644 --- a/core/framework/src/main/java/org/phoebus/framework/rdb/RDBInfo.java +++ b/core/framework/src/main/java/org/phoebus/framework/rdb/RDBInfo.java @@ -21,7 +21,6 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class RDBInfo { /** Start of MySQL URL, including " jdbc:mysql:replication://.." */ @@ -51,6 +50,13 @@ public enum Dialect private final String url, user, password; private final Dialect dialect; + /** + * Constructor + * @param url url of Database + * @param user user + * @param password password + * @throws Exception on error + */ public RDBInfo(final String url, final String user, final String password) throws Exception { this.url = url; @@ -79,7 +85,11 @@ public String getUser() return user; } - /** Create a new {@link Connection} */ + /** + * Create a new {@link Connection} + * @return Connection object + * @throws Exception on error + */ public Connection connect() throws Exception { Connection connection = null; diff --git a/core/framework/src/main/java/org/phoebus/framework/selection/Selection.java b/core/framework/src/main/java/org/phoebus/framework/selection/Selection.java index f2c56c3e4f..2e1bae697a 100644 --- a/core/framework/src/main/java/org/phoebus/framework/selection/Selection.java +++ b/core/framework/src/main/java/org/phoebus/framework/selection/Selection.java @@ -9,9 +9,10 @@ */ public interface Selection { - /** - * get the current list of selected objects - * @return - */ + /** + * get the current list of selected objects + * @param Object Type + * @return selected objects + */ public List getSelections(); } diff --git a/core/framework/src/main/java/org/phoebus/framework/selection/SelectionService.java b/core/framework/src/main/java/org/phoebus/framework/selection/SelectionService.java index bb7be320c3..56395c9c38 100644 --- a/core/framework/src/main/java/org/phoebus/framework/selection/SelectionService.java +++ b/core/framework/src/main/java/org/phoebus/framework/selection/SelectionService.java @@ -36,7 +36,7 @@ public static SelectionService getInstance() { /** * Add a selection change listener * - * @param selectionListner + * @param selectionListner listener */ public void addListener(SelectionChangeListener selectionListner) { listeners.add(selectionListner); @@ -45,7 +45,7 @@ public void addListener(SelectionChangeListener selectionListner) { /** * Remove a selection change listener * - * @param selectionListner + * @param selectionListner listener */ public void removeListener(SelectionChangeListener selectionListner) { listeners.remove(selectionListner); @@ -62,8 +62,8 @@ public Selection getSelection() { /** * Set the selection - * - * @param source the source of the new selection + * @param the Type of selected object + * @param source the source of the new selection * @param selection A list of objects to be warpped into a new selection */ public void setSelection(Object source, List selection) { diff --git a/core/framework/src/main/java/org/phoebus/framework/selection/SelectionUtil.java b/core/framework/src/main/java/org/phoebus/framework/selection/SelectionUtil.java index 1d4b7df0a5..4b656a4694 100644 --- a/core/framework/src/main/java/org/phoebus/framework/selection/SelectionUtil.java +++ b/core/framework/src/main/java/org/phoebus/framework/selection/SelectionUtil.java @@ -16,10 +16,20 @@ private SelectionUtil() { } + /** + * Empty selection + * @return selection + */ public static Selection emptySelection() { return EMPTY; } + /** + * Create selection + * @param Type of selection + * @param selection selected Objects + * @return selection + */ public static Selection createSelection(List selection) { return new Selection() { diff --git a/core/framework/src/main/java/org/phoebus/framework/spi/AppDescriptor.java b/core/framework/src/main/java/org/phoebus/framework/spi/AppDescriptor.java index e2dae71ea9..5a1aaffdce 100644 --- a/core/framework/src/main/java/org/phoebus/framework/spi/AppDescriptor.java +++ b/core/framework/src/main/java/org/phoebus/framework/spi/AppDescriptor.java @@ -51,6 +51,7 @@ public default void start() { /** * Create an instance of the application without any specific resources + * @return created instance */ public AppInstance create(); diff --git a/core/framework/src/main/java/org/phoebus/framework/spi/AppInstance.java b/core/framework/src/main/java/org/phoebus/framework/spi/AppInstance.java index fd01db885c..2115c9757e 100644 --- a/core/framework/src/main/java/org/phoebus/framework/spi/AppInstance.java +++ b/core/framework/src/main/java/org/phoebus/framework/spi/AppInstance.java @@ -4,8 +4,15 @@ import java.util.Optional; import org.phoebus.framework.persistence.Memento; +/** + * interface AppInstance + */ public interface AppInstance { + /** + * get descriptor + * @return descriptor + */ public AppDescriptor getAppDescriptor(); /** Is this application instance transient, diff --git a/core/framework/src/main/java/org/phoebus/framework/spi/AppResourceDescriptor.java b/core/framework/src/main/java/org/phoebus/framework/spi/AppResourceDescriptor.java index 9448e43f13..4a52bbf26c 100644 --- a/core/framework/src/main/java/org/phoebus/framework/spi/AppResourceDescriptor.java +++ b/core/framework/src/main/java/org/phoebus/framework/spi/AppResourceDescriptor.java @@ -55,6 +55,8 @@ public default boolean canOpenResource(String resource) { * Create the application using the list of resources, the resource can be the * path or url to a configuration file like .bob or .plt or it can be a list of * pv names, or a channelfinder query + * @param resource uri of resource + * @return created instance */ public AppInstance create(URI resource); diff --git a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java index 2d3923d67e..3559ae1b44 100644 --- a/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java +++ b/core/framework/src/main/java/org/phoebus/framework/util/ResourceParser.java @@ -33,7 +33,6 @@ * @author Kunal Shroff * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ResourceParser { private static final String UTF_8 = "UTF-8"; @@ -151,7 +150,7 @@ public static URI createResourceURI(final String resource) throws Exception } /** Get file for URI - * @return URI for the resource + * @param resource URI for the resource * @return {@link File} if URI represents a file, otherwise null */ public static File getFile(final URI resource) @@ -204,14 +203,14 @@ public static InputStream getContent(final URI resource, final int timeout_ms) t return connection.getInputStream(); } - /** Get list of PVs from a "pv://?PV1&PV2" type URL + /** Get list of PVs from a {@literal "pv://?PV1&PV2"} type URL * *

    If the URI is simply no "pv:" URI, an empty list * if returned. * If it is a "pv:" URI, at least one PV is expected, * otherwise an exception is thrown. * - * @param resource "pv://?PV1&PV2" type URL + * @param resource {@literal "pv://?PV1&PV2"} type URL * @return List of PVs parsed from the resource * @throws Exception on error, including no PVs */ diff --git a/core/framework/src/main/java/org/phoebus/framework/util/RingBuffer.java b/core/framework/src/main/java/org/phoebus/framework/util/RingBuffer.java index 59bae806da..40e9ec67e6 100644 --- a/core/framework/src/main/java/org/phoebus/framework/util/RingBuffer.java +++ b/core/framework/src/main/java/org/phoebus/framework/util/RingBuffer.java @@ -159,7 +159,12 @@ public T remove() return result; } - /** @return Array with content of ring buffer */ + + /** + * convert to array + * @param array ring butter + * @return Array with content of ring buffer + */ public T[] toArray(T[] array) { if (array.length != size) diff --git a/core/framework/src/main/java/org/phoebus/framework/workbench/ApplicationService.java b/core/framework/src/main/java/org/phoebus/framework/workbench/ApplicationService.java index cfa78d6542..3cf85a2202 100644 --- a/core/framework/src/main/java/org/phoebus/framework/workbench/ApplicationService.java +++ b/core/framework/src/main/java/org/phoebus/framework/workbench/ApplicationService.java @@ -27,10 +27,12 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class ApplicationService { - public static final ApplicationService INSTANCE = new ApplicationService(); + /** + * Singleton + */ + public static final ApplicationService INSTANCE = new ApplicationService(); /** All applications by name */ private final Map apps = new HashMap<>(); @@ -120,10 +122,11 @@ public static List getApplications(final URI resource) return Collections.emptyList(); } - /** Find application by name - * - * @param name Application name - * @return {@link AppDescriptor} or null if not found + /** + * Find application by name + * @param application descriptor Type + * @param name Application name + * @return {@link AppDescriptor} or null if not found */ @SuppressWarnings("unchecked") public static AD findApplication(final String name) @@ -131,10 +134,11 @@ public static AD findApplication(final String name) return (AD) INSTANCE.apps.get(name); } - /** Create instance of an application - * - * @param name Application name - * @return {@link AppInstance} or null if not found + /** + * Create instance of an application + * @param application instance Type + * @param name Application name + * @return {@link AppInstance} or null if not found */ @SuppressWarnings("unchecked") public static AI createInstance(final String name) @@ -148,8 +152,9 @@ public static AI createInstance(final String name) return (AI) app.create(); } - /** Create instance of an application that handles a resource - * + /** + * Create instance of an application that handles a resource + * @param application instance Type * @param name Application name * @param resource Resource to open in the application * @return {@link AppInstance} or null if not found diff --git a/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java b/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java index e8d7d262fc..b41cd52cb3 100644 --- a/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java +++ b/core/framework/src/main/java/org/phoebus/framework/workbench/Locations.java @@ -15,10 +15,11 @@ /** Information about key locations * @author Kay Kasemir */ -@SuppressWarnings("nls") public class Locations { + /** system property for phoebus installation path */ public static final String PHOEBUS_INSTALL = "phoebus.install"; + /** system property for phoebus logged user */ public static final String PHOEBUS_USER = "phoebus.user"; /** Initialize locations */ @@ -77,6 +78,7 @@ private static void initUser() *

    Can be set via "phoebus.install". * *

    Defaults to the location of the lib/framework.jar. + * @return the directory of the installation */ public static File install() { diff --git a/core/framework/src/main/java/org/phoebus/framework/workbench/WorkbenchPreferences.java b/core/framework/src/main/java/org/phoebus/framework/workbench/WorkbenchPreferences.java index c054661025..b4c3348fb6 100644 --- a/core/framework/src/main/java/org/phoebus/framework/workbench/WorkbenchPreferences.java +++ b/core/framework/src/main/java/org/phoebus/framework/workbench/WorkbenchPreferences.java @@ -19,13 +19,15 @@ /** Workbench Preferences * @author Kay Kasemir */ -@SuppressWarnings("nls") public class WorkbenchPreferences { /** Logger for the 'workbench' package */ public static final Logger logger = Logger.getLogger(WorkbenchPreferences.class.getPackageName()); + /** directory of external applications */ @Preference public static File external_apps_directory; + + /** external applications */ public static final Collection external_apps; static diff --git a/core/framework/src/test/java/org/phoebus/framework/macros/MacrosUnitTest.java b/core/framework/src/test/java/org/phoebus/framework/macros/MacrosUnitTest.java index d58ba577f9..b513a3fc6d 100644 --- a/core/framework/src/test/java/org/phoebus/framework/macros/MacrosUnitTest.java +++ b/core/framework/src/test/java/org/phoebus/framework/macros/MacrosUnitTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2019 Oak Ridge National Laboratory. + * Copyright (c) 2015-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,8 +7,6 @@ *******************************************************************************/ package org.phoebus.framework.macros; -import org.junit.jupiter.api.Test; - import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; @@ -16,6 +14,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; + /** JUnit test of macro handling * @author Kay Kasemir */ @@ -73,21 +73,19 @@ public void testSimpleSpec() throws Exception { // Plain NAME=VALUE with some spaces Macros macros = Macros.fromSimpleSpec("A=1, B = 2"); - assertThat(macros.getNames().size(), equalTo(2)); - assertThat(macros.getValue("A"), equalTo("1")); - assertThat(macros.getValue("B"), equalTo("2")); + assertThat(macros.toString(), equalTo("[A='1',B='2']")); + + // Specifications hold the values as provided without expanding them + macros = Macros.fromSimpleSpec("A=a, AA = $(A)$(A)"); + assertThat(macros.toString(), equalTo("[A='a',AA='$(A)$(A)']")); // Quoted value with spaces and comma macros = Macros.fromSimpleSpec("MSG = \"Hello, Dolly\" , B=2"); - assertThat(macros.getNames().size(), equalTo(2)); - assertThat(macros.getValue("MSG"), equalTo("Hello, Dolly")); - assertThat(macros.getValue("B"), equalTo("2")); + assertThat(macros.toString(), equalTo("[MSG='Hello, Dolly',B='2']")); // Value with escaped quote macros = Macros.fromSimpleSpec("MSG = \"This is a \\\"Message\\\" .. \" , B=2"); - assertThat(macros.getNames().size(), equalTo(2)); - assertThat(macros.getValue("MSG"), equalTo("This is a \"Message\" .. ")); - assertThat(macros.getValue("B"), equalTo("2")); + assertThat(macros.toString(), equalTo("[MSG='This is a \"Message\" .. ',B='2']")); } /** Test basic macro=value @@ -121,14 +119,67 @@ public void testMacros() throws Exception assertThat(MacroHandler.replace(macros, "Escaped \\\\$(S)"), equalTo("Escaped \\$(S)")); } + /** Test macro=$(other_macro) + * @throws Exception on error + */ + @Test + public void testMacrosInValue() throws Exception + { + final Macros macros = new Macros(); + macros.add("A", "a"); + macros.add("AA", "$(A)$(A)"); + + // MacroValues contain the expanded values + assertThat(macros.getValue("A"), equalTo("a")); + assertThat(macros.getValue("AA"), equalTo("aa")); + + // Swap values + macros.add("B", "b"); + assertThat(macros.getValue("A"), equalTo("a")); + assertThat(macros.getValue("B"), equalTo("b")); + + macros.add("X", "$(A)"); + macros.add("A", "$(B)"); + macros.add("B", "$(X)"); + // Values have been swapped + assertThat(macros.getValue("A"), equalTo("b")); + assertThat(macros.getValue("B"), equalTo("a")); + // Specs contain the history of how values were defined + assertThat(macros.toString(), equalTo("[A='a',AA='$(A)$(A)',B='b',X='$(A)',A='$(B)',B='$(X)']")); + // Values hold each macro with only one, the effective value + assertThat(macros.toExpandedString(), equalTo("[A='b',AA='aa',B='a',X='a']")); + + // OK to define macro with what it is (just a special case of known macro) + macros.add("A", "a"); + assertThat(macros.getValue("A"), equalTo("a")); + macros.add("A", "$(A)"); + assertThat(macros.getValue("A"), equalTo("a")); + + // When using unknown macro, it remains unresolved + // (with log message that's not tested here) + macros.add("A", "$(UNKNOWN)"); + assertThat(macros.getValue("A"), equalTo("$(UNKNOWN)")); + } + + /** Test macros from specs + * @throws Exception on error + */ + @Test + public void testMacrosFromSpecs() throws Exception + { + final Macros macros = Macros.fromSimpleSpec("A=a,B=b,X=$(A),A=$(B),B=$(X)"); + System.out.println(macros); + assertThat(macros.getValue("A"), equalTo("b")); + assertThat(macros.getValue("B"), equalTo("a")); + } + /** Test special cases * @throws Exception on error */ @Test public void testSpecials() throws Exception { - final MacroValueProvider macros = new Macros(); - System.out.println(macros); + final Macros macros = new Macros(); assertThat(macros.toString(), equalTo("[]")); assertThat(MacroHandler.replace(macros, "Plain Text"), equalTo("Plain Text")); @@ -165,6 +216,7 @@ public void testDefaults() throws Exception assertThat(MacroHandler.replace(macros, "/$(DERIVED)/$(DERIVED)/"), equalTo("/default/default/")); macros.add("MAIN", "main"); + macros.add("DERIVED", "$(MAIN=default)"); System.out.println(macros); assertThat(MacroHandler.replace(macros, "$(DERIVED)"), equalTo("main")); @@ -194,7 +246,7 @@ public void testDefaults2() throws Exception * @throws Exception on error */ @Test - public void testRecursion() + public void testRecursion() throws Exception { final Macros macros = new Macros(); macros.add("S", "$(S)"); @@ -210,6 +262,8 @@ public void testRecursion() try { + // S is known, so $(S=a) is replaced with the value of S, + // but that's $(S), ending in recursion MacroHandler.replace(macros, "Recursive $(S=a) default"); } catch (Exception ex) @@ -217,4 +271,39 @@ public void testRecursion() assertThat(ex.getMessage(), containsString(/* [Rr] */ "ecursive")); } } + + @Test + public void testHierarchy() throws Exception + { + // Macro specs provided by preferences + final Macros prefs = new Macros(); + prefs.add("P1", "p1"); + prefs.add("P2", "p2"); + + // Macro specs provided by the launcher + final Macros launcher = new Macros(); + launcher.add("L", "launcher"); + launcher.add("P2", "p2 replaced by launcher"); + + // Macro specs provided by the display + final Macros display = new Macros(); + display.add("T", "display"); + + // Macro specs provided by a group inside the display + final Macros group = new Macros(); + group.add("T", "group"); + + // Hierarchically expand specs + launcher.expandValues(prefs); + display .expandValues(launcher); + group .expandValues(display); + System.out.println("Group specs : " + group); + System.out.println("Group values: " + group.toExpandedString()); + + // MacroValues contain the expanded values + assertThat(group.getValue("P1"), equalTo("p1")); + assertThat(group.getValue("P2"), equalTo("p2 replaced by launcher")); + assertThat(display.getValue("T"), equalTo("display")); + assertThat(group.getValue("T"), equalTo("group")); + } } diff --git a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java index 1ad62c6562..3140692486 100644 --- a/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java +++ b/core/framework/src/test/java/org/phoebus/framework/util/ResourceParserTest.java @@ -14,6 +14,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.phoebus.framework.util.ResourceParser.PV_SCHEMA; import static org.phoebus.framework.util.ResourceParser.createResourceURI; @@ -38,8 +39,7 @@ public class ResourceParserTest private static final String OS = System.getProperty("os.name").toLowerCase(); @Test - public void checkFileToURI() throws Exception - { + public void checkFileToURI() throws Exception { // File URL URI uri = createResourceURI("file:/some/file/path"); System.out.println(uri); @@ -88,13 +88,10 @@ public void checkFileToURI() throws Exception System.out.println(bogus); assertThat(bogus.exists(), equalTo(false)); - try - { + try { getContent(uri); fail("Read nonexisting file?"); - } - catch (Exception ex) - { + } catch (Exception ex) { // Good, caught it } @@ -104,8 +101,8 @@ public void checkFileToURI() throws Exception assertThat(uri, not(nullValue())); assertThat(new File(uri).getCanonicalFile(), equalTo(spacey.getCanonicalFile())); assertThat(uri.getScheme(), equalTo("file")); - if(OS.indexOf("win") >= 0) { - assertThat(uri.toString(), equalTo("file:/C:/some/dir%20with%20space/file.abc")); + if (OS.indexOf("win") >= 0) { + assertTrue(uri.toString().matches("file:/[a-zA-Z]:/some/dir%20with%20space/file.abc")); } else { assertThat(uri.toString(), equalTo("file:/some/dir%20with%20space/file.abc")); } diff --git a/core/launcher/pom.xml b/core/launcher/pom.xml index d1d7ab510e..7c73b9c06d 100644 --- a/core/launcher/pom.xml +++ b/core/launcher/pom.xml @@ -3,19 +3,19 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-launcher org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-ui - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/logbook/pom.xml b/core/logbook/pom.xml index ee43aa0269..0acd896c27 100644 --- a/core/logbook/pom.xml +++ b/core/logbook/pom.xml @@ -3,19 +3,19 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-logbook org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogClient.java b/core/logbook/src/main/java/org/phoebus/logbook/LogClient.java index e5647fef46..d3945dd6c9 100644 --- a/core/logbook/src/main/java/org/phoebus/logbook/LogClient.java +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogClient.java @@ -359,7 +359,7 @@ default List findLogsByProperty(String propertyName) throws LogbookExc /** - * Delete the tag with name tagtag * * @param tagName - the name of the tag to be deleted */ @@ -474,7 +474,7 @@ default void delete(Property property, Collection logIds) throws LogbookEx } /** - * Remove file attachment from LogEntry logId + * Remove file attachment from LogEntry logId * * @param fileName - the file name to be removed * @param logId - the logid from which the attached file is to be removed @@ -490,10 +490,6 @@ default String getServiceUrl() { return null; } - default LogEntry updateLogEntry(LogEntry logEntry) throws LogbookException { - throw new LogbookException(new UnsupportedOperationException()); - } - default SearchResult search(Map map) throws LogbookException{ throw new LogbookException(new UnsupportedOperationException()); } @@ -509,4 +505,12 @@ default void groupLogEntries(List logEntryIds) throws LogbookException{ default String serviceInfo(){ return null; } + + /** + * @param id Unique log entry id + * @return A list of archived {@link LogEntry}s corresponding to the unique id. + */ + default SearchResult getArchivedEntries(long id){ + return null; + } } diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogEntry.java b/core/logbook/src/main/java/org/phoebus/logbook/LogEntry.java index e40965cc7f..ea7fde9450 100644 --- a/core/logbook/src/main/java/org/phoebus/logbook/LogEntry.java +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogEntry.java @@ -71,7 +71,7 @@ public default String getSource(){ public Collection getProperties(); /** - * return the {@link Property} with name propertyName if it exists + * return the {@link Property} with name propertyName if it exists * on this log else return null. * * @param propertyName diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogEntryChangeHandler.java b/core/logbook/src/main/java/org/phoebus/logbook/LogEntryChangeHandler.java new file mode 100644 index 0000000000..b9ff4b6e0d --- /dev/null +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogEntryChangeHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.logbook; + +/** + * Interface for the purpose of notifying implementations that a new {@link LogEntry} has been + * created, or an existing {@link LogEntry} has been updated. + */ +public interface LogEntryChangeHandler { + + /** + * + * @param logEntry new or updated {@link LogEntry}. + */ + void logEntryChanged(LogEntry logEntry); +} diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogEntryImpl.java b/core/logbook/src/main/java/org/phoebus/logbook/LogEntryImpl.java index f128010f59..eb57f265d3 100644 --- a/core/logbook/src/main/java/org/phoebus/logbook/LogEntryImpl.java +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogEntryImpl.java @@ -150,7 +150,7 @@ public Collection getPropertyNames() { } /** - * return the {@link Property} with name propertyName if it exists + * return the {@link Property} with name propertyName if it exists * on this log else return null. * * @param propertyName diff --git a/core/logbook/src/main/java/org/phoebus/logbook/LogbookPreferences.java b/core/logbook/src/main/java/org/phoebus/logbook/LogbookPreferences.java index 0007d15c42..6305d886b4 100644 --- a/core/logbook/src/main/java/org/phoebus/logbook/LogbookPreferences.java +++ b/core/logbook/src/main/java/org/phoebus/logbook/LogbookPreferences.java @@ -2,7 +2,6 @@ import org.phoebus.framework.preferences.AnnotatedPreferences; import org.phoebus.framework.preferences.Preference; -import org.phoebus.framework.preferences.PreferencesReader; import java.util.logging.Level; @@ -16,25 +15,24 @@ public class LogbookPreferences { @Preference public static boolean auto_title; @Preference + public static boolean auto_body; + @Preference public static boolean auto_property; - /** Is there support for a logbook? - * Is the 'logbook_factory' configured and available? + /** + * Is there support for a logbook? + * Is the 'logbook_factory' configured and available? */ public static boolean is_supported; - static - { - final PreferencesReader prefs = AnnotatedPreferences.initialize(LogbookPreferences.class, "/logbook_preferences.properties"); - if (logbook_factory.isEmpty()) - { + static { + AnnotatedPreferences.initialize(LogbookPreferences.class, "/logbook_preferences.properties"); + if (logbook_factory.isEmpty()) { is_supported = false; logger.log(Level.INFO, "No logbook factory selected"); - } - else - { + } else { is_supported = LogService.getInstance().getLogFactories(logbook_factory) != null; - if (! is_supported) + if (!is_supported) logger.log(Level.WARNING, "Cannot locate logbook factory '" + logbook_factory + "'"); } } diff --git a/core/logbook/src/main/resources/logbook_preferences.properties b/core/logbook/src/main/resources/logbook_preferences.properties index b7a7a8771f..75d0744c47 100644 --- a/core/logbook/src/main/resources/logbook_preferences.properties +++ b/core/logbook/src/main/resources/logbook_preferences.properties @@ -10,6 +10,10 @@ logbook_factory=inmemory # should auto generate a title (e.g. "Display Screenshot..."). auto_title=true +# Determines if a log entry created from context menu (e.g. display or data browser) +# should auto generate a body (e.g. "Display Screenshot..."). +auto_body=true + # Determines if a log entry created from context menu (e.g. display or data browser) # should auto generate properties (e.g. "resources.file"). auto_property=false \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 330f96931a..cc0da07e87 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -19,6 +19,6 @@ org.phoebus parent - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/pv/.classpath b/core/pv/.classpath index 5a25858a44..4d3e6523d6 100644 --- a/core/pv/.classpath +++ b/core/pv/.classpath @@ -11,8 +11,8 @@ - - + + diff --git a/core/pv/pom.xml b/core/pv/pom.xml index a1d4814401..d3cbdbc16b 100644 --- a/core/pv/pom.xml +++ b/core/pv/pom.xml @@ -4,7 +4,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -48,23 +48,23 @@ org.phoebus core-pva - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-formula - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.eclipse.paho diff --git a/core/pv/src/main/java/org/phoebus/pv/PV.java b/core/pv/src/main/java/org/phoebus/pv/PV.java index 8a8e00270d..cb7eba1ed4 100644 --- a/core/pv/src/main/java/org/phoebus/pv/PV.java +++ b/core/pv/src/main/java/org/phoebus/pv/PV.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -236,17 +235,17 @@ public VType read() /** Issue a read request * - *

    {@link Future} allows waiting for + *

    {@link CompletableFuture} allows waiting for * and obtaining the result, or its get() * calls will provide an error. * *

    As a side effect, registered listeners will * also receive the value obtained by this call. * - * @return {@link Future} for obtaining the result or Exception + * @return {@link CompletableFuture} for obtaining the result or Exception * @exception Exception on error */ - public Future asyncRead() throws Exception + public CompletableFuture asyncRead() throws Exception { // Default: Return last known value return CompletableFuture.completedFuture(last_value); @@ -270,17 +269,17 @@ public void write(final Object new_value) throws Exception /** Write value with confirmation * - *

    {@link Future} can be used to await completion + *

    {@link CompletableFuture} can be used to await completion * of the write. * The get() will not return a useful value (null), * but they will throw an error if the write failed. * * @param new_value Value to write to the PV - * @return {@link Future} for awaiting completion or exception + * @return {@link CompletableFuture} for awaiting completion or exception * @exception Exception on error * @see #write(Object) */ - public Future asyncWrite(final Object new_value) throws Exception + public CompletableFuture asyncWrite(final Object new_value) throws Exception { // Default: Normal write, declare 'done' right away write(new_value); return CompletableFuture.completedFuture(null); diff --git a/core/pv/src/main/java/org/phoebus/pv/PVPool.java b/core/pv/src/main/java/org/phoebus/pv/PVPool.java index 4c6dcf7f01..6b65301b11 100644 --- a/core/pv/src/main/java/org/phoebus/pv/PVPool.java +++ b/core/pv/src/main/java/org/phoebus/pv/PVPool.java @@ -153,12 +153,13 @@ public String toString() */ public static Set getNameVariants(final String name, final String [] equivalent_pv_prefixes) { + final String _name = name.trim(); // First, look for name as given final Set variants = new LinkedHashSet<>(); - variants.add(name); + variants.add(_name); if (equivalent_pv_prefixes != null && equivalent_pv_prefixes.length > 0) { // Optionally, if the original name is one of the equivalent types ... - final TypedName typed = TypedName.analyze(name); + final TypedName typed = TypedName.analyze(_name); for (String type : equivalent_pv_prefixes) if (type.equals(typed.type)) { @@ -201,15 +202,16 @@ public static Collection getSupportedPrefixes() */ public static PV getPV(final String name) throws Exception { - if (name.isBlank()) + final String _name = name.trim(); + if (_name.isBlank()) throw new Exception("Empty PV name"); - final TypedName type_name = TypedName.analyze(name); + final TypedName type_name = TypedName.analyze(_name); final PVFactory factory = factories.get(type_name.type); if (factory == null) - throw new Exception(name + " has unknown PV type '" + type_name.type + "'"); + throw new Exception(_name + " has unknown PV type '" + type_name.type + "'"); - final String core_name = factory.getCoreName(name); - final ReferencedEntry ref = pool.createOrGet(core_name, () -> createPV(factory, name, type_name.name)); + final String core_name = factory.getCoreName(_name); + final ReferencedEntry ref = pool.createOrGet(core_name, () -> createPV(factory, _name, type_name.name)); logger.log(Level.CONFIG, () -> "PV '" + ref.getEntry().getName() + "' references: " + ref.getReferences()); return ref.getEntry(); } diff --git a/core/pv/src/main/java/org/phoebus/pv/ca/JCA_PV.java b/core/pv/src/main/java/org/phoebus/pv/ca/JCA_PV.java index 7464e31dc0..a43480db21 100644 --- a/core/pv/src/main/java/org/phoebus/pv/ca/JCA_PV.java +++ b/core/pv/src/main/java/org/phoebus/pv/ca/JCA_PV.java @@ -420,7 +420,7 @@ public void getCompleted(final GetEvent ev) } @Override - public Future asyncRead() throws Exception + public CompletableFuture asyncRead() throws Exception { final DBRType type = channel.getFieldType(); if (type == null || type == DBRType.UNKNOWN) @@ -453,7 +453,7 @@ public void write(final Object new_value) throws Exception } @Override - public Future asyncWrite(final Object new_value) throws Exception + public CompletableFuture asyncWrite(final Object new_value) throws Exception { final PutCallbackFuture result = new PutCallbackFuture(); performWrite(new_value, result); diff --git a/core/pv/src/main/java/org/phoebus/pv/loc/ValueHelper.java b/core/pv/src/main/java/org/phoebus/pv/loc/ValueHelper.java index aed3a8149e..ddc68f76c4 100644 --- a/core/pv/src/main/java/org/phoebus/pv/loc/ValueHelper.java +++ b/core/pv/src/main/java/org/phoebus/pv/loc/ValueHelper.java @@ -122,8 +122,19 @@ else if (c == '"') items.add(text.substring(pos, end+1)); pos = end + 1; // Advance to comma at end of string - while (pos < text.length() && text.charAt(pos) != ',') - ++pos; + while (pos < text.length()) { + char currentChar = text.charAt(pos); + + if (currentChar == ',') { + break; + } + else if (currentChar != ' ' && currentChar != '\t') { + throw new Exception("A character that is not a space or a tab appeared after a closing quote"); + } + else { + pos++; + } + } ++pos; } diff --git a/core/pv/src/main/java/org/phoebus/pv/opva/PVA_PV.java b/core/pv/src/main/java/org/phoebus/pv/opva/PVA_PV.java index d8067b12a1..9ac8d7f892 100644 --- a/core/pv/src/main/java/org/phoebus/pv/opva/PVA_PV.java +++ b/core/pv/src/main/java/org/phoebus/pv/opva/PVA_PV.java @@ -8,6 +8,7 @@ package org.phoebus.pv.opva; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.logging.Level; @@ -249,7 +250,7 @@ public void unlisten(final Monitor monitor) /** {@inheritDoc} */ @Override - public Future asyncRead() throws Exception + public CompletableFuture asyncRead() throws Exception { final PVGetHandler result = new PVGetHandler(this); channel.createChannelGet(result, read_request); @@ -265,7 +266,7 @@ public void write(final Object new_value) throws Exception /** {@inheritDoc} */ @Override - public Future asyncWrite(Object new_value) throws Exception + public CompletableFuture asyncWrite(Object new_value) throws Exception { if (enum_labels != null && new_value instanceof String) { // Convert string-for-enum into index of corresponding label diff --git a/core/pv/src/main/java/org/phoebus/pv/opva/PVGetHandler.java b/core/pv/src/main/java/org/phoebus/pv/opva/PVGetHandler.java index d5d825efbc..b41b143216 100644 --- a/core/pv/src/main/java/org/phoebus/pv/opva/PVGetHandler.java +++ b/core/pv/src/main/java/org/phoebus/pv/opva/PVGetHandler.java @@ -27,7 +27,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -class PVGetHandler extends PVRequester implements ChannelGetRequester, Future +class PVGetHandler extends PVRequester implements ChannelGetRequester { final private PVA_PV pv; diff --git a/core/pv/src/main/java/org/phoebus/pv/opva/PVPutHandler.java b/core/pv/src/main/java/org/phoebus/pv/opva/PVPutHandler.java index ed254ecf73..c59bc5301e 100644 --- a/core/pv/src/main/java/org/phoebus/pv/opva/PVPutHandler.java +++ b/core/pv/src/main/java/org/phoebus/pv/opva/PVPutHandler.java @@ -25,6 +25,7 @@ import org.epics.pvdata.pv.Status; import org.epics.pvdata.pv.Structure; import org.phoebus.pv.PV; +import org.epics.vtype.VType; /** A {@link ChannelPutRequester} for writing a value to a {@link PVA_PV}, * indicating completion via a {@link Future} @@ -32,7 +33,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -class PVPutHandler extends PVRequester implements ChannelPutRequester, Future +class PVPutHandler extends PVRequester implements ChannelPutRequester { final private PV pv; final private Object new_value; @@ -136,7 +137,7 @@ public boolean isDone() // Future @Override - public Object get() throws InterruptedException, ExecutionException + public VType get() throws InterruptedException, ExecutionException { updates.await(); if (error != null) @@ -146,7 +147,7 @@ public Object get() throws InterruptedException, ExecutionException // Future @Override - public Object get(final long timeout, final TimeUnit unit) throws InterruptedException, + public VType get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { if (! updates.await(timeout, unit)) diff --git a/core/pv/src/main/java/org/phoebus/pv/opva/PVRequester.java b/core/pv/src/main/java/org/phoebus/pv/opva/PVRequester.java index c2849c0f65..929d883ce8 100644 --- a/core/pv/src/main/java/org/phoebus/pv/opva/PVRequester.java +++ b/core/pv/src/main/java/org/phoebus/pv/opva/PVRequester.java @@ -9,15 +9,17 @@ import static org.phoebus.pv.PV.logger; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import org.epics.pvdata.pv.MessageType; import org.epics.pvdata.pv.Requester; +import org.epics.vtype.VType; /** Base for PVAccess {@link Requester} * @author Kay Kasemir */ -class PVRequester implements Requester +class PVRequester extends CompletableFuture implements Requester { @Override public String getRequesterName() diff --git a/core/pv/src/main/java/org/phoebus/pv/pva/ImageDecoder.java b/core/pv/src/main/java/org/phoebus/pv/pva/ImageDecoder.java index dd5496745d..e9a15cad96 100644 --- a/core/pv/src/main/java/org/phoebus/pv/pva/ImageDecoder.java +++ b/core/pv/src/main/java/org/phoebus/pv/pva/ImageDecoder.java @@ -202,7 +202,7 @@ else if (colorMode <= 4 && colorMode >= 2 && dimensions.length != 3) if (codec_info != null) { final PVAString name = codec_info.get("name"); - if (name != null && !name.get().isBlank()) + if (name != null && name.get() != null && !name.get().isBlank()) { // For compressed data, values is ubyte[] and // codec.parameters holds original data type code diff --git a/core/pv/src/main/java/org/phoebus/pv/pva/PVAStructureHelper.java b/core/pv/src/main/java/org/phoebus/pv/pva/PVAStructureHelper.java index 4ec7599825..3d5f8d4d74 100644 --- a/core/pv/src/main/java/org/phoebus/pv/pva/PVAStructureHelper.java +++ b/core/pv/src/main/java/org/phoebus/pv/pva/PVAStructureHelper.java @@ -7,6 +7,8 @@ ******************************************************************************/ package org.phoebus.pv.pva; +import static java.util.stream.Collectors.toList; +import static java.util.stream.IntStream.range; import static org.phoebus.pv.pva.Decoders.decodeAlarm; import static org.phoebus.pv.pva.Decoders.decodeTime; @@ -17,6 +19,7 @@ import org.epics.pva.data.PVAArray; import org.epics.pva.data.PVABool; +import org.epics.pva.data.PVABoolArray; import org.epics.pva.data.PVAByteArray; import org.epics.pva.data.PVAData; import org.epics.pva.data.PVADoubleArray; @@ -30,10 +33,16 @@ import org.epics.pva.data.PVAStructure; import org.epics.pva.data.PVAStructureArray; import org.epics.pva.data.PVAUnion; +import org.epics.util.array.ArrayByte; import org.epics.util.array.ArrayDouble; import org.epics.util.array.ArrayFloat; import org.epics.util.array.ArrayInteger; +import org.epics.util.array.ArrayLong; +import org.epics.util.array.ArrayShort; +import org.epics.util.array.ArrayUByte; import org.epics.util.array.ArrayUInteger; +import org.epics.util.array.ArrayULong; +import org.epics.util.array.ArrayUShort; import org.epics.vtype.Alarm; import org.epics.vtype.AlarmSeverity; import org.epics.vtype.AlarmStatus; @@ -203,6 +212,45 @@ else if (column instanceof PVAStringArray) types.add(String.class); values.add(Arrays.asList(typed.get())); } + else if (column instanceof PVAShortArray) + { + final PVAShortArray typed = (PVAShortArray)column; + types.add(Short.TYPE); + if (typed.isUnsigned()) + values.add(ArrayUShort.of(typed.get())); + else + values.add(ArrayShort.of(typed.get())); + } + else if (column instanceof PVALongArray) + { + final PVALongArray typed = (PVALongArray)column; + types.add(Long.TYPE); + if (typed.isUnsigned()) + values.add(ArrayULong.of(typed.get())); + else + values.add(ArrayLong.of(typed.get())); + } + else if (column instanceof PVAByteArray) + { + final PVAByteArray typed = (PVAByteArray)column; + types.add(Byte.TYPE); + if (typed.isUnsigned()) + values.add(ArrayUByte.of(typed.get())); + else + values.add(ArrayByte.of(typed.get())); + } + else if (column instanceof PVABoolArray) + { + final PVABoolArray typed = (PVABoolArray)column; + types.add(Boolean.TYPE); + boolean[] data = typed.get(); + // Convert to boxed Integer to add to List + values.add(range(0, data.length).mapToObj(i -> data[i]).collect(toList())); + } + else + { + throw new IllegalArgumentException("Could not decode table column of type: " + column.getClass()); + } } return VTable.of(types, names, values); diff --git a/core/pv/src/main/java/org/phoebus/pv/pva/PVA_PV.java b/core/pv/src/main/java/org/phoebus/pv/pva/PVA_PV.java index 7f22f22395..a34180c2b3 100644 --- a/core/pv/src/main/java/org/phoebus/pv/pva/PVA_PV.java +++ b/core/pv/src/main/java/org/phoebus/pv/pva/PVA_PV.java @@ -8,6 +8,7 @@ package org.phoebus.pv.pva; import java.util.BitSet; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -96,11 +97,11 @@ private void handleMonitor(final PVAChannel channel, } @Override - public Future asyncRead() throws Exception + public CompletableFuture asyncRead() throws Exception { final Future data = channel.read(name_helper.getRequest()); // Wrap into Future that converts PVAStructure into VType - return new Future<>() + return new CompletableFuture<>() { @Override public boolean cancel(final boolean mayInterruptIfRunning) @@ -190,7 +191,7 @@ public void write(final Object new_value) throws Exception } @Override - public Future asyncWrite(final Object new_value) throws Exception + public CompletableFuture asyncWrite(final Object new_value) throws Exception { // Perform a put with completion, // i.e., process target and block until processing completes, diff --git a/core/pv/src/main/resources/pv_ca_preferences.properties b/core/pv/src/main/resources/pv_ca_preferences.properties index b8f6fb04c8..aa214cb00a 100644 --- a/core/pv/src/main/resources/pv_ca_preferences.properties +++ b/core/pv/src/main/resources/pv_ca_preferences.properties @@ -2,6 +2,17 @@ # Package org.phoebus.pv.ca # ------------------------- +# By default, we use the following preferences settings, +# but when the System property "jca.use_env" is "true", +# JCA falls back to the EPICS_CA_... environment variables. +# +# Sites that prefer to use the EPICS_CA_... environment variables +# thus need to add +# +# -Djca.use_env=true +# +# to their launcher script. + # Channel Access address list addr_list= diff --git a/core/pv/src/test/java/org/phoebus/pv/PVPoolTest.java b/core/pv/src/test/java/org/phoebus/pv/PVPoolTest.java index e03bc91690..de52894590 100644 --- a/core/pv/src/test/java/org/phoebus/pv/PVPoolTest.java +++ b/core/pv/src/test/java/org/phoebus/pv/PVPoolTest.java @@ -57,14 +57,47 @@ public void equivalentPVs() assertThat(pvs, hasItem("ca://ramp")); assertThat(pvs, hasItem("pva://ramp")); + // Repeat to verify trimming of pv name + pvs = PVPool.getNameVariants("pva://ramp\n", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(3)); + assertThat(pvs, hasItem("ramp")); + assertThat(pvs, hasItem("ca://ramp")); + assertThat(pvs, hasItem("pva://ramp")); + + pvs = PVPool.getNameVariants("pva://ramp ", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(3)); + assertThat(pvs, hasItem("ramp")); + assertThat(pvs, hasItem("ca://ramp")); + assertThat(pvs, hasItem("pva://ramp")); + + // For loc or sim which are not in the equivalent list, pass name through pvs = PVPool.getNameVariants("loc://ramp", equivalent_pv_prefixes); assertThat(pvs.size(), equalTo(1)); assertThat(pvs, hasItem("loc://ramp")); + // Repeat to verify trimming of pv name + pvs = PVPool.getNameVariants("loc://ramp\n", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(1)); + assertThat(pvs, hasItem("loc://ramp")); + + pvs = PVPool.getNameVariants("loc://ramp ", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(1)); + assertThat(pvs, hasItem("loc://ramp")); + pvs = PVPool.getNameVariants("sim://ramp", equivalent_pv_prefixes); assertThat(pvs.size(), equalTo(1)); assertThat(pvs, hasItem("sim://ramp")); + + // Repeat to verify trimming of pv name + pvs = PVPool.getNameVariants("sim://ramp\n", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(1)); + assertThat(pvs, hasItem("sim://ramp")); + + pvs = PVPool.getNameVariants("sim://ramp ", equivalent_pv_prefixes); + assertThat(pvs.size(), equalTo(1)); + assertThat(pvs, hasItem("sim://ramp")); + } diff --git a/core/pv/src/test/java/org/phoebus/pv/SimPVTest.java b/core/pv/src/test/java/org/phoebus/pv/SimPVTest.java index effd355a73..44ea6a4084 100644 --- a/core/pv/src/test/java/org/phoebus/pv/SimPVTest.java +++ b/core/pv/src/test/java/org/phoebus/pv/SimPVTest.java @@ -37,6 +37,42 @@ public void demoSine() throws Exception PVPool.releasePV(pv); } + @Test + public void demoSineWithTrimming1() throws Exception + { + final CountDownLatch done = new CountDownLatch(3); + + System.out.println("Awaiting " + done.getCount() + " updates..."); + final PV pv = PVPool.getPV("sim://sine\n"); + final Disposable flow = pv.onValueEvent() + .subscribe(value -> + { + System.out.println("Received update " + value); + done.countDown(); + }); + done.await(); + flow.dispose(); + PVPool.releasePV(pv); + } + + @Test + public void demoSineWithTrimming2() throws Exception + { + final CountDownLatch done = new CountDownLatch(3); + + System.out.println("Awaiting " + done.getCount() + " updates..."); + final PV pv = PVPool.getPV("sim://sine "); + final Disposable flow = pv.onValueEvent() + .subscribe(value -> + { + System.out.println("Received update " + value); + done.countDown(); + }); + done.await(); + flow.dispose(); + PVPool.releasePV(pv); + } + @Test public void demoConst() throws Exception { diff --git a/core/pva/.gitignore b/core/pva/.gitignore new file mode 100644 index 0000000000..47872020c5 --- /dev/null +++ b/core/pva/.gitignore @@ -0,0 +1 @@ +demo/ diff --git a/core/pva/README.md b/core/pva/README.md index ec9fae73e8..dcfb8df47c 100644 --- a/core/pva/README.md +++ b/core/pva/README.md @@ -106,9 +106,9 @@ IPv6 Support Both the server and client support IPv6, which at this time needs to be enabled by configuring the `EPICS_PVAS_INTF_ADDR_LIST` of the server respectively the -`EPICS_PVA_ADDR_LIST` and/or `EPICS_PVA_NAME_SERVERS` of the client to provide the desired IPv6 addresses. +`EPICS_PVA_ADDR_LIST` and/or `EPICS_PVA_NAME_SERVERS` of the client to provide the desired IPv6 addresses. To completely disable any IPv6 functionality, set `EPICS_PVA_ENABLE_IPV6` to false. -See Javadoc of `EPICS_PVAS_INTF_ADDR_LIST`, `EPICS_PVA_ADDR_LIST` and `EPICS_PVA_NAME_SERVERS` in `PVASettings` +See Javadoc of `EPICS_PVAS_INTF_ADDR_LIST`, , `EPICS_PVA_ADDR_LIST`, `EPICS_PVA_NAME_SERVERS` and `EPICS_PVA_ENABLE_IPV6` in `PVASettings` for details. Command-line Example diff --git a/core/pva/TLS.md b/core/pva/TLS.md new file mode 100644 index 0000000000..7129a59cd6 --- /dev/null +++ b/core/pva/TLS.md @@ -0,0 +1,303 @@ +Secure Socket Support +===================== + +By default, the server and client will use plain TCP sockets to communicate. +By configuring a keystore for the server and a truststore for the client, +the communication can be switched to secure (TLS) sockets. +The sockets are encrypted, and clients will only communicate with trusted servers. +The following describes a minimal setup for initial tests, +followed by a more elaborate setup later in this document. + +Step 1: Create a server KEYSTORE that contains a public and private key. +------- + +The server passes the public key to clients. Clients then use that to encrypt messages +to the server which only the server can decode with its private key. + +``` +keytool -genkey -alias mykey -dname "CN=server" -keystore KEYSTORE -storepass changeit -keyalg RSA +``` + +To check, note "Entry type: PrivateKeyEntry" because the certificate holds both a public and private key: + +``` +keytool -list -v -keystore KEYSTORE -storepass changeit +``` + + +Step 2: Create a client TRUSTSTORE to register the public server key +------- + +Clients check a list of public keys to identify trusted servers. +Clients can technically use the keystore we just created, but +they should really only have access to the server's public key, +not the server's private key. +In addition, you may want to add public keys from more than one server into +the client truststore. + +First export the server's public key. + +``` +keytool -export -alias mykey -keystore KEYSTORE -storepass changeit -rfc -file mykey.cer +``` + +Import the certificate into a new client truststore. + +``` +keytool -import -alias mykey -file mykey.cer -keystore TRUSTSTORE -storepass changeit -noprompt +``` + +To check, note "Entry type: trustedCertEntry" because the truststore contains only public keys: + +``` +keytool -list -v -keystore TRUSTSTORE -storepass changeit +``` + +While the key and trust files were created with the Java keytool, +they are compatible with generic open SSL tools: + +``` +openssl pkcs12 -info -in KEYSTORE -nodes +``` + +The essential commands are also in `make_tls_simple.sh` + + +Step 3: Configure and run the demo server +------- + +Set environment variable `EPICS_PVAS_TLS_KEYCHAIN` to inform the server about its keystore. +The format of this setting is `/path/to/file;password`. +If you used `make_tls_simple.sh`, that would be `demo/KEYSTORE;changeit`. + +Then run a demo server: + +``` +export EPICS_PVAS_TLS_KEYCHAIN="/path/to/KEYSTORE;changeit" +java -cp target/classes org.epics.pva.server.ServerDemo +``` + + +Step 4: Configure and run the demo client +------- + +Set environment variable `EPICS_PVA_TLS_KEYCHAIN` to inform the client about its truststore. +If you used `make_tls_simple.sh`, that would be `demo/TRUSTSTORE;changeit`. +Then run a demo client (`-v 5` to see protocol detail): + +``` +export EPICS_PVA_TLS_KEYCHAIN="/path/to/TRUSTSTORE;changeit" +java -cp target/classes org.epics.pva.client.PVAClientMain get -v 5 demo +# Or: ./pvaclient get -v 5 demo +``` + +On both the server and the client note how they mention "TLS" in their log messages. + +Logging +------- + +The PVA server and client code logs in the `org.epics.pva` package. +For lower level encryption information, add `-Djavax.net.debug=all`. + +For example, when receiving subscription updates for a string PV with status and timestamp, +the log messages indicate that the encrypted data size of 74 bytes is almost twice the size +of the decrypted payload of 41 bytes: + +``` +javax.net.ssl|DEBUG|91|TCP receiver /127.0.0.1|2023-05-05 15:57:37.299 EDT|SSLSocketInputRecord.java:214|READ: TLSv1.2 application_data, length = 74 +javax.net.ssl|DEBUG|91|TCP receiver /127.0.0.1|2023-05-05 15:57:37.299 EDT|SSLSocketInputRecord.java:493|Raw read ( + 0000: 65 27 79 1D 33 5D 7C AB A6 06 86 31 0D 9C 10 70 e'y.3].....1...p + 0010: 3C 85 58 D3 FD AA 81 57 00 0A 04 DF 37 3D EE 31 <.X....W....7=.1 + 0020: C4 2A CE E5 24 A3 E8 F3 F2 B6 7E 64 BE 32 9E 71 .*..$......d.2.q + 0030: F8 29 81 C4 3F 61 D0 E4 D1 D5 A7 BC 5A 21 D0 B8 .)..?a......Z!.. + 0040: F5 5F DD 5E DE 59 79 F8 45 86 ._.^.Yy.E. +) +javax.net.ssl|DEBUG|91|TCP receiver /127.0.0.1|2023-05-05 15:57:37.300 EDT|SSLCipher.java:1935|Plaintext after DECRYPTION ( + 0000: CA 02 40 0D 21 00 00 00 01 00 00 00 00 02 02 03 ..@.!........... + 0010: 0B 56 61 6C 75 65 20 69 73 20 38 33 B1 5F 55 64 .Value is 83._Ud + 0020: 00 00 00 00 10 0E AD 11 00 ......... +``` + +Firewalls +--------- + +Tests from other hosts may require firewall openings. +For RHEL9, use this get both immediate openings and have them +persist a reboot: + +``` +# Default UDP search port +sudo firewall-cmd --zone=public --add-port=5076/udp +sudo firewall-cmd --zone=public --add-port=5076/udp --permanent + +# Default plain TCP port +sudo firewall-cmd --zone=public --add-port=5075/tcp +sudo firewall-cmd --zone=public --add-port=5075/tcp --permanent + +# Default secure (TLS) TCP port +sudo firewall-cmd --zone=public --add-port=5076/tcp +sudo firewall-cmd --zone=public --add-port=5076/tcp --permanent + +# Show +sudo firewall-cmd --info-zone=public +``` + +Use a Certification Authority +----------------------------- + +Instead of creating a separate key pair for each server and telling each client to trust all those public server keys, +we can use a Certification Authority (CA). +That way, clients trust the CA, and you can create individual key pairs for servers without need +to distribute their public keys to each client. + +For a stand-alone demo, we create our own CA, and make its public certificate available as `myca.cer`: + +``` +keytool -genkeypair -alias myca -keystore ca.p12 -storepass changeit -dname "CN=myca" -keyalg RSA -ext BasicConstraints=ca:true +keytool -list -v -keystore ca.p12 -storepass changeit +keytool -exportcert -alias myca -keystore ca.p12 -storepass changeit -rfc -file myca.cer +keytool -printcert -file myca.cer +``` + +Create a truststore that holds the public certificate of our CA. +A client that simply wants to trust any CA-signed certificate +could use this, it's equivalent to the `TRUSTSTORE` from the original example. + +``` +keytool -importcert -alias myca -keystore trust_ca.p12 -storepass changeit -file myca.cer -noprompt +keytool -list -v -keystore trust_ca.p12 -storepass changeit +``` + +Now create a server keypair for use by the IOC: + +``` +keytool -genkeypair -alias myioc -keystore ioc.p12 -storepass changeit -dname "CN=myioc" -keyalg RSA +keytool -list -v -keystore ioc.p12 -storepass changeit +``` + +It starts out as a "self-signed certificate" with `myioc` as both "owner" and "issuer". +Create a certificate signing request. The CSR could be sent to a commercial CA, but we sign it with our own CA. + +``` +keytool -certreq -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.csr +keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:myioc -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myioc.csr -outfile myioc.cer +keytool -printcert -file myioc.cer +``` + +Import the signed certificate into the ioc keystore. Since `ioc.cer` is signed by `myca`, which +is not a generally known CA, we will get an error "Failed to establish chain" +unless we first import `myca.cer` to trust out local CA. + +``` +keytool -importcert -alias myca -keystore ioc.p12 -storepass changeit -file myca.cer -noprompt +keytool -importcert -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.cer +keytool -list -v -keystore ioc.p12 -storepass changeit +``` + +We can now run the server with `EPICS_PVAS_TLS_KEYCHAIN=/path/to/ioc.p12;changeit` and clients with +`EPICS_PVA_TLS_KEYCHAIN=/path/to/trust_ca.p12;changeit`. + +See `make_tls_ca.sh` for a copy of the essential commands. +You can create additional server files `ioc1.p12`, `ioc2.p12` and have each IOC use its own key pair. +Clients will trust them without any changes to the `trust_ca.p12` +as long as the IOC certificates are signed by your CA. + + +Add client certificate +---------------------- + +Using certificates as described so far results in encrypted communication. +Clients trust servers either because a server's public key is directly known to the client, +or a server's certificate is signed by a CA known to the client. +Servers, on the other hand, do not know for certain who the client is. +Authentication is handled by the client sending a name, typically based on the user name provided by the operating system. +This is called "ca authentication" because it mimics the Channel Access behavior. + +By adding a client certificate that is signed by the CA, the client identifies +via that signed certificate. This is called "x509 authentication". + +Create a client certificate that uses the name "Fred F." for x509 authentication: + +``` +keytool -genkeypair -alias myclient -keystore client.p12 -storepass changeit -dname "CN=Fred F." -keyalg RSA +keytool -list -v -keystore client.p12 -storepass changeit +``` + +Sign the client certificate with the CA and update the client file with that signed certificate: + +``` +keytool -certreq -alias myclient -keystore client.p12 -storepass changeit -file myclient.csr +keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:client -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myclient.csr -outfile myclient.cer +keytool -printcert -file myclient.cer + +keytool -importcert -alias myca -keystore client.p12 -storepass changeit -file myca.cer -noprompt +keytool -importcert -alias myclient -keystore client.p12 -storepass changeit -file myclient.cer +keytool -list -v -keystore client.p12 -storepass changeit +``` + +In the last step, note that the file `client.p12` uses an "alias" and "DNSName" of "myclient", +and an "owner" of `CN=Fred F.`. The latter "Common Name" listed as the "owner" +is used as the client name with x509 authentication. + +We can now run the server with `EPICS_PVAS_TLS_KEYCHAIN=/path/to/ioc.p12;changeit` and a client with +`EPICS_PVA_TLS_KEYCHAIN=/path/to/client.p12;changeit`. +The server will identify the client as "Fred F.". + +By default, the server supports clients with certificate and x509 authentication, +but client certificates are not required. +By setting `EPICS_PVAS_TLS_OPTIONS="client_cert=require"`, the server will +abort the initial TLS handshake for clients that do not have a certificate. + + +In total, we now have the following: + + * `KEYSTORE`, `TRUSTSTORE`: + Server keystore and client truststore from the first, minimalistic example. + All IOCs could use the KEYSTORE and all clients the TRUSTSTORE, but + example becomes cumbersome when we want to add certificates that are specific + to IOCs, or if clients should use x509 authentication. + + * `ca.p12`: + Keystore with public and private key of our Certification Authority (CA). + This file needs to be guarded because it allows creating new IOC + and client keystores. + + * `myca.cer`: + Public certificate of the CA. + Imported into any `*.p12` that needs to trust the CA. + + * `trust_ca.p12`: + Truststore with public certificate of the CA. + Equivalent to `myca.cer`, and some tools might directly use `myca.cer`, + but `trust_ca.p12` presents it in the commonly used PKCS12 `*.p12` file format. + Clients can set their `EPICS_PVA_TLS_KEYCHAIN` to this file to + communicate with IOCs, resulting in encryption and "ca" authentication. + + * `ioc.p12`: + Keystore with public and private key of an IOC. + The public key certificate is signed by the CA so that clients + will trust it. + To be used with `EPICS_PVAS_TLS_KEYCHAIN` of IOCs. + + * `myioc.cer`, `myioc.csr`: Public IOC certificate and certificate signing request. + Intermediate files used to sign the IOC certificate. + May be deleted. + + * `client.p12`: + Keystore with public and private key of a client, + providing a client name for x509 authentication. + Clients can set their `EPICS_PVA_TLS_KEYCHAIN` to this file to + communicate with IOCs using x509 authentication. + If the server runs with `EPICS_PVAS_TLS_OPTIONS="client_cert=require"`, + the clients MUST use a client certificate like `client.p12`. + Clients with just `trust_ca.p12` will fail during the initial TLS handshake. + + * `myclient.cer`, `myclient.csr`: + Intermediate files used to sign the client certificate. + May be deleted. + + +The example can be extended by creating one certificate per IOC and one per client, +and by adding one or more intermediate CA levels instead of directly +signing IOC and client certificates with the "root" CA. + diff --git a/core/pva/make_tls_ca.sh b/core/pva/make_tls_ca.sh new file mode 100755 index 0000000000..18a61aa070 --- /dev/null +++ b/core/pva/make_tls_ca.sh @@ -0,0 +1,54 @@ +# More elaborate example, see TLS.md "Use a Certification Authority" +rm -rf demo +mkdir demo +cd demo + +# Create our own CA, and make its public certificate available as `myca.cer`: +keytool -genkeypair -alias myca -keystore ca.p12 -storepass changeit -dname "CN=myca" -keyalg RSA -ext BasicConstraints=ca:true +keytool -list -v -keystore ca.p12 -storepass changeit +keytool -exportcert -alias myca -keystore ca.p12 -storepass changeit -rfc -file myca.cer +keytool -printcert -file myca.cer + +# For clients, create a truststore that holds the public certificate of our CA +keytool -importcert -alias myca -keystore trust_ca.p12 -storepass changeit -file myca.cer -noprompt +keytool -list -v -keystore trust_ca.p12 -storepass changeit + +# Now create a server keypair for use by the IOC: +keytool -genkeypair -alias myioc -keystore ioc.p12 -storepass changeit -dname "CN=myioc" -keyalg RSA +keytool -list -v -keystore ioc.p12 -storepass changeit + +# Create signing request, sign with our CA, import signed cert into ioc.p12 +keytool -certreq -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.csr +keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:myioc -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myioc.csr -outfile myioc.cer +keytool -printcert -file myioc.cer +keytool -importcert -alias myca -keystore ioc.p12 -storepass changeit -file myca.cer -noprompt +keytool -importcert -alias myioc -keystore ioc.p12 -storepass changeit -file myioc.cer +keytool -list -v -keystore ioc.p12 -storepass changeit + +# Create client keypair so client can authenticate as "Fred F." +keytool -genkeypair -alias myclient -keystore client.p12 -storepass changeit -dname "CN=Fred F." -keyalg RSA +keytool -list -v -keystore client.p12 -storepass changeit + +# Sign client certificate with our CA +keytool -certreq -alias myclient -keystore client.p12 -storepass changeit -file myclient.csr +keytool -gencert -alias myca -keystore ca.p12 -storepass changeit -ext SubjectAlternativeName=DNS:client -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth -infile myclient.csr -outfile myclient.cer +keytool -printcert -file myclient.cer +keytool -importcert -alias myca -keystore client.p12 -storepass changeit -file myca.cer -noprompt +keytool -importcert -alias myclient -keystore client.p12 -storepass changeit -file myclient.cer +keytool -list -v -keystore client.p12 -storepass changeit + +echo "*************************************************************" +echo "***************** trust_ca **********************************" +echo "*************************************************************" +keytool -list -v -keystore trust_ca.p12 -storepass changeit + +echo "*************************************************************" +echo "***************** IOC ***************************************" +echo "*************************************************************" +keytool -list -v -keystore ioc.p12 -storepass changeit + +echo "*************************************************************" +echo "***************** Client ************************************" +echo "*************************************************************" +keytool -list -v -keystore client.p12 -storepass changeit + diff --git a/core/pva/make_tls_simple.sh b/core/pva/make_tls_simple.sh new file mode 100755 index 0000000000..29926cbf9d --- /dev/null +++ b/core/pva/make_tls_simple.sh @@ -0,0 +1,24 @@ +# Minimal example, see TLS.md steps 1-4 + +rm -rf demo +mkdir demo +cd demo + + +# Creates a key pair for the server +keytool -genkey -alias mykey -dname "CN=server" -keystore KEYSTORE -storepass changeit -keyalg RSA +# Export the public key +keytool -export -alias mykey -keystore KEYSTORE -storepass changeit -rfc -file mykey.cer + +# Create a trust store for the client to make it aware of the server's public key +keytool -import -alias mykey -file mykey.cer -keystore TRUSTSTORE -storepass changeit -noprompt + +echo "*************************************************************" +echo "***************** KEYSTORE **********************************" +echo "*************************************************************" +keytool -list -v -keystore KEYSTORE -storepass changeit + +echo "*************************************************************" +echo "***************** TRUSTSTORE ********************************" +echo "*************************************************************" +keytool -list -v -keystore TRUSTSTORE -storepass changeit diff --git a/core/pva/pom.xml b/core/pva/pom.xml index 60d6a447eb..8a75ea60fc 100644 --- a/core/pva/pom.xml +++ b/core/pva/pom.xml @@ -4,7 +4,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -28,7 +28,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.3.0 diff --git a/core/pva/serverdemo b/core/pva/serverdemo new file mode 100755 index 0000000000..15751ee7f3 --- /dev/null +++ b/core/pva/serverdemo @@ -0,0 +1,11 @@ +#!/bin/sh + +JAR=`echo target/core-pva*.jar` +if [ -r "$JAR" ] +then + # Echo use jar file + java -cp $JAR org.epics.pva.server.ServerDemo +else + # Use build output + java -cp target/classes org.epics.pva.server.ServerDemo +fi diff --git a/core/pva/src/main/java/org/epics/pva/PVASettings.java b/core/pva/src/main/java/org/epics/pva/PVASettings.java index 379151c858..1876e3170c 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -79,9 +79,11 @@ public class PVASettings *

    Example entries: * *

    -     *  192.168.10.20    Send name lookups to that IPv4 TCP address at EPICS_PVA_SERVER_PORT (default 5075)
    -     *  ::1              Search to IPv6 localhost at EPICS_PVA_SERVER_PORT
    -     *  [::1]:9876       Same with non-standard port
    +     *  192.168.10.20              Send name lookups to that IPv4 TCP address at EPICS_PVA_SERVER_PORT (default 5075)
    +     *  ::1                        Search to IPv6 localhost at EPICS_PVA_SERVER_PORT
    +     *  [::1]:9876                 Same with non-standard port
    +     *  pvas://192.168.10.20       Use TLS, defaulting to EPICS_PVAS_TLS_PORT (5076)
    +     *  pvas://192.168.10.20:5086  Use TLS with specific port
          *  
    */ public static String EPICS_PVA_NAME_SERVERS = ""; @@ -89,9 +91,12 @@ public class PVASettings /** PVA client port for sending name searches and receiving beacons */ public static int EPICS_PVA_BROADCAST_PORT = 5076; - /** First PVA port used by server */ + /** First PVA port used by plain TCP server */ public static int EPICS_PVA_SERVER_PORT = 5075; + /** First PVA port used by TLS server */ + public static int EPICS_PVAS_TLS_PORT = 5076; + /** Local addresses to which server will listen. * *

    First must be an IPv4 and/or IPv6 address that enables @@ -130,13 +135,52 @@ public class PVASettings /** Multicast address used for the local re-send of IPv4 unicasts */ public static String EPICS_PVA_MULTICAST_GROUP = "224.0.0.128"; + /** Path to PVA server keystore and truststore, a PKCS12 file that contains server's public and private key + * as well as trusted CAs that are used to verify client certificates. + * + *

    Format: "/path/to/file;password". + * + *

    When empty, PVA server does not support secure (TLS) communication. + */ + public static String EPICS_PVAS_TLS_KEYCHAIN = ""; + + /** Secure server options + * + *

      + *
    • client_cert=optional: + * Default; clients can provide certificate for "X509" authentication, + * but may also use "ca" or "anonymous" authentication + *
    • client_cert=require: + * Clients must provide certificate for "X509" authentication. + * Socket with otherwise be closed during initial handshake. + * Server will log "SSLHandshakeException: Empty client certificate chain", + * client will log "SSLHandshakeException: Received fatal alert: bad_certificate" + *
    + */ + public static String EPICS_PVAS_TLS_OPTIONS = ""; + + /** Does EPICS_PVAS_TLS_OPTIONS contain "client_cert=require"? */ + public static boolean require_client_cert; + + /** Path to PVA client keystore and truststore, a PKCS12 file that contains the certificates or root CA + * that the client will trust when verifying a server certificate, + * and optional client certificate used with x509 authentication to establish the client's name. + * + *

    Format: "/path/to/file;password". + * + *

    When empty, PVA client does not support secure (TLS) communication. + * When configured, PVA client can reply to PVA servers that offer "tls" in a search reply, + * and searches via EPICS_PVA_NAME_SERVERS will also use TLS. + */ + public static String EPICS_PVA_TLS_KEYCHAIN = ""; + /** TCP buffer size for sending data * *

    Messages are constructed within this buffer, * so it needs to be pre-configured to hold the maximum * package size. */ - // double[8 million] plus some protocol overhead + // 1 million 'double' plus some protocol overhead public static int EPICS_PVA_SEND_BUFFER_SIZE = 8001000; /** Initial TCP buffer size for receiving data @@ -198,21 +242,37 @@ public class PVASettings */ public static int EPICS_PVA_MAX_BEACON_AGE = 300; + + + /** Whether to allow PVA to use IPv6 + * + *

    If this is false then PVA will not attempt to + * use any IPv6 capability at all. This is useful if your + * system does not have any IPv6 support. + */ + public static boolean EPICS_PVA_ENABLE_IPV6 = true; + static { EPICS_PVA_ADDR_LIST = get("EPICS_PVA_ADDR_LIST", EPICS_PVA_ADDR_LIST); EPICS_PVA_AUTO_ADDR_LIST = get("EPICS_PVA_AUTO_ADDR_LIST", EPICS_PVA_AUTO_ADDR_LIST); EPICS_PVA_NAME_SERVERS = get("EPICS_PVA_NAME_SERVERS", EPICS_PVA_NAME_SERVERS); EPICS_PVA_SERVER_PORT = get("EPICS_PVA_SERVER_PORT", EPICS_PVA_SERVER_PORT); + EPICS_PVAS_TLS_PORT = get("EPICS_PVAS_TLS_PORT", EPICS_PVAS_TLS_PORT); EPICS_PVAS_INTF_ADDR_LIST = get("EPICS_PVAS_INTF_ADDR_LIST", EPICS_PVAS_INTF_ADDR_LIST).trim(); EPICS_PVA_BROADCAST_PORT = get("EPICS_PVA_BROADCAST_PORT", EPICS_PVA_BROADCAST_PORT); EPICS_PVAS_BROADCAST_PORT = get("EPICS_PVAS_BROADCAST_PORT", EPICS_PVAS_BROADCAST_PORT); EPICS_PVA_CONN_TMO = get("EPICS_PVA_CONN_TMO", EPICS_PVA_CONN_TMO); EPICS_PVA_MAX_ARRAY_FORMATTING = get("EPICS_PVA_MAX_ARRAY_FORMATTING", EPICS_PVA_MAX_ARRAY_FORMATTING); + EPICS_PVAS_TLS_KEYCHAIN = get("EPICS_PVAS_TLS_KEYCHAIN", EPICS_PVAS_TLS_KEYCHAIN); + EPICS_PVAS_TLS_OPTIONS = get("EPICS_PVAS_TLS_OPTIONS", EPICS_PVAS_TLS_OPTIONS); + require_client_cert = EPICS_PVAS_TLS_OPTIONS.contains("client_cert=require"); + EPICS_PVA_TLS_KEYCHAIN = get("EPICS_PVA_TLS_KEYCHAIN", EPICS_PVA_TLS_KEYCHAIN); EPICS_PVA_SEND_BUFFER_SIZE = get("EPICS_PVA_SEND_BUFFER_SIZE", EPICS_PVA_SEND_BUFFER_SIZE); EPICS_PVA_FAST_BEACON_MIN = get("EPICS_PVA_FAST_BEACON_MIN", EPICS_PVA_FAST_BEACON_MIN); EPICS_PVA_FAST_BEACON_MAX = get("EPICS_PVA_FAST_BEACON_MAX", EPICS_PVA_FAST_BEACON_MAX); EPICS_PVA_MAX_BEACON_AGE = get("EPICS_PVA_MAX_BEACON_AGE", EPICS_PVA_MAX_BEACON_AGE); + EPICS_PVA_ENABLE_IPV6 = get("EPICS_PVA_ENABLE_IPV6", EPICS_PVA_ENABLE_IPV6); } /** Get setting from property, environment or default diff --git a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java index 2c78726514..6d8c89415f 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java +++ b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -21,7 +21,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.logging.Level; import org.epics.pva.PVASettings; @@ -76,6 +76,9 @@ class ChannelSearch /** Minimum search for a channel is ASAP, then incrementing by 1 */ private static final int MIN_SEARCH_PERIOD = 0; + /** Seconds to delay to start a search "soon", not right now */ + private static final int SEARCH_SOON_DELAY = 5; + /** Maximum and eternal search period is every 30 sec */ private static final int MAX_SEARCH_PERIOD = 30; @@ -117,7 +120,7 @@ private class SearchedChannel /** Search buckets * *

    The {@link #current_search_bucket} selects the list - * of channels to be searched by {@link #runSeaches()}, + * of channels to be searched by {@link #runSearches()}, * which runs roughly once per second, each time moving to * the next search bucket in a ring buffer fashion. * @@ -153,7 +156,8 @@ private class SearchedChannel private final ClientUDPHandler udp; - private final Function tcp_provider; + /** Create ClientTCPHandler from IP address and 'tls' flag */ + private final BiFunction tcp_provider; /** Buffer for assembling search messages */ private final ByteBuffer send_buffer = ByteBuffer.allocate(PVASettings.MAX_UDP_UNFRAGMENTED_SEND); @@ -166,9 +170,16 @@ private class SearchedChannel b_or_mcast_search_addresses = new ArrayList<>(), name_server_addresses = new ArrayList<>(); + /** Create channel searcher + * @param udp UDP handler + * @param udp_addresses UDP addresses to search + * @param tcp_provider Function that creates ClientTCPHandler for IP address and 'tls' flag + * @param name_server_addresses TCP addresses to search + * @throws Exception on error + */ public ChannelSearch(final ClientUDPHandler udp, final List udp_addresses, - final Function tcp_provider, + final BiFunction tcp_provider, final List name_server_addresses) throws Exception { this.udp = udp; @@ -188,6 +199,10 @@ public ChannelSearch(final ClientUDPHandler udp, // while multicast and broadcast messages are not. for (AddressInfo addr : udp_addresses) { + if(addr.getAddress().getAddress() == null){ // E.g. address unreachable + logger.log(Level.CONFIG, "Skipping unreachable address " + addr.getAddress()); + continue; + } // Trouble is, how do you recognize a unicast address? if (addr.getAddress().getAddress().isMulticastAddress()) { // Multicast -> Certainly no unicast! @@ -229,7 +244,7 @@ public void start() */ public void register(final PVAChannel channel, final boolean now) { - logger.log(Level.FINE, () -> "Register search for " + channel); + logger.log(Level.FINE, () -> "Register search for " + channel + (now ? " now" : " soon")); final ClientChannelState old = channel.setState(ClientChannelState.SEARCHING); if (old == ClientChannelState.SEARCHING) @@ -239,8 +254,14 @@ public void register(final PVAChannel channel, final boolean now) synchronized (search_buckets) { - search_buckets.get(current_search_bucket.get()).add(sc); + int bucket = current_search_bucket.get(); + if (!now) + bucket = (bucket + SEARCH_SOON_DELAY) % search_buckets.size(); + search_buckets.get(bucket).add(sc); } + // Jumpstart search instead of waiting up to ~1 second for current bucket to be handled + if (now) + timer.execute(this::runSearches); } /** Stop searching for channel @@ -307,6 +328,7 @@ private void runSearches() // Determine current search bucket final int current = current_search_bucket.getAndUpdate(i -> (i + 1) % search_buckets.size()); final LinkedList bucket = search_buckets.get(current); + logger.log(Level.FINEST, () -> "Search bucket " + current); // Remove searched channels from the current bucket SearchedChannel sc; @@ -389,13 +411,15 @@ else if (count == 0) /** Issue a PVA server list request */ public void list() { + final boolean tls = !PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank(); + // Search is invoked for new SearchedChannel(channel, now) // as well as by regular, timed search. // Lock the send buffer to avoid concurrent use. synchronized (send_buffer) { logger.log(Level.FINE, "List Request"); - sendSearch(0, null); + sendSearch(0, null, tls); } } @@ -404,10 +428,16 @@ public void list() */ private void search(final Collection channels) { + // Do we support TLS? This will be encoded in the search requests + // to tell server if we can support TLS? + final boolean tls = !PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank(); + // Search via TCP for (AddressInfo name_server : name_server_addresses) { - final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress()); + // For search via TCP, do we use plain TCP or do we send the search itself via TLS? + // This is configured in EPICS_PVA_NAME_SERVERS via prefix pvas:// + final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress(), name_server.isTLS()); // In case of connection errors (TCP connection blocked by firewall), // tcp will be null @@ -425,7 +455,7 @@ private void search(final Collection channels) // Use 'any' reply address since reply will be via this TCP socket final InetSocketAddress response_address = new InetSocketAddress(0); - SearchRequest.encode(true, seq, channels, response_address , buffer); + SearchRequest.encode(true, seq, channels, response_address, tls , buffer); }; tcp.submit(search_request); } @@ -444,19 +474,23 @@ private void search(final Collection channels) // match up duplicate packets and allows debugging bucket usage final int seq = current_search_bucket.get(); logger.log(Level.FINE, () -> "UDP Search Request #" + seq + " for " + channels); - sendSearch(seq, channels); + sendSearch(seq, channels, tls); } } - /** Send a 'list' or channel search out via UDP */ - private void sendSearch(final int seq, final Collection channels) + /** Send a 'list' or channel search out via UDP + * @param seq Search sequence number + * @param channels Channels to search, null for listing channels + * @param tls Use TLS? + */ + private void sendSearch(final int seq, final Collection channels, final boolean tls) { // Buffer starts out with UNICAST bit set in the search message for (AddressInfo addr : unicast_search_addresses) { send_buffer.clear(); final InetSocketAddress response = udp.getResponseAddress(addr); - SearchRequest.encode(true, seq, channels, response, send_buffer); + SearchRequest.encode(true, seq, channels, response, tls, send_buffer); send_buffer.flip(); try { @@ -474,7 +508,7 @@ private void sendSearch(final int seq, final Collection c { send_buffer.clear(); final InetSocketAddress response = udp.getResponseAddress(addr); - SearchRequest.encode(false, seq, channels, response, send_buffer); + SearchRequest.encode(false, seq, channels, response, tls, send_buffer); send_buffer.flip(); try { diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java b/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java index 249e73af2f..ec54977c0c 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientAuthentication.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,6 +16,7 @@ import java.util.logging.Level; import org.epics.pva.common.PVAAuth; +import org.epics.pva.data.PVAFieldDesc; import org.epics.pva.data.PVAString; import org.epics.pva.data.PVAStructure; @@ -34,6 +35,24 @@ abstract class ClientAuthentication @Override public abstract String toString(); + /** X509 authentication: Server uses the 'principal' name sent with SSL certificate */ + public static final ClientAuthentication X509 = new ClientAuthentication() + { + @Override + public void encode(final ByteBuffer buffer) throws Exception + { + PVAString.encodeString(PVAAuth.X509, buffer); + // No detail because server already has name + buffer.put(PVAFieldDesc.NULL_TYPE_CODE); + } + + @Override + public String toString() + { + return PVAAuth.X509; + } + }; + /** Anonymous authentication */ public static final ClientAuthentication Anonymous = new ClientAuthentication() @@ -42,6 +61,8 @@ abstract class ClientAuthentication public void encode(final ByteBuffer buffer) throws Exception { PVAString.encodeString(PVAAuth.ANONYMOUS, buffer); + // No detail because we're anonymous + buffer.put(PVAFieldDesc.NULL_TYPE_CODE); } @Override @@ -82,6 +103,7 @@ private static class CAAuthentication extends ClientAuthentication public void encode(final ByteBuffer buffer) throws Exception { PVAString.encodeString(PVAAuth.CA, buffer); + // Send identity detail identity.encodeType(buffer, new BitSet()); identity.encode(buffer); } diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java index 5ce8845435..aa22e2451d 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,8 +10,8 @@ import static org.epics.pva.PVASettings.logger; import java.net.InetSocketAddress; +import java.net.Socket; import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -22,10 +22,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; +import javax.net.ssl.SSLSocket; + import org.epics.pva.PVASettings; import org.epics.pva.common.CommandHandlers; import org.epics.pva.common.PVAHeader; import org.epics.pva.common.RequestEncoder; +import org.epics.pva.common.SecureSockets; import org.epics.pva.common.TCPHandler; import org.epics.pva.data.PVATypeRegistry; import org.epics.pva.server.Guid; @@ -56,6 +59,13 @@ class ClientTCPHandler extends TCPHandler /** Client context */ private final PVAClient client; + /** When using TLS, the socket may come with a local certificate + * that TLS uses to authenticate to the server, + * and this is the name from that certificate. + * Otherwise null + */ + private String x509_name; + /** Channels that use this connection */ private final CopyOnWriteArrayList channels = new CopyOnWriteArrayList<>(); @@ -97,31 +107,35 @@ class ClientTCPHandler extends TCPHandler * Client must not send get/put/.. messages until * this flag is set. */ - private final AtomicBoolean connection_validated = new AtomicBoolean(); + private final AtomicBoolean connection_validated = new AtomicBoolean(false); - public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid) throws Exception + public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid, final boolean tls) throws Exception { - super(createSocket(address), true); + super(createSocket(address, tls), true); logger.log(Level.FINE, () -> "TCPHandler " + guid + " for " + address + " created ============================"); this.client = client; this.guid = guid; + // For TLS, check if the socket has a name that's used to authenticate + x509_name = tls ? SecureSockets.getLocalPrincipalName((SSLSocket) socket) : null; + // For default EPICS_CA_CONN_TMO: 30 sec, send echo at ~15 sec: // Check every ~3 seconds last_life_sign = last_message_sent = System.currentTimeMillis(); final long period = Math.max(1, PVASettings.EPICS_PVA_CONN_TMO * 1000L / 30 * 3); alive_check = timer.scheduleWithFixedDelay(this::checkResponsiveness, period, period, TimeUnit.MILLISECONDS); - // Don't start the send thread, yet. + + // Start receiver, but not the send thread, yet. // To prevent sending messages before the server is ready, // it's started when server confirms the connection. + startReceiver(); } - private static SocketChannel createSocket(InetSocketAddress address) throws Exception + private static Socket createSocket(final InetSocketAddress address, final boolean tls) throws Exception { - final SocketChannel socket = SocketChannel.open(address); - socket.configureBlocking(true); - socket.socket().setTcpNoDelay(true); - socket.socket().setKeepAlive(true); + final Socket socket = SecureSockets.createClientSocket(address, tls); + socket.setTcpNoDelay(true); + socket.setKeepAlive(true); return socket; } @@ -131,6 +145,12 @@ PVAClient getClient() return client; } + /** @return Name used by TLS socket's certificate, or null */ + String getX509Name() + { + return x509_name; + } + /** @param channel Channel that uses this TCP connection */ void addChannel(final PVAChannel channel) { diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java index ed79700766..bc7051caa5 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -60,8 +60,9 @@ public interface SearchResponseHandler * @param server Server that replied to a search request * @param version Server version * @param guid Globally unique ID of the server + * @param tcp Does server require TLS? */ - void handleSearchResponse(int channel_id, InetSocketAddress server, int version, Guid guid); + void handleSearchResponse(int channel_id, InetSocketAddress server, int version, Guid guid, boolean tls); } private final BeaconHandler beacon_handler; @@ -99,19 +100,26 @@ public ClientUDPHandler(final BeaconHandler beacon_handler, udp_search4 = Network.createUDP(StandardProtocolFamily.INET, null, 0); udp_search4.socket().setBroadcast(true); local_multicast = Network.configureLocalIPv4Multicast(udp_search4, PVASettings.EPICS_PVA_BROADCAST_PORT); + udp_localaddr4 = (InetSocketAddress) udp_search4.getLocalAddress(); - // IPv6 socket - udp_search6 = Network.createUDP(StandardProtocolFamily.INET6, null, 0); + String ipV6Msg; + // IPv6 sockets // Beacon socket only receives, does not send broadcasts - udp_beacon = Network.createUDP(StandardProtocolFamily.INET6, null, PVASettings.EPICS_PVA_BROADCAST_PORT); - - udp_localaddr4 = (InetSocketAddress) udp_search4.getLocalAddress(); - udp_localaddr6 = (InetSocketAddress) udp_search6.getLocalAddress(); - - logger.log(Level.FINE, "Awaiting search replies on UDP " + udp_localaddr4 + - " and " + udp_localaddr6 + - ", and beacons on " + Network.getLocalAddress(udp_beacon)); + if (PVASettings.EPICS_PVA_ENABLE_IPV6) { + udp_search6 = Network.createUDP(StandardProtocolFamily.INET6, null, 0); + udp_localaddr6 = (InetSocketAddress) udp_search6.getLocalAddress(); + ipV6Msg = String.format(" and %s", udp_localaddr6); + udp_beacon = Network.createUDP(StandardProtocolFamily.INET6, null, PVASettings.EPICS_PVA_BROADCAST_PORT); + } + else { + udp_search6 = null; + udp_beacon = Network.createUDP(StandardProtocolFamily.INET, null, PVASettings.EPICS_PVA_BROADCAST_PORT); + udp_localaddr6 = null; + ipV6Msg = ""; + } + String logMsg = String.format("Awaiting search replies on UDP %s%s and beacons on %s", udp_localaddr4, ipV6Msg, Network.getLocalAddress(udp_beacon)); + logger.log(Level.FINE, logMsg); } /** @param target Address to which message will be sent @@ -141,6 +149,12 @@ public void send(final ByteBuffer buffer, final AddressInfo info) throws Excepti } else { + if (!PVASettings.EPICS_PVA_ENABLE_IPV6) { + throw new Exception( + "EPICS_PVA_ENABLE_IPV6 must be enabled to use IPv6 address!" + ); + } + synchronized (udp_search6) { if (info.getAddress().getAddress().isMulticastAddress()) @@ -162,10 +176,12 @@ public void start() search_thread4.setDaemon(true); search_thread4.start(); - final ByteBuffer receive_buffer6 = ByteBuffer.allocate(PVASettings.MAX_UDP_PACKET); - search_thread6 = new Thread(() -> listen(udp_search6, receive_buffer6), "UDP6-receiver " + Network.getLocalAddress(udp_search6)); - search_thread6.setDaemon(true); - search_thread6.start(); + if (PVASettings.EPICS_PVA_ENABLE_IPV6) { + final ByteBuffer receive_buffer6 = ByteBuffer.allocate(PVASettings.MAX_UDP_PACKET); + search_thread6 = new Thread(() -> listen(udp_search6, receive_buffer6), "UDP6-receiver " + Network.getLocalAddress(udp_search6)); + search_thread6.setDaemon(true); + search_thread6.start(); + } beacon_thread = new Thread(() -> listen(udp_beacon, beacon_buffer), "UDP-beacon-receiver " + Network.getLocalAddress(udp_beacon)); beacon_thread.setDaemon(true); @@ -283,7 +299,7 @@ private boolean handleSearchRequest(final InetSocketAddress from, final byte ver if (search.reply_required) { forward_buffer.clear(); - SearchRequest.encode(false, 0, null, search.client, forward_buffer); + SearchRequest.encode(false, 0, null, search.client, search.tls, forward_buffer); forward_buffer.flip(); logger.log(Level.FINER, () -> "Forward search to list servers to " + local_multicast + "\n" + Hexdump.toHexdump(forward_buffer)); send(forward_buffer, local_multicast); @@ -292,7 +308,7 @@ private boolean handleSearchRequest(final InetSocketAddress from, final byte ver else { forward_buffer.clear(); - SearchRequest.encode(false, search.seq, search.channels, search.client, forward_buffer); + SearchRequest.encode(false, search.seq, search.channels, search.client, search.tls, forward_buffer); forward_buffer.flip(); logger.log(Level.FINER, () -> "Forward search to " + local_multicast + "\n" + Hexdump.toHexdump(forward_buffer)); send(forward_buffer, local_multicast); @@ -321,10 +337,10 @@ private boolean handleSearchReply(final InetSocketAddress from, final byte versi // Server may reply with list of PVs that it does _not_ have... if (! response.found) - search_response.handleSearchResponse(-1, server, version, response.guid); + search_response.handleSearchResponse(-1, server, version, response.guid, response.tls); else for (int cid : response.cid) - search_response.handleSearchResponse(cid, server, version, response.guid); + search_response.handleSearchResponse(cid, server, version, response.guid, response.tls); } catch (Exception ex) { @@ -342,9 +358,11 @@ public void close() // Close sockets, wait a little for threads to exit try { - udp_search6.close(); - udp_search4.close(); + if (PVASettings.EPICS_PVA_ENABLE_IPV6){ + udp_search6.close(); + } udp_beacon.close(); + udp_search4.close(); if (search_thread6 != null) search_thread6.join(5000); diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java b/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java index d7a96ff36b..610ab5fc5b 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAChannel.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,7 +11,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +38,13 @@ * *

    When no longer in use, the channel should be {@link #close()}d. * + * + * Note that several methods return a CompletableFuture. + * This has been done because at this time the Futures used internally are indeed CompletableFutures + * and this type offers an extensive API for composition and chaining of futures. + * But note that user code must never call 'complete(..)' nor 'completeExceptionally()' + * on the provided CompletableFutures. + * * @author Kay Kasemir */ @SuppressWarnings("nls") @@ -50,6 +56,8 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable * Since the server tends to start at 1, this makes it * more obvious in tests which ID is the SID * and which is the CID. + * + * PVXS starts first channel with ID 0x12345678 */ private static final AtomicInteger CID_Provider = new AtomicInteger(1); @@ -114,7 +122,7 @@ public boolean isConnected() } /** Wait for channel to connect - * @return {@link Future} to await connection. + * @return {@link CompletableFuture} to await connection. * true on success, * {@link TimeoutException} on timeout. */ @@ -205,18 +213,18 @@ boolean resetConnection() * the values are not set. * * @param subfield Sub field to get, "" for complete data type - * @return {@link Future} for fetching the result + * @return {@link CompletableFuture} for fetching the result */ - public Future info(final String subfield) + public CompletableFuture info(final String subfield) { return new GetTypeRequest(this, subfield); } /** Read (get) channel's value from server * @param request Request, "" for all fields, or "field_a, field_b.subfield" - * @return {@link Future} for fetching the result + * @return {@link CompletableFuture} for fetching the result */ - public Future read(final String request) + public CompletableFuture read(final String request) { return new GetRequest(this, request); } @@ -241,11 +249,11 @@ public Future read(final String request) * @param request Request for element to write, e.g. "field(value)" * @param new_value New value: Number, String * @throws Exception on error - * @return {@link Future} for awaiting completion and getting Exception in case of error + * @return {@link CompletableFuture} for awaiting completion and getting Exception in case of error * @deprecated Use {@link #write(boolean, String, Object)} */ @Deprecated - public Future write(final String request, final Object new_value) throws Exception + public CompletableFuture write(final String request, final Object new_value) throws Exception { return write(false, request, new_value); } @@ -271,9 +279,9 @@ public Future write(final String request, final Object new_value) throws E * @param request Request for element to write, e.g. "field(value)" * @param new_value New value: Number, String * @throws Exception on error - * @return {@link Future} for awaiting completion and getting Exception in case of error + * @return {@link CompletableFuture} for awaiting completion and getting Exception in case of error */ - public Future write(final boolean completion, final String request, final Object new_value) throws Exception + public CompletableFuture write(final boolean completion, final String request, final Object new_value) throws Exception { return new PutRequest(this, completion, request, new_value); } @@ -315,9 +323,9 @@ public AutoCloseable subscribe(final String request, final int pipeline, final M /** Invoke remote procedure call (RPC) * @param request Request, i.e. parameters sent to the RPC call - * @return {@link Future} for fetching the result returned by the RPC call + * @return {@link CompletableFuture} for fetching the result returned by the RPC call */ - public Future invoke(final PVAStructure request) + public CompletableFuture invoke(final PVAStructure request) { return new RPCRequest(this, request); } diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java index a7306ad84e..4f22ad140a 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.logging.Level; import org.epics.pva.PVASettings; @@ -89,13 +89,13 @@ public PVAClient() throws Exception // TCP traffic is handled by one ClientTCPHandler per address (IP, socket). // Pass helper to channel search for getting such a handler. - final Function tcp_provider = the_addr -> + final BiFunction tcp_provider = (the_addr, use_tls) -> tcp_handlers.computeIfAbsent(the_addr, addr -> { try { // If absent, create with initial empty GUID - return new ClientTCPHandler(this, addr, Guid.EMPTY); + return new ClientTCPHandler(this, addr, Guid.EMPTY, use_tls); } catch (Exception ex) { @@ -216,7 +216,7 @@ private void handleBeacon(final InetSocketAddress server, final Guid guid, final search.boost(); } - void handleSearchResponse(final int channel_id, final InetSocketAddress server, final int version, final Guid guid) + void handleSearchResponse(final int channel_id, final InetSocketAddress server, final int version, final Guid guid, final boolean tls) { // Generic server 'list' response? if (channel_id < 0) @@ -248,13 +248,13 @@ void handleSearchResponse(final int channel_id, final InetSocketAddress server, return; } channel.setState(ClientChannelState.FOUND); - logger.log(Level.FINE, () -> "Reply for " + channel + " from " + server + " " + guid); + logger.log(Level.FINE, () -> "Reply for " + channel + " from " + (tls ? "TLS " : "TCP ") + server + " " + guid); final ClientTCPHandler tcp = tcp_handlers.computeIfAbsent(server, addr -> { try { - return new ClientTCPHandler(this, addr, guid); + return new ClientTCPHandler(this, addr, guid, tls); } catch (Exception ex) { @@ -262,9 +262,14 @@ void handleSearchResponse(final int channel_id, final InetSocketAddress server, } return null; }); - // In case of connection errors (TCP connection blocked by firewall), - // tcp will be null - if (tcp != null) + // In case of connection errors, tcp will be null + if (tcp == null) + { // Cannot connect to server on provided port? Likely a server or firewall problem. + // On the next search, that same server might reply and then we fail the same way on connect. + // Still, no way around re-registering the search so we succeed once the server is fixed. + search.register(channel, false /* not "now" but eventually */); + } + else { if (tcp.updateGuid(guid)) logger.log(Level.FINE, "Search-only TCP handler received GUID, now " + tcp); diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java index d70082b7d3..93d9b14f7a 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,6 +16,7 @@ import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; +import java.util.logging.Logger; import org.epics.pva.PVASettings; import org.epics.pva.data.PVAData; @@ -59,6 +60,7 @@ private static void help() private static void setLogLevel(final Level level) { PVASettings.logger.setLevel(level); + Logger.getLogger("jdk.event.security").setLevel(level); } /** Get info for each PV on the list, then close PV diff --git a/core/pva/src/main/java/org/epics/pva/client/PutRequest.java b/core/pva/src/main/java/org/epics/pva/client/PutRequest.java index b14400946a..23ee9d4149 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PutRequest.java +++ b/core/pva/src/main/java/org/epics/pva/client/PutRequest.java @@ -11,6 +11,7 @@ import java.nio.ByteBuffer; import java.util.BitSet; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -137,18 +138,41 @@ public void encodeRequest(final byte version, final ByteBuffer buffer) throws Ex fail(ex); } + // Bitset to describe which field(s) we're about to write + final BitSet changed = new BitSet(); if (field instanceof PVAStructure) { final PVAStructure struct = (PVAStructure) field; // For enumerated type, write to index. if ("enum_t".equals(struct.getStructureName()) || - data.getStructureName().toLowerCase().indexOf("ntenum") > 0) + data.getStructureName().toLowerCase().indexOf("ntenum") > 0){ field = struct.get("index"); + } + else{ + // Must also set bits for the elements of the structure + List elements = struct.get(); + if(elements != null){ + for(int i = 0; i < elements.size(); i++){ + changed.set(data.getIndex(elements.get(i))); + } + } + } } + // Set bit for the field to write + changed.set(data.getIndex(field)); // Bitset to describe which field we're about to write - final BitSet changed = new BitSet(); + //final BitSet changed = new BitSet(); changed.set(data.getIndex(field)); + if (field instanceof PVAStructure) { + final PVAStructure struct = (PVAStructure) field; + List elements = struct.get(); + if(elements != null){ + for(int i = 0; i < elements.size(); i++){ + changed.set(data.getIndex(elements.get(i))); + } + } + } logger.log(Level.FINE, () -> "Updated structure elements: " + changed); PVABitSet.encodeBitSet(changed, buffer); diff --git a/core/pva/src/main/java/org/epics/pva/client/SearchResponseHandler.java b/core/pva/src/main/java/org/epics/pva/client/SearchResponseHandler.java index 24aa86aaee..3425948df1 100644 --- a/core/pva/src/main/java/org/epics/pva/client/SearchResponseHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/SearchResponseHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -64,13 +64,13 @@ public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) t final PVAChannel channel = tcp.getClient().getChannel(cid); if (channel == null) - logger.log(Level.FINE, "Got search response for unknown CID " + cid + " from " + server + " " + + logger.log(Level.FINE, "Got search response for unknown CID " + cid + " from " + (response.tls ? "TLS " : "TCP ") + server + " " + response.guid + " V" + response.version); else { - logger.log(Level.FINE, "Got search response for " + channel + " from " + response.server + + logger.log(Level.FINE, "Got search response for " + channel + " from " + (response.tls ? "TLS " : "TCP ") + response.server + " (using " + server + ") " + response.guid + " V" + response.version); - tcp.getClient().handleSearchResponse(cid, server, response.version, response.guid); + tcp.getClient().handleSearchResponse(cid, server, response.version, response.guid, response.tls); } } } diff --git a/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java b/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java index 28beda9bf9..488e3eec3e 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ValidationHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -14,6 +14,7 @@ import java.util.List; import org.epics.pva.common.CommandHandler; +import org.epics.pva.common.PVAAuth; import org.epics.pva.common.PVAHeader; import org.epics.pva.data.PVASize; import org.epics.pva.data.PVAString; @@ -48,9 +49,12 @@ public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) t logger.finer(() -> "Server registry max size: " + server_introspection_registry_max_size); logger.finer(() -> "Server authentication methods: " + auth); - // Support "ca" authorization, fall back to anonymouse + // Support "x509" or "ca" authorization, fall back to any-no-mouse final ClientAuthentication authentication; - if (auth.contains("ca")) + // Even if server suggests x509, check that we have a certificate with name + if (tcp.getX509Name() != null && auth.contains(PVAAuth.X509)) + authentication = ClientAuthentication.X509; + else if (auth.contains(PVAAuth.CA)) authentication = ClientAuthentication.CA; else authentication = ClientAuthentication.Anonymous; diff --git a/core/pva/src/main/java/org/epics/pva/common/AddressInfo.java b/core/pva/src/main/java/org/epics/pva/common/AddressInfo.java index cff2dd67d7..5e7ef08ba5 100644 --- a/core/pva/src/main/java/org/epics/pva/common/AddressInfo.java +++ b/core/pva/src/main/java/org/epics/pva/common/AddressInfo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,16 +19,19 @@ @SuppressWarnings("nls") public class AddressInfo { + private final boolean tls; private final InetSocketAddress address; private final int ttl; private final NetworkInterface iface; - /** @param address Network address and port + /** @param tls Is address for TLS, a secure TCP socket? + * @param address Network address and port * @param ttl Time-to-live for UDP packets * @param iface Interface via which to send/receive */ - public AddressInfo(final InetSocketAddress address, final int ttl, final NetworkInterface iface) + public AddressInfo(final boolean tls, final InetSocketAddress address, final int ttl, final NetworkInterface iface) { + this.tls = tls; this.address = address; this.ttl = ttl; this.iface = iface; @@ -46,6 +49,12 @@ public boolean isIPv6() return address.getAddress() instanceof Inet6Address; } + /** @return Is the address for a TLS socket? EPICS_PVA_NAME_SERVERS=pvas://...? */ + public boolean isTLS() + { + return tls; + } + /** @return Network address */ public InetSocketAddress getAddress() { @@ -114,6 +123,8 @@ else if (isIPv4()) buf.append("IPv4 address "); else buf.append("Unknown INET type address "); + if (tls) + buf.append("TLS "); buf.append(address.getHostString()); if (address.getAddress().isAnyLocalAddress()) buf.append(" (ANY LOCAL)"); diff --git a/core/pva/src/main/java/org/epics/pva/common/Network.java b/core/pva/src/main/java/org/epics/pva/common/Network.java index 53cac2be43..bbed69c232 100644 --- a/core/pva/src/main/java/org/epics/pva/common/Network.java +++ b/core/pva/src/main/java/org/epics/pva/common/Network.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -68,7 +68,7 @@ public static List getBroadcastAddresses(final int port) for (InterfaceAddress addr : iface.getInterfaceAddresses()) if (addr.getBroadcast() != null) { - final AddressInfo bcast = new AddressInfo(new InetSocketAddress(addr.getBroadcast(), port), 1, iface); + final AddressInfo bcast = new AddressInfo(false, new InetSocketAddress(addr.getBroadcast(), port), 1, iface); if (! addresses.contains(bcast)) addresses.add(bcast); } @@ -85,7 +85,7 @@ public static List getBroadcastAddresses(final int port) } if (addresses.isEmpty()) - addresses.add(new AddressInfo(new InetSocketAddress("255.255.255.255", port), 1, null)); + addresses.add(new AddressInfo(false, new InetSocketAddress("255.255.255.255", port), 1, null)); return addresses; } @@ -103,6 +103,7 @@ public static List getBroadcastAddresses(final int port) * [::1]:9876 IPv6 address with port * 224.0.2.3,255@192.168.1.1 IPv4 224.0.2.3, TTL 255, using interface with address 192.168.1.1 * [ff02::42:1]:5076,1@br0 IPv6 ff02::42:1, port 5076, TTL 1, interface br0 + * pvas://.... Request TLS, i.e. secure TCP, and default to EPICS_PVAS_TLS_PORT * * * @param setting Address "IP:port,TTL@iface" to parse @@ -117,6 +118,14 @@ public static AddressInfo parseAddress(final String setting, final int default_p int port = default_port; String parsed = setting; + + final boolean tls = parsed.startsWith("pvas://"); + if (tls) + { + parsed = parsed.substring(7); + port = PVASettings.EPICS_PVAS_TLS_PORT; + } + // Optional @interface int sep = parsed.lastIndexOf('@'); if (sep >= 0) @@ -175,7 +184,7 @@ else if (sep >= 0) // Parse remaining address final InetSocketAddress address = new InetSocketAddress(parsed, port); - return new AddressInfo(address, ttl, iface); + return new AddressInfo(tls, address, ttl, iface); } /** Parse network addresses @@ -298,7 +307,7 @@ public static AddressInfo configureLocalIPv4Multicast(final DatagramChannel udp, udp.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, true); udp.setOption(StandardSocketOptions.IP_MULTICAST_IF, loopback); - return new AddressInfo(local_multicast, 1, loopback); + return new AddressInfo(false, local_multicast, 1, loopback); } } catch (Exception ex) diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java b/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java index 0d340ff9de..bb93b30477 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAAuth.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,4 +18,7 @@ public class PVAAuth /** CA authentication based on user name and host */ public static String CA = "ca"; + + /**Authentication based on 'Common Name' in certificate */ + public static String X509 = "x509"; } diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java index edd8279aa4..f95019b54b 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -71,6 +71,8 @@ public String toString() public boolean reply_required; /** Address of client */ public InetSocketAddress client; + /** Use TLS, or plain TCP? */ + public boolean tls; /** Names requested in search, null for 'list' */ public List channels; @@ -134,17 +136,18 @@ public static SearchRequest decode(final InetSocketAddress from, final byte vers search.client = new InetSocketAddress(addr, port); // Assert that client supports "tcp", ignore rest - boolean tcp = false; + boolean tcp = search.tls = false; int count = Byte.toUnsignedInt(buffer.get()); - String protocol = ""; + String unknown_protocol = ""; for (int i=0; i(count); @@ -179,9 +182,10 @@ public static SearchRequest decode(final InetSocketAddress from, final byte vers * @param seq Sequence number * @param channels Channels to search, null for 'list' * @param address client's address + * @param tls Use TLS? * @param buffer Buffer into which to encode */ - public static void encode(final boolean unicast, final int seq, final Collection channels, final InetSocketAddress address, final ByteBuffer buffer) + public static void encode(final boolean unicast, final int seq, final Collection channels, final InetSocketAddress address, final boolean tls, final ByteBuffer buffer) { // Create with zero payload size, to be patched later PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_NONE, PVAHeader.CMD_SEARCH, 0); @@ -189,6 +193,8 @@ public static void encode(final boolean unicast, final int seq, final Collection final int payload_start = buffer.position(); // SEARCH message sequence + // PVXS sends "find".getBytes() instead + // For TCP search via EPICS_PVA_NAME_SERVERS, we send "look" ("kool" for little endian) buffer.putInt(seq); // If a host has multiple listeners on the UDP search port, @@ -219,7 +225,14 @@ public static void encode(final boolean unicast, final int seq, final Collection } else { - buffer.put((byte)1); + // Support both tls and tcp, or only tcp? + if (tls) + { + buffer.put((byte)2); + PVAString.encodeString("tls", buffer); + } + else + buffer.put((byte)1); PVAString.encodeString("tcp", buffer); buffer.putShort((short)channels.size()); diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java index d59ffdcb84..1367d34626 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -34,6 +34,9 @@ public class SearchResponse /** Server's address, may be all zero (any local) */ public InetSocketAddress server; + /** Did server request usage of TLS? */ + public boolean tls; + /** Did server reply that channels are found, or did it send negative response? */ public boolean found; @@ -85,7 +88,9 @@ public static SearchResponse decode(final int payload, final ByteBuffer buffer) result.server = new InetSocketAddress(addr, port); final String protocol = PVAString.decodeString(buffer); - if (! "tcp".equals(protocol)) + + result.tls = "tls".equals(protocol); + if (! result.tls && ! "tcp".equals(protocol)) throw new Exception("PVA Server sent search reply #" + result.seq + " for protocol '" + protocol + "'"); // Server may reply with list of PVs that it does _not_ have... @@ -105,10 +110,12 @@ public static SearchResponse decode(final int payload, final ByteBuffer buffer) * @param cid Client's channel ID or -1 * @param address Address where client can connect to access the channel * @param port Associated TCP port + * @param tls Use TLS? Otherwise plain TCP * @param buffer Buffer into which search response will be encoded */ public static void encode(final Guid guid, final int seq, final int cid, final InetAddress address, final int port, + final boolean tls, final ByteBuffer buffer) { PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_SEARCH_RESPONSE, 12+4+16+2+4+1+2+ (cid < 0 ? 0 : 4)); @@ -124,7 +131,7 @@ public static void encode(final Guid guid, final int seq, final int cid, buffer.putShort((short)port); // Protocol - PVAString.encodeString("tcp", buffer); + PVAString.encodeString(tls ? "tls" : "tcp", buffer); // Found PVABool.encodeBoolean(cid >= 0, buffer); diff --git a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java new file mode 100644 index 0000000000..9710eb952b --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2023 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.epics.pva.common; + +import static org.epics.pva.PVASettings.logger; + +import java.io.FileInputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.KeyStore; +import java.util.logging.Level; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import org.epics.pva.PVASettings; + +/** Helpers for creating secure sockets + * + * By default, provide plain TCP sockets. + * + * To enable TLS sockets, EPICS_PVAS_TLS_KEYCHAIN can be set to + * select a key- and truststore for the server, and EPICS_PVA_TLS_KEYCHAIN can define + * one for the client. + * + * @author Kay Kasemir + */ +@SuppressWarnings("nls") +public class SecureSockets +{ + /** Supported protocols. PVXS prefers 1.3 */ + private static final String[] PROTOCOLS = new String[] { "TLSv1.3"}; + + private static boolean initialized = false; + private static SSLServerSocketFactory tls_server_sockets; + private static SSLSocketFactory tls_client_sockets; + + /** @param keychain_setting "/path/to/keychain;password" + * @return {@link SSLContext} with 'keystore' and 'truststore' set to content of keystore + * @throws Exception on error + */ + private static SSLContext createContext(final String keychain_setting) throws Exception + { + final String path; + final char[] pass; + + // We support the default "" empty as well as actual passwords, but not null for no password + final int sep = keychain_setting.indexOf(';'); + if (sep > 0) + { + path = keychain_setting.substring(0, sep); + pass = keychain_setting.substring(sep+1).toCharArray(); + } + else + { + path = keychain_setting; + pass = "".toCharArray(); + } + + logger.log(Level.CONFIG, () -> "Loading keychain '" + path + "'"); + + final KeyStore key_store = KeyStore.getInstance("PKCS12"); + key_store.load(new FileInputStream(path), pass); + + final KeyManagerFactory key_manager = KeyManagerFactory.getInstance("PKIX"); + key_manager.init(key_store, pass); + + final TrustManagerFactory trust_manager = TrustManagerFactory.getInstance("PKIX"); + trust_manager.init(key_store); + + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(key_manager.getKeyManagers(), trust_manager.getTrustManagers(), null); + + return context; + } + + private static synchronized void initialize() throws Exception + { + if (initialized) + return; + + if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank()) + { + final SSLContext context = createContext(PVASettings.EPICS_PVAS_TLS_KEYCHAIN); + tls_server_sockets = context.getServerSocketFactory(); + } + + if (! PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank()) + { + final SSLContext context = createContext(PVASettings.EPICS_PVA_TLS_KEYCHAIN); + tls_client_sockets = context.getSocketFactory(); + } + initialized = true; + } + + /** Create server socket + * @param address IP address and port to which the socket will be bound + * @param tls Use TLS socket? Otherwise plain TCP + * @return Plain or secure server socket + * @throws Exception on error + */ + public static ServerSocket createServerSocket(final InetSocketAddress address, final boolean tls) throws Exception + { + initialize(); + final ServerSocket socket; + if (tls) + { + if (tls_server_sockets == null) + throw new Exception("TLS is not supported. Configure EPICS_PVAS_TLS_KEYCHAIN"); + socket = tls_server_sockets.createServerSocket(); + final SSLServerSocket ssl = (SSLServerSocket) socket; + + // Do we require client's certificate with 'principal' name for x509 authentication, + // and initial handshake will otherwise fail? + // Or do we support a client certificate, but it's not essential? + if (PVASettings.require_client_cert) + { + ssl.setNeedClientAuth(true); + logger.log(Level.FINE, "Server requires client certificate"); + } + else + ssl.setWantClientAuth(true); + ssl.setEnabledProtocols(PROTOCOLS); + } + else + socket = new ServerSocket(); + + try + { + socket.setReuseAddress(true); + socket.bind(address); + } + catch (Exception ex) + { + socket.close(); + throw ex; + } + return socket; + } + + /** Create client socket + * @param address IP address and port to which the socket will be bound + * @param tls Use TLS socket? Otherwise plain TCP + * @return Plain or secure client socket + * @throws Exception on error + */ + public static Socket createClientSocket(final InetSocketAddress address, final boolean tls) throws Exception + { + initialize(); + if (! tls) + return new Socket(address.getAddress(), address.getPort()); + + if (tls_client_sockets == null) + throw new Exception("TLS is not supported. Configure EPICS_PVA_TLS_KEYCHAIN"); + final SSLSocket socket = (SSLSocket) tls_client_sockets.createSocket(address.getAddress(), address.getPort()); + socket.setEnabledProtocols(PROTOCOLS); + // Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket + socket.startHandshake(); + return socket; + } + + /** Get name from local principal + * + * @param socket {@link SSLSocket} that may have local principal + * @return Name (without "CN=..") or null if socket has certificate to authenticate + */ + public static String getLocalPrincipalName(final SSLSocket socket) + { + try + { + String name = socket.getSession().getLocalPrincipal().getName(); + if (name.startsWith("CN=")) + name = name.substring(3); + else + logger.log(Level.WARNING, "Client has principal '" + name + "', expected 'CN=...'"); + return name; + } + catch (Exception ex) + { // May not have certificate with name + } + return null; + } + + /** Information from TLS socket handshake */ + public static class TLSHandshakeInfo + { + /** Name by which the peer identified */ + public String name; + + /** Host of the peer */ + public String hostname; + + + /** Get TLS/SSH info from socket + * @param socket {@link SSLSocket} + * @return {@link TLSHandshakeInfo} or null + * @throws Exception on error + */ + public static TLSHandshakeInfo fromSocket(final SSLSocket socket) throws Exception + { + // Start ASAP instead of waiting for first read/write on socket. + // "This method is synchronous for the initial handshake on a connection + // and returns when the negotiated handshake is complete", + // so no need to addHandshakeCompletedListener() + + // If server socket was configured to require client authentication, + // there will be an SSLHandshakeException with message "Empty client certificate chain", + // but no obvious way to catch that + socket.startHandshake(); + + try + { + // No way to check if there is peer info (certificates, principal, ...) + // other then success vs. exception.. + String name = socket.getSession().getPeerPrincipal().getName(); + if (name.startsWith("CN=")) + name = name.substring(3); + else + logger.log(Level.WARNING, "Peer " + socket.getInetAddress() + " sent '" + name + "' as principal name, expected 'CN=...'"); + final TLSHandshakeInfo info = new TLSHandshakeInfo(); + info.name = name; + + info.hostname = socket.getInetAddress().getHostName(); + + return info; + } + catch (Exception ex) + { + // Clients may not have a certificate.. + // System.out.println("No x509 name from client"); + // ex.printStackTrace(); + } + return null; + } + } +} diff --git a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java index 2acf6a51fb..b8adebabb4 100644 --- a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -9,11 +9,14 @@ import static org.epics.pva.PVASettings.logger; +import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.channels.SocketChannel; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -58,8 +61,12 @@ abstract public class TCPHandler /** Is this the client, expecting received messages to be marked as server messages? */ private final boolean client_mode; - /** TCP socket to PVA peer */ - private final SocketChannel socket; + /** TCP socket to PVA peer + * + * Reading and writing is handled by receive and send threads, + * but 'protected' so that derived classes may peek at socket properties. + */ + protected final Socket socket; /** Flag to indicate that 'close' was called to close the 'socket' */ protected volatile boolean running = true; @@ -90,6 +97,7 @@ public void encodeRequest(final byte version, final ByteBuffer buffer) throws Ex }; /** Pool for sender and receiver threads */ + // Default keeps idle threads for one minute private static final ExecutorService thread_pool = Executors.newCachedThreadPool(runnable -> { final Thread thread = new Thread(runnable); @@ -98,10 +106,10 @@ public void encodeRequest(final byte version, final ByteBuffer buffer) throws Ex }); /** Thread that runs {@link TCPHandler#receiver()} */ - private final Future receive_thread; + private volatile Future receive_thread = null; /** Thread that runs {@link TCPHandler#sender()} */ - private volatile Future send_thread; + private volatile Future send_thread = null; /** Start receiving messages * @@ -113,9 +121,9 @@ public void encodeRequest(final byte version, final ByteBuffer buffer) throws Ex * @param client_mode Is this the client, expecting to receive messages from server? * @see #startSender() */ - public TCPHandler(final SocketChannel socket, final boolean client_mode) + public TCPHandler(final Socket socket, final boolean client_mode) { - this.socket = socket; + this.socket = Objects.requireNonNull(socket); this.client_mode = client_mode; // Receive buffer byte order is set based on header flag of each received message. @@ -123,8 +131,13 @@ public TCPHandler(final SocketChannel socket, final boolean client_mode) // For server, it stays that way. // For client, order is updated during connection validation (PVAHeader.CTRL_SET_BYTE_ORDER) send_buffer.order(ByteOrder.nativeOrder()); + } - // Start receiving data + /** Start receiving data + * To be called by Client/ServerTCPHandler when fully constructed + */ + protected void startReceiver() + { receive_thread = thread_pool.submit(this::receiver); } @@ -146,7 +159,7 @@ protected void startSender() throws Exception /** @return Remote address of this end of the TCP socket */ public InetSocketAddress getRemoteAddress() { - return new InetSocketAddress(socket.socket().getInetAddress(), socket.socket().getPort()); + return new InetSocketAddress(socket.getInetAddress(), socket.getPort()); } /** @return Is the send queue idle/empty? */ @@ -173,8 +186,8 @@ private Void sender() { try { - Thread.currentThread().setName("TCP sender from " + socket.getLocalAddress() + " to " + socket.getRemoteAddress()); - logger.log(Level.FINER, Thread.currentThread().getName() + " started"); + Thread.currentThread().setName("TCP sender from " + socket.getLocalSocketAddress() + " to " + socket.getRemoteSocketAddress()); + logger.log(Level.FINER, () -> Thread.currentThread().getName() + " started"); while (true) { send_buffer.clear(); @@ -219,35 +232,26 @@ protected void send(final ByteBuffer buffer) throws Exception // Limiting buffer size increases performance. final int batch_limit = server_buffer_size / 2; final int total = buffer.limit(); - int batch = total - buffer.position(); + int pos = buffer.position(); + int batch = total - pos; if (batch > batch_limit) { batch = batch_limit; - buffer.limit(buffer.position() + batch); + buffer.limit(pos + batch); } - int tries = 0; + final OutputStream out = socket.getOutputStream(); while (batch > 0) { - final int sent = socket.write(buffer); - if (sent < 0) - throw new Exception("Connection closed"); - else if (sent == 0) - { - logger.log(Level.FINER, "Send buffer full after " + buffer.position() + " of " + total + " bytes."); - Thread.sleep(Math.max(++tries * 100, 1000)); - } - else - { - // Wrote _something_ - tries = 0; - // Determine next batch - batch = total - buffer.position(); - if (batch > batch_limit) - batch = batch_limit; - // In case batch > 0, move limit to write that batch - buffer.limit(buffer.position() + batch); - } + out.write(buffer.array(), pos, batch); + pos += batch; + buffer.position(pos); + // Determine next batch + batch = total - pos; + if (batch > batch_limit) + batch = batch_limit; + // Move limit to write that batch + buffer.limit(buffer.position() + batch); } } @@ -256,10 +260,11 @@ private Void receiver() { try { - Thread.currentThread().setName("TCP receiver " + socket.getRemoteAddress()); - logger.log(Level.FINER, Thread.currentThread().getName() + " started"); + Thread.currentThread().setName("TCP receiver " + socket.getLocalSocketAddress()); + logger.log(Level.FINER, () -> Thread.currentThread().getName() + " started for " + socket.getRemoteSocketAddress()); logger.log(Level.FINER, "Native byte order " + receive_buffer.order()); receive_buffer.clear(); + final InputStream in = socket.getInputStream(); while (true) { // Read at least one complete message, @@ -268,7 +273,7 @@ private Void receiver() while (receive_buffer.position() < message_size) { receive_buffer = assertBufferSize(receive_buffer, message_size); - final int read = socket.read(receive_buffer); + final int read = in.read(receive_buffer.array(), receive_buffer.position(), receive_buffer.remaining()); if (read < 0) { logger.log(Level.FINER, () -> Thread.currentThread().getName() + ": socket closed"); @@ -276,6 +281,7 @@ private Void receiver() } if (read > 0) logger.log(Level.FINER, () -> Thread.currentThread().getName() + ": " + read + " bytes"); + receive_buffer.position(receive_buffer.position() + read); // and once we get the header, it will tell // us how large the message actually is message_size = PVAHeader.checkMessageAndGetSize(receive_buffer, client_mode); @@ -558,7 +564,7 @@ public void close(final boolean wait) { running = false; socket.close(); - if (wait) + if (wait && receive_thread != null) receive_thread.get(5, TimeUnit.SECONDS); } catch (Exception ex) @@ -575,7 +581,7 @@ public String toString() buf.append("TCPHandler"); try { - final SocketAddress server = socket.getRemoteAddress(); + final SocketAddress server = socket.getRemoteSocketAddress(); buf.append(" ").append(server); } catch (Exception ex) diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java index b51ffbea2f..64f2ee6206 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java @@ -22,7 +22,8 @@ public static void encodeBitSet(final BitSet bits, final ByteBuffer buffer) { final byte[] bytes = bits.toByteArray(); PVASize.encodeSize(bytes.length, buffer); - buffer.put(bits.toByteArray()); + byte[] b = bits.toByteArray(); + buffer.put(b); } /** @param buffer Source buffer diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java index 73028283ea..d01d2b2e9d 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java @@ -131,6 +131,7 @@ public String getType() protected void format(final int level, final StringBuilder buffer) { formatType(level, buffer); + buffer.append(" ["); final String[] safe = value; if (safe == null) buffer.append("null"); diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java index 73dbd5c574..88bb0ac667 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java @@ -78,10 +78,11 @@ static PVAStructure decodeType(final PVATypeRegistry types, final String name, f */ private final List elements; + /** @param name Name of the structure (may be "") * @param struct_name Type name of the structure (may be "") - * @param elements Elements, must be named - * @throws IllegalArgumentException when an element is not named + * @param elements Elements, name may be "", but not null + * @throws IllegalArgumentException if an element name is null */ public PVAStructure(final String name, final String struct_name, final PVAData... elements) { @@ -90,16 +91,16 @@ public PVAStructure(final String name, final String struct_name, final PVAData.. /** @param name Name of the structure (may be "") * @param struct_name Type name of the structure (may be "") - * @param elements Elements, must be named - * @throws IllegalArgumentException when an element is not named + * @param elements Elements, name may be "", but not null + * @throws IllegalArgumentException if an element name is null */ public PVAStructure(final String name, final String struct_name, final List elements) { super(name); this.struct_name = struct_name; for (PVAData element : elements) - if (element.getName().isEmpty()) - throw new IllegalArgumentException("Structure with unnamed element"); + if (element.getName() == null) + throw new IllegalArgumentException("Structure with null element name"); this.elements = Collections.unmodifiableList(elements); } diff --git a/core/pva/src/main/java/org/epics/pva/data/nt/PVAURI.java b/core/pva/src/main/java/org/epics/pva/data/nt/PVAURI.java index b718df4bbc..00c9edd2e5 100644 --- a/core/pva/src/main/java/org/epics/pva/data/nt/PVAURI.java +++ b/core/pva/src/main/java/org/epics/pva/data/nt/PVAURI.java @@ -32,21 +32,18 @@ * NTURI is the EPICS V4 Normative Type that describes a Uniform Resource Identifier (URI) bib:uri. *

    * NTURI := - *
      + * * structure - *
    • + * *
        *
      • string scheme
      • *
      • string authority : opt
      • *
      • string path
      • *
      • structure query : opt - *
          - *
        • {string | double | int }0+ - * * { }0+
        • *
        - *
      • - *
      - *
    • + *
        + *
      • {@literal{string | double | int }0+}
      • + *
      • {@literal{ }0+}
      • *
      *

      * Zero or more pvData Fields whose type are not defined until runtime, may be added to an NTURI @@ -55,6 +52,7 @@ *

      */ public class PVAURI extends PVAStructure { + /** structure name */ public static final String STRUCT_NAME = "epics:nt/NTURI:1.0"; private static final String SCHEME_NAME = "scheme"; @@ -95,7 +93,7 @@ public PVAURI(String name, String path) { * Set all non-optional parameters. * * @param name Name of PV - * @param scheme The scheme name must be given. For the pva scheme, the scheme name is “pva”. The pva scheme + * @param scheme The scheme name must be given. For the pva scheme, the scheme name is 'pva'. The pva scheme * implies but does not require use of the pvAccess protocol. * @param path The path gives the channel from which data is being requested. */ @@ -107,7 +105,7 @@ public PVAURI(String name, String scheme, String path) { * Set all the parameters of the NTURI * * @param name Name of PV - * @param scheme The scheme name must be given. For the pva scheme, the scheme name is “pva”. The pva scheme + * @param scheme The scheme name must be given. For the pva scheme, the scheme name is 'pva'. The pva scheme * implies but does not require use of the pvAccess protocol. * @param authority If given, then the IP name or address of an EPICS network pvAccess or channel access server. * @param path The path gives the channel from which data is being requested. @@ -141,7 +139,7 @@ public String getPath() { /** * Gets the query in a map format * - * @return Returns the query in a Map, returns empty + * @return Returns the query in a {@literal Map}, returns empty * an empty map if the query is null. * @throws NotValueException If a query in the queries structure * does not implement {@link PVAValue} diff --git a/core/pva/src/main/java/org/epics/pva/server/PVASearchMonitorMain.java b/core/pva/src/main/java/org/epics/pva/server/PVASearchMonitorMain.java index 772df9cf13..230b1def61 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVASearchMonitorMain.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVASearchMonitorMain.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Oak Ridge National Laboratory. + * Copyright (c) 2022-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.LogManager; @@ -50,6 +51,7 @@ private static class SearchInfo implements Comparable this.name = name; } + /** Sort by search count */ @Override public int compareTo(final SearchInfo other) { @@ -93,7 +95,7 @@ public static void main(String[] args) throws Exception LogManager.getLogManager().readConfiguration(PVASettings.class.getResourceAsStream("/pva_logging.properties")); setLogLevel(Level.WARNING); long update_period = 10; - boolean once = false; + final AtomicBoolean once = new AtomicBoolean(); for (int i=0; i { - // Quit when receiving search for name "QUIT" - if (name.equals("QUIT")) + // In "continuing" mode, quit when receiving search for name "QUIT" + if (!once.get() && name.equals("QUIT")) done.countDown(); final SearchInfo search = searches.computeIfAbsent(name, n -> new SearchInfo(name)); @@ -155,7 +160,7 @@ else if (arg.startsWith("-v") && (i+1) < args.length) ( final PVAServer server = new PVAServer(search_handler) ) { System.out.println("Monitoring search requests for " + update_period + " seconds..."); - if (! once) + if (! once.get()) System.out.println("Run 'pvget QUIT' to stop"); while (! done.await(update_period, TimeUnit.SECONDS)) { @@ -172,7 +177,7 @@ else if (arg.startsWith("-v") && (i+1) < args.length) info.client.toString(), now.getEpochSecond() - info.last.getEpochSecond()); }); - if (once) + if (once.get()) break; } } diff --git a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java index 883c7b1864..cd10f3bedf 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,6 +16,7 @@ import java.util.function.Consumer; import java.util.logging.Level; +import org.epics.pva.PVASettings; import org.epics.pva.data.PVAStructure; /** PVA Server @@ -54,7 +55,7 @@ public class PVAServer implements AutoCloseable /** TCP connection listener, creates {@link ServerTCPHandler} for each connecting client */ private final ServerTCPListener tcp; - /** Optional handler for seatches that's checked first */ + /** Optional searche handler 'hook' */ private final SearchHandler custom_search_handler; /** Handlers for the TCP connections clients established to this server */ @@ -88,10 +89,12 @@ public PVAServer(final SearchHandler search_handler) throws Exception tcp = new ServerTCPListener(this); } - /** @return TCP address and port where server is accepting clients */ - public InetSocketAddress getTCPAddress() + /** @param tls Request TLS or plain TCP address? + * @return TCP address and port where server is accepting clients + */ + public InetSocketAddress getTCPAddress(final boolean tls) { - return tcp.getResponseAddress(); + return tcp.getResponseAddress(tls); } /** Create a read-only PV which serves data to clients @@ -167,21 +170,28 @@ ServerPV getPV(final int sid) * @param cid Client's channel ID * @param name PV Name * @param client Client's UDP reply address + * @param tls_requested Does client support tls? * @param tcp_connection Optional TCP connection for search received via TCP, else null * @return */ boolean handleSearchRequest(final int seq, final int cid, final String name, final InetSocketAddress client, + final boolean tls_requested, final ServerTCPHandler tcp_connection) { + // Both client and server must support TLS + final boolean tls = tls_requested && !PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank(); + if (tls_requested && !tls) + logger.log(Level.WARNING, "PVA Client " + client + " searches for '" + name + "' with TLS, but EPICS_PVAS_TLS_KEYCHAIN is not configured"); + final Consumer send_search_reply = server_address -> { // If received via TCP, reply via same connection. if (tcp_connection != null) - tcp_connection.submitSearchReply(guid, seq, cid, server_address); + tcp_connection.submitSearchReply(guid, seq, cid, server_address, tls); else // Otherwise reply via UDP to the given address. - POOL.execute(() -> udp.sendSearchReply(guid, seq, cid, server_address, client)); + POOL.execute(() -> udp.sendSearchReply(guid, seq, cid, server_address, tls, client)); }; // Does custom handler consume the search request? @@ -192,9 +202,9 @@ boolean handleSearchRequest(final int seq, final int cid, final String name, if (cid < 0) { // 'List servers' search, no specific name if (tcp_connection != null) - tcp_connection.submitSearchReply(guid, seq, -1, USE_THIS_TCP_CONNECTION); + tcp_connection.submitSearchReply(guid, seq, -1, USE_THIS_TCP_CONNECTION, tls); else - POOL.execute(() -> udp.sendSearchReply(guid, 0, -1, getTCPAddress(), client)); + POOL.execute(() -> udp.sendSearchReply(guid, 0, -1, getTCPAddress(tls), tls, client)); return true; } else @@ -211,7 +221,7 @@ boolean handleSearchRequest(final int seq, final int cid, final String name, if (tcp_connection != null) send_search_reply.accept(USE_THIS_TCP_CONNECTION); else - send_search_reply.accept(getTCPAddress()); + send_search_reply.accept(getTCPAddress(tls)); return true; } else diff --git a/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java b/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java index 9ed1694e61..070c095c8d 100644 --- a/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021-2022 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -35,6 +35,6 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t if (search.channels != null) for (SearchRequest.Channel channel : search.channels) tcp.getServer().handleSearchRequest(search.seq, channel.getCID(), channel.getName(), - search.client, tcp); + search.client, search.tls, tcp); } } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java b/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java index 979be131ed..1f96b82026 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerAuth.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import org.epics.pva.common.PVAAuth; +import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; import org.epics.pva.data.PVAData; import org.epics.pva.data.PVAString; import org.epics.pva.data.PVAStructure; @@ -33,14 +34,35 @@ abstract class ServerAuth /** Decode authentication and then determine authorizations * @param tcp TCP Handler * @param buffer Buffer, positioned on String auth, optional detail + * @param tls_info {@link TLSHandshakeInfo}, may be null * @return ClientAuthorization * @throws Exception on error */ - public static ServerAuth decode(final ServerTCPHandler tcp, final ByteBuffer buffer) throws Exception + public static ServerAuth decode(final ServerTCPHandler tcp, final ByteBuffer buffer, TLSHandshakeInfo tls_info) throws Exception { final String auth = PVAString.decodeString(buffer); + + if (buffer.remaining() < 1) + throw new Exception("Missing authentication detail for '" + auth + "'"); + + final PVATypeRegistry types = tcp.getClientTypes(); + final PVAData type = types.decodeType("", buffer); + PVAStructure info = null; + if (type instanceof PVAStructure) + { + info = (PVAStructure) type; + info.decode(types, buffer); + } + if (PVAAuth.CA.equals(auth)) - return new CAServerAuth(tcp, buffer); + return new CAServerAuth(info); + + if (info != null) + throw new Exception("Expected no authentication detail for '" + auth + "' but got " + info); + + if (PVAAuth.X509.equals(auth)) + return new X509ServerAuth(tls_info); + return Anonymous; } @@ -63,20 +85,8 @@ private static class CAServerAuth extends ServerAuth { private String user, host; - public CAServerAuth(final ServerTCPHandler tcp, final ByteBuffer buffer) throws Exception + public CAServerAuth(final PVAStructure info) throws Exception { - final PVATypeRegistry types = tcp.getClientTypes(); - - if (buffer.remaining() < 1) - throw new Exception("Missing 'ca' authentication info"); - - final PVAData data = types.decodeType("", buffer); - if (! (data instanceof PVAStructure)) - throw new Exception("Expected structure for 'ca' authentication info, got " + data); - - final PVAStructure info = (PVAStructure) data; - info.decode(types, buffer); - PVAString element = info.get("user"); if (element == null) throw new Exception("Missing 'ca' authentication 'user', got " + info); @@ -91,9 +101,7 @@ public CAServerAuth(final ServerTCPHandler tcp, final ByteBuffer buffer) throws @Override public boolean hasWriteAccess(final String channel) { - // TODO Implement access security based on `acf` type config file - // if (! channel.contains("demo")) - // return false; + // TODO Implement access security based on `acf` type config file, checking channel for user and host return true; } @@ -103,4 +111,31 @@ public String toString() return "ca(" + user + "@" + host + ")"; } } + + + private static class X509ServerAuth extends ServerAuth + { + private String user, host; + + public X509ServerAuth(final TLSHandshakeInfo tls_info) throws Exception + { + if (tls_info == null) + throw new Exception("x509 authentication requires principal name from TLS certificate"); + user = tls_info.name; + host = tls_info.hostname; + } + + @Override + public boolean hasWriteAccess(final String channel) + { + // TODO Implement access security based on `acf` type config file, checking channel for user and host + return true; + } + + @Override + public String toString() + { + return "x509(" + user + "@" + host + ")"; + } + } } diff --git a/core/pva/src/test/java/org/epics/pva/server/ServerDemo.java b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java similarity index 100% rename from core/pva/src/test/java/org/epics/pva/server/ServerDemo.java rename to core/pva/src/main/java/org/epics/pva/server/ServerDemo.java diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java index 028920b997..c6379f3b9c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,14 +10,17 @@ import static org.epics.pva.PVASettings.logger; import java.net.InetSocketAddress; +import java.net.Socket; import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; +import java.util.Objects; import java.util.logging.Level; import org.epics.pva.common.CommandHandlers; +import org.epics.pva.common.PVAAuth; import org.epics.pva.common.PVAHeader; import org.epics.pva.common.RequestEncoder; import org.epics.pva.common.SearchResponse; +import org.epics.pva.common.SecureSockets.TLSHandshakeInfo; import org.epics.pva.common.TCPHandler; import org.epics.pva.data.PVASize; import org.epics.pva.data.PVAString; @@ -47,17 +50,24 @@ class ServerTCPHandler extends TCPHandler /** Server that holds all the PVs */ private final PVAServer server; + /** Info from TLS socket handshake or null */ + private final TLSHandshakeInfo tls_info; + /** Types declared by client at other end of this TCP connection */ private final PVATypeRegistry client_types = new PVATypeRegistry(); /** Auth info, e.g. client user info and his/her permissions */ private volatile ServerAuth auth = ServerAuth.Anonymous; - public ServerTCPHandler(final PVAServer server, final SocketChannel client) throws Exception + + public ServerTCPHandler(final PVAServer server, final Socket client, final TLSHandshakeInfo tls_info) throws Exception { super(client, false); - this.server = server; + this.server = Objects.requireNonNull(server); + this.tls_info = tls_info; + server.register(this); + startReceiver(); startSender(); // Initialize TCP connection by setting byte order.. @@ -78,9 +88,10 @@ public ServerTCPHandler(final PVAServer server, final SocketChannel client) thro submit((version, buffer) -> { logger.log(Level.FINE, () -> "Sending Validation Request"); - PVAHeader.encodeMessageHeader(buffer, - PVAHeader.FLAG_SERVER, - PVAHeader.CMD_CONNECTION_VALIDATION, 4+2+1+PVAString.getEncodedSize("anonymous") + PVAString.getEncodedSize("ca")); + + final int size_offset = buffer.position() + PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE; + PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_CONNECTION_VALIDATION, 4+2+1); + final int payload_start = buffer.position(); // int serverReceiveBufferSize; buffer.putInt(receive_buffer.capacity()); @@ -88,12 +99,17 @@ public ServerTCPHandler(final PVAServer server, final SocketChannel client) thro // short serverIntrospectionRegistryMaxSize; buffer.putShort(Short.MAX_VALUE); - // string[] authNZ; - PVASize.encodeSize(1, buffer); + // If client identified itself on secure connection, server supports "x509" + boolean support_x509 = this.tls_info != null; - // TODO ServerAuthentication - PVAString.encodeString("ca", buffer); - PVAString.encodeString("anonymous", buffer); + // string[] authNZ; listing most secure at end + PVASize.encodeSize(support_x509 ? 3 : 2, buffer); + PVAString.encodeString(PVAAuth.ANONYMOUS, buffer); + PVAString.encodeString(PVAAuth.CA, buffer); + if (support_x509) + PVAString.encodeString(PVAAuth.X509, buffer); + + buffer.putInt(size_offset, buffer.position() - payload_start); }); } @@ -102,6 +118,11 @@ PVAServer getServer() return server; } + TLSHandshakeInfo getTLSHandshakeInfo() + { + return tls_info; + } + PVATypeRegistry getClientTypes() { return client_types; @@ -141,12 +162,19 @@ protected void handleApplicationMessage(final byte command, final ByteBuffer buf super.handleApplicationMessage(command, buffer); } - void submitSearchReply(final Guid guid, final int seq, final int cid, final InetSocketAddress server_address) + /** Send a "channel found" reply to a client's search + * @param guid This server's GUID + * @param seq Client search request sequence number + * @param cid Client's channel ID or -1 + * @param server_address TCP address where client can connect to server + * @param tls Should client use tls? + */ + void submitSearchReply(final Guid guid, final int seq, final int cid, final InetSocketAddress server_address, final boolean tls) { final RequestEncoder encoder = (version, buffer) -> { - logger.log(Level.FINER, "Sending TCP search reply"); - SearchResponse.encode(guid, seq, cid, server_address.getAddress(), server_address.getPort(), buffer); + logger.log(Level.FINER, () -> "Sending " + (tls ? "TLS" : "TCP") + " search reply"); + SearchResponse.encode(guid, seq, cid, server_address.getAddress(), server_address.getPort(), tls, buffer); }; submit(encoder); } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java index a9092564fd..aeee63d1ff 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,15 +12,20 @@ import java.net.BindException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.net.Socket; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; +import java.net.SocketTimeoutException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; + import org.epics.pva.PVASettings; +import org.epics.pva.common.SecureSockets; +import org.epics.pva.common.SecureSockets.TLSHandshakeInfo;; /** Listen to TCP connections * @@ -41,11 +46,11 @@ class ServerTCPListener private final PVAServer server; - /** TCP channel on which we listen for connections */ - private final ServerSocketChannel server_socket; + /** Open TCP socket on which we listen for clients */ + private final ServerSocket tcp_server_socket; - /** Server's TCP address on which clients can connect */ - private final InetSocketAddress local_address; + /** Secure TCP socket on which we listen for clients */ + private final ServerSocket tls_server_socket; private volatile boolean running = true; private volatile Thread listen_thread; @@ -54,21 +59,40 @@ public ServerTCPListener(final PVAServer server) throws Exception { this.server = server; - server_socket = createSocket(); + // Is TLS configured? + final boolean tls = !PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank(); - local_address = (InetSocketAddress) server_socket.getLocalAddress(); + // Support open TCP, maybe also TLS + tcp_server_socket = createSocket(PVASettings.EPICS_PVA_SERVER_PORT, false); + InetSocketAddress local_address = (InetSocketAddress) tcp_server_socket.getLocalSocketAddress(); logger.log(Level.CONFIG, "Listening on TCP " + local_address); + String name = "TCP-listener " + local_address.getAddress() + ":" + local_address.getPort(); + + if (tls) + { + tls_server_socket = createSocket(PVASettings.EPICS_PVAS_TLS_PORT, true); + local_address = (InetSocketAddress) tls_server_socket.getLocalSocketAddress(); + logger.log(Level.CONFIG, "Listening on TLS " + local_address); + name += ", TLS:" + local_address.getPort(); + } + else + tls_server_socket = null; // Start accepting connections - listen_thread = new Thread(this::listen, "TCP-listener " + local_address.getAddress() + ":" + local_address.getPort()); + listen_thread = new Thread(this::listen, name); listen_thread.setDaemon(true); listen_thread.start(); } - /** @return Server's TCP address on which clients can connect */ - public InetSocketAddress getResponseAddress() + /** @param tls Request TLS or plain TCP address? + * @return Server's TCP address on which clients can connect + */ + public InetSocketAddress getResponseAddress(final boolean tls) { - return local_address; + if (tls && tls_server_socket != null) + return (InetSocketAddress) tls_server_socket.getLocalSocketAddress(); + + return (InetSocketAddress) tcp_server_socket.getLocalSocketAddress(); } // How to check if the desired TCP server port is already in use? @@ -120,22 +144,24 @@ private static boolean checkForIPv4Server(final int desired_port) /** Create server's TCP socket * - * @return Socket bound to EPICS_PVA_SERVER_PORT or unused port + * @param port Preferred TCP port + * @param tls Use TLS? + * @return Socket bound to preferred port or unused port * @throws Exception on error */ - private static ServerSocketChannel createSocket() throws Exception + private static ServerSocket createSocket(final int port, final boolean tls) throws Exception { - if (checkForIPv4Server(PVASettings.EPICS_PVA_SERVER_PORT)) - logger.log(Level.FINE, "Found existing IPv4 server on port " + PVASettings.EPICS_PVA_SERVER_PORT); + if (checkForIPv4Server(port)) + logger.log(Level.FINE, "Found existing IPv4 server on port " + port); else { // Try to bind to desired port try { - return createBoundSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT)); + return SecureSockets.createServerSocket(new InetSocketAddress(port), tls); } catch (BindException ex) { - logger.log(Level.INFO, "TCP port " + PVASettings.EPICS_PVA_SERVER_PORT + " already in use, switching to automatically assigned port"); + logger.log(Level.INFO, (tls ? "TLS" : "TCP") + " port " + port + " already in use, switching to automatically assigned port"); } } @@ -143,7 +169,7 @@ private static ServerSocketChannel createSocket() throws Exception final InetSocketAddress any = new InetSocketAddress(0); try { - return createBoundSocket(any); + return SecureSockets.createServerSocket(any, tls); } catch (Exception e) { @@ -151,37 +177,67 @@ private static ServerSocketChannel createSocket() throws Exception } } - /** Try to create socket that's bound to an address - * @param addr Desired address - * @return {@link ServerSocketChannel} - * @throws Exception on error - */ - private static ServerSocketChannel createBoundSocket(final InetSocketAddress addr) throws Exception - { - ServerSocketChannel socket = ServerSocketChannel.open(); - try - { - socket.configureBlocking(true); - socket.socket().setReuseAddress(true); - socket.bind(addr); - } - catch (Exception ex) - { - socket.close(); - throw ex; - } - return socket; - } - private void listen() { try { logger.log(Level.FINER, Thread.currentThread().getName() + " started"); + + // Assume that open TCP, secure TLS, or both are configured. + // Need to accept clients on any. + // ServerSocketChannel allows using the Selector, + // but there is no SSLServerSocketChannel. + // SSLContext only creates (SSL)ServerSocket, and no easy way to + // turn (SSL)ServerSocket into (SSL)ServerSocketChannel + // https://stackoverflow.com/questions/37763038/is-there-any-way-to-use-sslcontext-with-serversocketchannel + // + // As a workaround we configure a (short) timeout on the sockets + // and then take turns 'accept'ing from them + if (tcp_server_socket != null) + tcp_server_socket.setSoTimeout(10); + if (tls_server_socket != null) + tls_server_socket.setSoTimeout(10); while (running) { - final SocketChannel client = server_socket.accept(); - new ServerTCPHandler(server, client); + if (tcp_server_socket != null) + { + try + { // Check TCP + final Socket client = tcp_server_socket.accept(); + logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TCP client " + client.getRemoteSocketAddress()); + new ServerTCPHandler(server, client, null); + } + catch (SocketTimeoutException timeout) + { // Ignore + } + } + if (tls_server_socket != null) + { + try + { // Check TLS + final Socket client = tls_server_socket.accept(); + TLSHandshakeInfo tls_info = null; + if (client instanceof SSLSocket) + { + logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted TLS client " + client.getRemoteSocketAddress()); + try + { + tls_info = TLSHandshakeInfo.fromSocket((SSLSocket) client); + } + catch (SSLHandshakeException ssl) + { + logger.log(Level.FINE, "SSL Handshake error for " + client.getRemoteSocketAddress(), ssl); + continue; + } + } + else + logger.log(Level.WARNING, () -> Thread.currentThread().getName() + " expected TLS client " + client.getRemoteSocketAddress() + " but did not get SSLSocket"); + new ServerTCPHandler(server, client, tls_info); + } + catch (SocketTimeoutException timeout) + { // Ignore + } + } } } catch (Exception ex) @@ -198,7 +254,10 @@ public void close() // Close sockets, wait a little for threads to exit try { - server_socket.close(); + if (tcp_server_socket != null) + tcp_server_socket.close(); + if (tls_server_socket != null) + tls_server_socket.close(); if (listen_thread != null) listen_thread.join(5000); diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java index 1c39cec919..08b887ce4c 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -79,8 +79,10 @@ public ServerUDPHandler(final PVAServer server) throws Exception if (info.isIPv6()) { + if (!PVASettings.EPICS_PVA_ENABLE_IPV6) + throw new Exception("Must have IPv6 enabled for IPv6 address!"); if (udp6 != null) - throw new Exception("EPICS_PVAS_INTF_ADDR_LIST has more than one IPv4 address"); + throw new Exception("EPICS_PVAS_INTF_ADDR_LIST has more than one IPv6 address"); udp6 = Network.createUDP(StandardProtocolFamily.INET6, info.getAddress().getAddress(), PVASettings.EPICS_PVAS_BROADCAST_PORT); logger.log(Level.FINE, "Awaiting searches and sending beacons on UDP " + info); } @@ -183,9 +185,9 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, { if (search.reply_required) { // pvlist request - final boolean handled = server.handleSearchRequest(0, -1, null, search.client, null); + final boolean handled = server.handleSearchRequest(0, -1, null, search.client, search.tls, null); if (! handled && search.unicast) - PVAServer.POOL.submit(() -> forwardSearchRequest(0, null, search.client)); + PVAServer.POOL.submit(() -> forwardSearchRequest(0, null, search.client, search.tls)); } } else @@ -193,7 +195,7 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, List forward = null; for (SearchRequest.Channel channel : search.channels) { - final boolean handled = server.handleSearchRequest(search.seq, channel.getCID(), channel.getName(), search.client, null); + final boolean handled = server.handleSearchRequest(search.seq, channel.getCID(), channel.getName(), search.client, search.tls, null); if (! handled && search.unicast) { if (forward == null) @@ -205,7 +207,7 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, if (forward != null) { final List to_forward = forward; - PVAServer.POOL.submit(() -> forwardSearchRequest(search.seq, to_forward, search.client)); + PVAServer.POOL.submit(() -> forwardSearchRequest(search.seq, to_forward, search.client, search.tls)); } } @@ -223,8 +225,9 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, * @param seq Search sequence or 0 * @param channels Channel CIDs and names or null for 'list' * @param address Client's address and port + * @param tls Use TLS or plain TCP? */ - private void forwardSearchRequest(final int seq, final Collection channels, final InetSocketAddress address) + private void forwardSearchRequest(final int seq, final Collection channels, final InetSocketAddress address, final boolean tls) { // TODO Remove the local IPv4 multicast re-send from the protocol, just use multicast from the start as with IPv6 if (local_multicast == null) @@ -232,7 +235,7 @@ private void forwardSearchRequest(final int seq, final Collection "Forward search to " + local_multicast + "\n" + Hexdump.toHexdump(send_buffer)); try @@ -250,15 +253,16 @@ private void forwardSearchRequest(final int seq, final Collection "Sending UDP search reply to " + client + "\n" + Hexdump.toHexdump(send_buffer)); diff --git a/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java b/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java index 165d7d4158..cacad6bb97 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ValidationHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -39,7 +39,7 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t final int client_registry_size = Short.toUnsignedInt(buffer.getShort()); final short quos = buffer.getShort(); - final ServerAuth auth = ServerAuth.decode(tcp, buffer); + final ServerAuth auth = ServerAuth.decode(tcp, buffer, tcp.getTLSHandshakeInfo()); logger.log(Level.FINE, "Connection validated, auth '" + auth + "'"); tcp.setAuth(auth); sendConnectionValidated(tcp); diff --git a/core/pva/src/test/java/org/epics/pva/combined/ServerClientTest.java b/core/pva/src/test/java/org/epics/pva/combined/ServerClientTest.java index b3e55d2ff0..4feeb994d1 100644 --- a/core/pva/src/test/java/org/epics/pva/combined/ServerClientTest.java +++ b/core/pva/src/test/java/org/epics/pva/combined/ServerClientTest.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import org.epics.pva.PVASettings; import org.epics.pva.client.MonitorListener; import org.epics.pva.client.PVAChannel; import org.epics.pva.client.PVAClient; @@ -77,6 +78,8 @@ public class ServerClientTest { private static final long afterLastEventTimeOut = 1000; private static PVAServer testServer() { + if (!PVASettings.EPICS_PVA_ENABLE_IPV6) + PVASettings.EPICS_PVAS_INTF_ADDR_LIST = "0.0.0.0"; try { return new PVAServer(); } catch (Exception e) { diff --git a/core/pva/src/test/java/org/epics/pva/common/NetworkTest.java b/core/pva/src/test/java/org/epics/pva/common/NetworkTest.java index c3605504b2..62fcabd11b 100644 --- a/core/pva/src/test/java/org/epics/pva/common/NetworkTest.java +++ b/core/pva/src/test/java/org/epics/pva/common/NetworkTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021-2022 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,8 +7,10 @@ ******************************************************************************/ package org.epics.pva.common; -import org.epics.pva.PVASettings; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.Inet4Address; import java.net.Inet6Address; @@ -17,10 +19,8 @@ import java.util.List; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.epics.pva.PVASettings; +import org.junit.jupiter.api.Test; /** Unit test of Network helper * @author Kay Kasemir @@ -30,7 +30,8 @@ public class NetworkTest { // If running on a host that does not support IPv6, // ignore the checks that require a local "::1" IPv6 address - private static final boolean ignore_local_ipv6 = Boolean.parseBoolean(System.getProperty("ignore_local_ipv6")); + private static final boolean ignore_local_ipv6 = Boolean.parseBoolean(System.getProperty("ignore_local_ipv6")) + || !PVASettings.EPICS_PVA_ENABLE_IPV6; @Test public void testBroadcastAddresses() @@ -72,6 +73,12 @@ public void testIPv4() throws Exception System.out.println(addr); assertEquals("127.0.0.255", addr.getAddress().getHostString()); assertTrue(addr.isBroadcast()); + assertFalse(addr.isTLS()); + + addr = Network.parseAddress("pvas://127.0.0.1", PVASettings.EPICS_PVA_SERVER_PORT); + System.out.println(addr); + assertTrue(addr.isTLS()); + assertEquals(PVASettings.EPICS_PVAS_TLS_PORT, addr.getAddress().getPort()); } @Test diff --git a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java index 37ac5d02b2..64892be4b0 100644 --- a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java +++ b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java @@ -58,6 +58,13 @@ public void testBitSet() buffer.flip(); System.out.println(Hexdump.toHexdump(buffer)); + bits.clear(); + bits.set(14); + buffer.clear(); + PVABitSet.encodeBitSet(bits, buffer); + buffer.flip(); + System.out.println(Hexdump.toHexdump(buffer)); + final BitSet copy = PVABitSet.decodeBitSet(buffer); assertThat(copy, equalTo(bits)); } diff --git a/core/pva/src/test/java/org/epics/pva/data/StructureTest.java b/core/pva/src/test/java/org/epics/pva/data/StructureTest.java index 8254f96d3d..149ff3ab1b 100644 --- a/core/pva/src/test/java/org/epics/pva/data/StructureTest.java +++ b/core/pva/src/test/java/org/epics/pva/data/StructureTest.java @@ -127,7 +127,10 @@ public void testError() // But _elements_ of the structure need names to address them try { + // This is OK, i.e. element names may be empty new PVAStructure("", "", new PVADouble("")); + // But this must fail, element names must be non-null + new PVAStructure("", "", new PVADouble(null)); fail("Structure elements must be named"); } catch (Exception ex) diff --git a/core/pva/src/test/java/org/epics/pva/server/SearchMonitorDemo.java b/core/pva/src/test/java/org/epics/pva/server/SearchMonitorDemo.java index dd71b3fadb..869f51bd2b 100644 --- a/core/pva/src/test/java/org/epics/pva/server/SearchMonitorDemo.java +++ b/core/pva/src/test/java/org/epics/pva/server/SearchMonitorDemo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020-2022 Oak Ridge National Laboratory. + * Copyright (c) 2020-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -116,7 +116,7 @@ public static void main(String[] args) throws Exception { System.out.println("For UDP search, run 'pvget' or 'pvxget' with"); System.out.println("EPICS_PVA_BROADCAST_PORT=" + PVASettings.EPICS_PVAS_BROADCAST_PORT); - System.out.println("For TCP search, set EPICS_PVA_NAME_SERVERS = " + server.getTCPAddress()); + System.out.println("For TCP search, set EPICS_PVA_NAME_SERVERS = " + server.getTCPAddress(false)); System.out.println("or other IP address of this host and same port."); System.out.println("Run 'pvget QUIT' to stop"); done.await(); diff --git a/core/pva/src/test/java/org/epics/pva/server/TableDemo.java b/core/pva/src/test/java/org/epics/pva/server/TableDemo.java index 95badcb168..42aba1bdb6 100644 --- a/core/pva/src/test/java/org/epics/pva/server/TableDemo.java +++ b/core/pva/src/test/java/org/epics/pva/server/TableDemo.java @@ -7,6 +7,7 @@ ******************************************************************************/ package org.epics.pva.server; +import java.util.BitSet; import java.util.concurrent.TimeUnit; import java.util.logging.LogManager; @@ -37,7 +38,7 @@ public static void main(String[] args) throws Exception final PVAStringArray labels = new PVAStringArray("labels", "sec", "nano", "M1X", "M1Y"); final PVAIntArray sec = new PVAIntArray("secondsPastEpoch", true, 1, 2); final PVAIntArray nano = new PVAIntArray("nanoseconds", true, 3, 4); - final PVADoubleArray m1x = new PVADoubleArray("m1x", 3.13, 3.15); + final PVADoubleArray m1x = new PVADoubleArray("m1x", 2.13, 3.15); final PVADoubleArray m1y = new PVADoubleArray("m1y", 31.15, 32.14); final PVAStructure data = new PVAStructure("demo", "epics:nt/NTTable:1.0", labels, @@ -49,7 +50,12 @@ public static void main(String[] args) throws Exception time); // Create PVs - final ServerPV pv1 = server.createPV("demo", data); + final ServerPV pv1 = server.createPV("demo", data, new WriteEventHandler() { + @Override + public void handleWrite(ServerPV pv, BitSet changes, PVAStructure written) throws Exception { + System.out.println(); + } + }); TimeUnit.DAYS.sleep(1); } diff --git a/core/security/pom.xml b/core/security/pom.xml index f0d5e2ff00..4b463be19f 100644 --- a/core/security/pom.xml +++ b/core/security/pom.xml @@ -3,7 +3,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-security @@ -22,7 +22,7 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/security/src/main/java/org/phoebus/security/PhoebusSecurity.java b/core/security/src/main/java/org/phoebus/security/PhoebusSecurity.java index cba5d558f5..358907f916 100644 --- a/core/security/src/main/java/org/phoebus/security/PhoebusSecurity.java +++ b/core/security/src/main/java/org/phoebus/security/PhoebusSecurity.java @@ -16,7 +16,6 @@ /** Phoebus security logger and Preference settings * @author Kay Kasemir */ -@SuppressWarnings("nls") public class PhoebusSecurity { /** Shared logger */ @@ -25,6 +24,7 @@ public class PhoebusSecurity /** Preference setting */ @Preference public static String authorization_file; + /** Preference setting */ @Preference public static SecureStoreTarget secure_store_target; static diff --git a/core/security/src/main/java/org/phoebus/security/authorization/ServiceAuthenticationProvider.java b/core/security/src/main/java/org/phoebus/security/authorization/ServiceAuthenticationProvider.java index e0e06d0697..9e807e0c69 100644 --- a/core/security/src/main/java/org/phoebus/security/authorization/ServiceAuthenticationProvider.java +++ b/core/security/src/main/java/org/phoebus/security/authorization/ServiceAuthenticationProvider.java @@ -18,6 +18,8 @@ package org.phoebus.security.authorization; +import org.phoebus.security.tokens.AuthenticationScope; + /** * Implementations of this interface are used to announce support for * a log in procedure to a service. @@ -33,21 +35,21 @@ public interface ServiceAuthenticationProvider { /** * Signs out user from the service. - * @param token User name or other type of token (e.g. session cookie). + * @param token Username or other type of token (e.g. session cookie). */ void logout(String token); /** * The identity of the announced service. This must be unique between all implementations. - * * NOTE: This identity is used to create keys (aka aliases) * under which credentials are persisted in the * {@link org.phoebus.security.store.SecureStore}. Such keys are stored in * lower case in the key store that backs {@link org.phoebus.security.store.SecureStore}, and * is a behavior defined by the encryption scheme implementation. - * Consequently an identity like "UPPER" will be persisted as "upper", i.e. case insensitivity + * Consequently, an identity like "UPPER" will be persisted as "upper", i.e. case insensitivity * must be considered when defining an identity. * @return Service name */ - String getServiceName(); + AuthenticationScope getAuthenticationScope(); + } diff --git a/core/security/src/main/java/org/phoebus/security/store/MemoryBasedStore.java b/core/security/src/main/java/org/phoebus/security/store/MemoryBasedStore.java index e4dd92c5a2..4f2013f9d2 100644 --- a/core/security/src/main/java/org/phoebus/security/store/MemoryBasedStore.java +++ b/core/security/src/main/java/org/phoebus/security/store/MemoryBasedStore.java @@ -1,6 +1,5 @@ package org.phoebus.security.store; -import org.phoebus.security.tokens.ScopedAuthenticationToken; import java.util.ArrayList; import java.util.HashMap; @@ -14,10 +13,12 @@ public class MemoryBasedStore implements Store { private final Map store = new HashMap<>(); + /** Singleton */ private static MemoryBasedStore INSTANCE = new MemoryBasedStore(); private MemoryBasedStore() {} + /** @return instance */ public static MemoryBasedStore getInstance() { return INSTANCE; } diff --git a/core/security/src/main/java/org/phoebus/security/store/SecureStore.java b/core/security/src/main/java/org/phoebus/security/store/SecureStore.java index 157be03327..d70029174d 100644 --- a/core/security/src/main/java/org/phoebus/security/store/SecureStore.java +++ b/core/security/src/main/java/org/phoebus/security/store/SecureStore.java @@ -9,17 +9,19 @@ import java.util.ArrayList; import java.util.List; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; import org.phoebus.security.PhoebusSecurity; +import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; /** * Handles reading/writing username, passsword, and token data. Internally delegates to * a Store implementation that handles storage of that data. */ -@SuppressWarnings("nls") + public class SecureStore { @@ -27,7 +29,7 @@ public class SecureStore /** Tags */ public static final String USERNAME_TAG = "username", - PASSWORD_TAG = "password"; + PASSWORD_TAG = "password"; private static final Logger LOGGER = Logger.getLogger(SecureStore.class.getName()); @@ -35,7 +37,7 @@ public class SecureStore * Default constructor, self-initializes underlying store based on * security preferences. * - * @see {@link org.phoebus.security.PhoebusSecurity} + * @see org.phoebus.security.PhoebusSecurity * * @throws Exception if underlying store isn't configured, configured incorrectly. * See javadocs for underlying implementations for details. @@ -90,21 +92,20 @@ public void delete(String tag) throws Exception{ store.delete(tag); } - /** @param scope Scope identifier, will be converted to lower case, see {@link ScopedAuthenticationToken} + /** @param scope Scope identifier, its name will be converted to lower case, see {@link ScopedAuthenticationToken} * @return Token for that scope * @throws Exception on error */ - public ScopedAuthenticationToken getScopedAuthenticationToken(String scope) throws Exception{ + public ScopedAuthenticationToken getScopedAuthenticationToken(AuthenticationScope scope) throws Exception{ String username; String password; - if(scope == null || scope.trim().isEmpty()){ + if(scope == null || scope.getName().trim().isEmpty()){ username = get(USERNAME_TAG); password = get(PASSWORD_TAG); } else{ - scope = scope.toLowerCase(); - username = get(scope + "." + USERNAME_TAG); - password = get(scope + "." + PASSWORD_TAG); + username = get(scope.getName().toLowerCase() + "." + USERNAME_TAG); + password = get(scope.getName().toLowerCase() + "." + PASSWORD_TAG); } if(username == null || password == null){ return null; @@ -115,16 +116,17 @@ public ScopedAuthenticationToken getScopedAuthenticationToken(String scope) thro /** @param scope Scope identifier, will be converted to lower case, see {@link ScopedAuthenticationToken} * @throws Exception on error */ - public void deleteScopedAuthenticationToken(String scope) throws Exception{ + public void deleteScopedAuthenticationToken(AuthenticationScope scope) throws Exception{ LOGGER.log(Level.INFO, "Deleting authentication token for scope: " + scope); - if(scope == null || scope.trim().isEmpty()){ + if(scope == null || scope.getName().trim().isEmpty()){ delete(USERNAME_TAG); delete(PASSWORD_TAG); } else{ - delete(scope + "." + USERNAME_TAG); - delete(scope + "." + PASSWORD_TAG); + delete(scope.getName() + "." + USERNAME_TAG); + delete(scope.getName() + "." + PASSWORD_TAG); } + notifyChangeListeners(); } /** @throws Exception on error */ @@ -132,7 +134,7 @@ public void deleteAllScopedAuthenticationTokens() throws Exception{ List allScopedAuthenticationTokens = getAuthenticationTokens(); allScopedAuthenticationTokens.stream().forEach(token -> { try { - deleteScopedAuthenticationToken(token.getScope()); + deleteScopedAuthenticationToken(token.getAuthenticationScope()); } catch (Exception exception) { LOGGER.log(Level.WARNING, "Failed to delete scoped authentication token " + token.toString(), exception); } @@ -148,15 +150,16 @@ public void setScopedAuthentication(ScopedAuthenticationToken scopedAuthenticati if(username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()){ throw new RuntimeException("Username and password must both be non-null and non-empty"); } - String scope = scopedAuthenticationToken.getScope(); - if(scope == null || scope.trim().isEmpty()){ + AuthenticationScope scope = scopedAuthenticationToken.getAuthenticationScope(); + if(scope == null || scope.getName().trim().isEmpty()){ set(USERNAME_TAG, username); set(PASSWORD_TAG, password); } else{ - set(scope + "." + USERNAME_TAG, username); - set(scope + "." + PASSWORD_TAG, password); + set(scope.getName() + "." + USERNAME_TAG, username); + set(scope.getName() + "." + PASSWORD_TAG, password); } + notifyChangeListeners(); LOGGER.log(Level.INFO, "Storing scoped authentication token " + scopedAuthenticationToken); } @@ -193,7 +196,7 @@ private List matchEntries(List aliases) throw String[] tokens = alias.split("\\."); String username; String password; - String scope = null; + AuthenticationScope scope = null; // Non-scoped alias? if(tokens.length == 1 && USERNAME_TAG.equals(tokens[0])){ // It is assumed that the secure store can contain zero or one entries named "username" or "password". @@ -203,9 +206,9 @@ private List matchEntries(List aliases) throw password = get(PASSWORD_TAG); } else{ - scope = tokens[0]; - username = get(scope + "." + USERNAME_TAG); - password = get(scope + "." + PASSWORD_TAG); + scope = AuthenticationScope.fromString(tokens[0]); + username = get(scope.getName() + "." + USERNAME_TAG); + password = get(scope.getName() + "." + PASSWORD_TAG); } // Add only if password was found. if(password != null){ @@ -215,4 +218,14 @@ private List matchEntries(List aliases) throw return allScopedAuthenticationTokens; } + private void notifyChangeListeners(){ + ServiceLoader changeHandlers = ServiceLoader.load(SecureStoreChangeHandler.class); + changeHandlers.stream().forEach(c -> { + try { + c.get().secureStoreChanged(getAuthenticationTokens()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to notify secure store change handlers", e); + } + }); + } } diff --git a/core/security/src/main/java/org/phoebus/security/store/SecureStoreChangeHandler.java b/core/security/src/main/java/org/phoebus/security/store/SecureStoreChangeHandler.java new file mode 100644 index 0000000000..2ca402768c --- /dev/null +++ b/core/security/src/main/java/org/phoebus/security/store/SecureStoreChangeHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.security.store; + +import org.phoebus.security.tokens.ScopedAuthenticationToken; + +import java.util.List; + +/** + * Interface used to listen to changes in authentication status. Implementations can register over SPI + * to get notified when user logs in or logs out from an {@link org.phoebus.security.tokens.AuthenticationScope}. + */ +public interface SecureStoreChangeHandler { + + /** + * Callback method implemented by listeners. + * @param validTokens A list of valid {@link ScopedAuthenticationToken}s, i.e. a list of tokens associated + * with scopes where is authenticated. + */ + void secureStoreChanged(List validTokens); +} diff --git a/core/security/src/main/java/org/phoebus/security/store/Store.java b/core/security/src/main/java/org/phoebus/security/store/Store.java index a0632b0fad..a1d94764d3 100644 --- a/core/security/src/main/java/org/phoebus/security/store/Store.java +++ b/core/security/src/main/java/org/phoebus/security/store/Store.java @@ -2,11 +2,34 @@ import java.util.List; +/** + * Interface Store + * @param Key Type + * @param Value Type + */ public interface Store { - V get(K key) throws Exception; + /** + * @param key key + * @return Value value + * @throws Exception on error + */ + V get(K key) throws Exception; + /** + * @param key key + * @param value value + * @throws Exception on error + */ void set(K key, V value) throws Exception; + /** + * @return list of keys + * @throws Exception on error + */ List getKeys() throws Exception; + /** + * @param key key + * @throws Exception on error + */ void delete(K key) throws Exception; } diff --git a/core/security/src/main/java/org/phoebus/security/tokens/AuthenticationScope.java b/core/security/src/main/java/org/phoebus/security/tokens/AuthenticationScope.java new file mode 100644 index 0000000000..a91367ef8c --- /dev/null +++ b/core/security/src/main/java/org/phoebus/security/tokens/AuthenticationScope.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.security.tokens; + +import java.util.Arrays; + +/** + * Enum constants for authentication scopes used in dedicated use cases and applications. + * Restrictions on the name of an {@link AuthenticationScope} value: + *
        + *
      • Must match regexp [a-z-]*, i.e. lower case alphanumeric chars, except digits, plus hyphen (-)
      • + *
      • Must be unique among other enum names
      • + *
      + */ +public enum AuthenticationScope { + + LOGBOOK("logbook"), + SAVE_AND_RESTORE("save-and-restore"); + + private String name = null; + + private String supportedNamePattern = "[a-z-]*"; + + /** + * Internal use. + * @param name Valid name + * @throws IllegalArgumentException if the name does not match [a-z-]*. + */ + AuthenticationScope(String name) throws IllegalArgumentException{ + if(!name.matches(supportedNamePattern)){ + throw new IllegalArgumentException("Name " + name + " invalid"); + } + this.name = name; + } + + public String getName(){ + return name; + } + + public static AuthenticationScope fromString(String s){ + return Arrays.stream(values()).filter(a -> a.name.equals(s)).findFirst().orElse(null); + } +} diff --git a/core/security/src/main/java/org/phoebus/security/tokens/ScopedAuthenticationToken.java b/core/security/src/main/java/org/phoebus/security/tokens/ScopedAuthenticationToken.java index ab0364ce4e..c90b60ef2d 100644 --- a/core/security/src/main/java/org/phoebus/security/tokens/ScopedAuthenticationToken.java +++ b/core/security/src/main/java/org/phoebus/security/tokens/ScopedAuthenticationToken.java @@ -18,6 +18,8 @@ package org.phoebus.security.tokens; +import java.util.Objects; + /** * Extension of {@link SimpleAuthenticationToken}. * @@ -28,7 +30,7 @@ */ public class ScopedAuthenticationToken extends SimpleAuthenticationToken{ - private String scope; + private AuthenticationScope scope; /** @param username Username * @param password Password @@ -41,20 +43,20 @@ public ScopedAuthenticationToken(String username, String password){ * @param username Username * @param password Password */ - public ScopedAuthenticationToken(String scope, String username, String password){ + public ScopedAuthenticationToken(AuthenticationScope scope, String username, String password){ this(username, password); if(scope != null){ - if(scope.trim().isEmpty()){ + if(scope.getName().trim().isEmpty()){ this.scope = null; } else{ - this.scope = scope.toLowerCase(); + this.scope = scope; } } } /** @return Scope */ - public String getScope(){ + public AuthenticationScope getAuthenticationScope(){ return scope; } @@ -64,16 +66,16 @@ public boolean equals(Object other){ return false; } ScopedAuthenticationToken otherToken = (ScopedAuthenticationToken)other; - return (otherToken.getScope() + "." + otherToken.getUsername()).equals(getScope() + "." + getUsername()); + return (otherToken.getAuthenticationScope() + "." + otherToken.getUsername()).equals(getAuthenticationScope() + "." + getUsername()); } @Override public int hashCode(){ - return (getScope() + "." + getUsername()).hashCode(); + return Objects.hash(getAuthenticationScope(), getUsername()); } @Override public String toString(){ - return "Scope: " + scope + ", username: " + getUsername(); + return "Scope: " + (scope != null ? scope.getName() : "") + ", username: " + getUsername(); } } diff --git a/core/security/src/test/java/org/phoebus/security/authorization/tokens/ScopedAuthenticationTokenTest.java b/core/security/src/test/java/org/phoebus/security/authorization/tokens/ScopedAuthenticationTokenTest.java index b013fea080..c55a59de5c 100644 --- a/core/security/src/test/java/org/phoebus/security/authorization/tokens/ScopedAuthenticationTokenTest.java +++ b/core/security/src/test/java/org/phoebus/security/authorization/tokens/ScopedAuthenticationTokenTest.java @@ -19,6 +19,7 @@ package org.phoebus.security.authorization.tokens; import org.junit.jupiter.api.Test; +import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -31,27 +32,27 @@ public class ScopedAuthenticationTokenTest { public void testScopedAuthenticationToken() { ScopedAuthenticationToken scopedAuthenticationToken = new ScopedAuthenticationToken("username", "password"); assertEquals("username", scopedAuthenticationToken.getUsername()); - assertNull(scopedAuthenticationToken.getScope()); + assertNull(scopedAuthenticationToken.getAuthenticationScope()); - scopedAuthenticationToken = new ScopedAuthenticationToken(" ", "username", "password"); - assertNull(scopedAuthenticationToken.getScope()); + scopedAuthenticationToken = new ScopedAuthenticationToken(null, "username", "password"); + assertNull(scopedAuthenticationToken.getAuthenticationScope()); - scopedAuthenticationToken = new ScopedAuthenticationToken("", "username", "password"); - assertNull(scopedAuthenticationToken.getScope()); + scopedAuthenticationToken = new ScopedAuthenticationToken(null, "username", "password"); + assertNull(scopedAuthenticationToken.getAuthenticationScope()); scopedAuthenticationToken = new ScopedAuthenticationToken(null, "username", "password"); - assertNull(scopedAuthenticationToken.getScope()); + assertNull(scopedAuthenticationToken.getAuthenticationScope()); - scopedAuthenticationToken = new ScopedAuthenticationToken("scope", "username", "password"); - assertEquals("scope", scopedAuthenticationToken.getScope()); + scopedAuthenticationToken = new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username", "password"); + assertEquals(AuthenticationScope.LOGBOOK, scopedAuthenticationToken.getAuthenticationScope()); } @Test public void testEqualsAndHashCode() { ScopedAuthenticationToken scopedAuthenticationToken1 = new ScopedAuthenticationToken("username", "password"); ScopedAuthenticationToken scopedAuthenticationToken2 = new ScopedAuthenticationToken("username", "somethingelse"); - ScopedAuthenticationToken scopedAuthenticationToken3 = new ScopedAuthenticationToken("scope", "username", "somethingelse"); - ScopedAuthenticationToken scopedAuthenticationToken4 = new ScopedAuthenticationToken("scope", "username1", "somethingelse"); + ScopedAuthenticationToken scopedAuthenticationToken3 = new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username", "somethingelse"); + ScopedAuthenticationToken scopedAuthenticationToken4 = new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "somethingelse"); assertEquals(scopedAuthenticationToken1, scopedAuthenticationToken2); assertNotEquals(scopedAuthenticationToken1, scopedAuthenticationToken3); @@ -63,6 +64,10 @@ public void testEqualsAndHashCode() { scopedAuthenticationToken2 = new ScopedAuthenticationToken("username1", "somethingelse"); assertNotEquals(scopedAuthenticationToken1, scopedAuthenticationToken2); assertNotEquals(scopedAuthenticationToken1.hashCode(), scopedAuthenticationToken2.hashCode()); + } + @Test + public void testFromString(){ + assertEquals(AuthenticationScope.SAVE_AND_RESTORE, AuthenticationScope.fromString(AuthenticationScope.SAVE_AND_RESTORE.getName())); } } diff --git a/core/security/src/test/java/org/phoebus/security/store/SecureStoreTest.java b/core/security/src/test/java/org/phoebus/security/store/SecureStoreTest.java index a1e5d647dc..5842ceea4c 100644 --- a/core/security/src/test/java/org/phoebus/security/store/SecureStoreTest.java +++ b/core/security/src/test/java/org/phoebus/security/store/SecureStoreTest.java @@ -20,16 +20,13 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.phoebus.security.tokens.AuthenticationScope; import org.phoebus.security.tokens.ScopedAuthenticationToken; import java.io.File; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; /** * Exercises the SecureStore via a FileBased underlying store implementation. @@ -44,7 +41,7 @@ public class SecureStoreTest { public static void setup() throws Exception { File secureStoreFile; secureStoreFile = new File(System.getProperty("user.home"), "TestOnlySecureStore.dat"); - if(secureStoreFile.exists()){ + if (secureStoreFile.exists()) { secureStoreFile.delete(); } secureStoreFile.deleteOnExit(); @@ -118,11 +115,10 @@ public void testGetAllScopedTokens() throws Exception { secureStore.set(SecureStore.USERNAME_TAG, "username"); secureStore.set(SecureStore.PASSWORD_TAG, "password"); - secureStore.set("scope1." + SecureStore.USERNAME_TAG, "username1"); - secureStore.set("scope1." + SecureStore.PASSWORD_TAG, "password1"); - secureStore.set("scope2." + SecureStore.USERNAME_TAG, "username2"); - secureStore.set("scope2." + SecureStore.PASSWORD_TAG, "password2"); - secureStore.set("scope3." + SecureStore.USERNAME_TAG, "username3"); + secureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.USERNAME_TAG, "username1"); + secureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.PASSWORD_TAG, "password1"); + secureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.USERNAME_TAG, "username2"); + secureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.PASSWORD_TAG, "password2"); List tokens = secureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -133,11 +129,10 @@ public void testGetAllScopedTokens() throws Exception { memorySecureStore.set(SecureStore.USERNAME_TAG, "username"); memorySecureStore.set(SecureStore.PASSWORD_TAG, "password"); - memorySecureStore.set("scope1." + SecureStore.USERNAME_TAG, "username1"); - memorySecureStore.set("scope1." + SecureStore.PASSWORD_TAG, "password1"); - memorySecureStore.set("scope2." + SecureStore.USERNAME_TAG, "username2"); - memorySecureStore.set("scope2." + SecureStore.PASSWORD_TAG, "password2"); - memorySecureStore.set("scope3." + SecureStore.USERNAME_TAG, "username3"); + memorySecureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.USERNAME_TAG, "username1"); + memorySecureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.PASSWORD_TAG, "password1"); + memorySecureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.USERNAME_TAG, "username2"); + memorySecureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.PASSWORD_TAG, "password2"); tokens = memorySecureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -150,45 +145,43 @@ public void testGetScopedToken() throws Exception { secureStore.set(SecureStore.USERNAME_TAG, "username"); secureStore.set(SecureStore.PASSWORD_TAG, "password"); - secureStore.set("scope1." + SecureStore.USERNAME_TAG, "username1"); - secureStore.set("scope1." + SecureStore.PASSWORD_TAG, "password1"); - secureStore.set("scope2." + SecureStore.USERNAME_TAG, "username2"); - secureStore.set("scope2." + SecureStore.PASSWORD_TAG, "password2"); - secureStore.set("scope3." + SecureStore.USERNAME_TAG, "username3"); + secureStore.set(AuthenticationScope.LOGBOOK.getName() + "." +SecureStore.USERNAME_TAG, "username1"); + secureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.PASSWORD_TAG, "password1"); + secureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.USERNAME_TAG, "username2"); + secureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.PASSWORD_TAG, "password2"); ScopedAuthenticationToken token = secureStore.getScopedAuthenticationToken(null); assertNotNull(token); - assertNull(token.getScope()); + assertNull(token.getAuthenticationScope()); - token = secureStore.getScopedAuthenticationToken("scope1"); + token = secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertNotNull(token); - assertEquals("scope1", token.getScope()); + assertEquals(AuthenticationScope.LOGBOOK, token.getAuthenticationScope()); assertEquals("username1", token.getUsername()); - token = secureStore.getScopedAuthenticationToken("invalid"); - assertNull(token); + token = secureStore.getScopedAuthenticationToken(null); + assertNotNull(token); secureStore.deleteAllScopedAuthenticationTokens(); memorySecureStore.set(SecureStore.USERNAME_TAG, "username"); memorySecureStore.set(SecureStore.PASSWORD_TAG, "password"); - memorySecureStore.set("scope1." + SecureStore.USERNAME_TAG, "username1"); - memorySecureStore.set("scope1." + SecureStore.PASSWORD_TAG, "password1"); - memorySecureStore.set("scope2." + SecureStore.USERNAME_TAG, "username2"); - memorySecureStore.set("scope2." + SecureStore.PASSWORD_TAG, "password2"); - memorySecureStore.set("scope3." + SecureStore.USERNAME_TAG, "username3"); + memorySecureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.USERNAME_TAG, "username1"); + memorySecureStore.set(AuthenticationScope.LOGBOOK.getName() + "." + SecureStore.PASSWORD_TAG, "password1"); + memorySecureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.USERNAME_TAG, "username2"); + memorySecureStore.set(AuthenticationScope.SAVE_AND_RESTORE.getName() + "." + SecureStore.PASSWORD_TAG, "password2"); token = memorySecureStore.getScopedAuthenticationToken(null); assertNotNull(token); - assertNull(token.getScope()); + assertNull(token.getAuthenticationScope()); - token = memorySecureStore.getScopedAuthenticationToken("scope1"); + token = memorySecureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertNotNull(token); - assertEquals("scope1", token.getScope()); + assertEquals(AuthenticationScope.LOGBOOK, token.getAuthenticationScope()); assertEquals("username1", token.getUsername()); - token = memorySecureStore.getScopedAuthenticationToken("invalid"); - assertNull(token); + token = memorySecureStore.getScopedAuthenticationToken(null); + assertNotNull(token); memorySecureStore.deleteAllScopedAuthenticationTokens(); } @@ -215,9 +208,8 @@ public void testGetAllScopedTokensLegacyOnly() throws Exception { @Test public void testSetScopedAuthenticationToken() throws Exception { secureStore.setScopedAuthentication(new ScopedAuthenticationToken("username", "password")); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username1", "password1")); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username2", "password2")); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username3", "password3")); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "username2", "password2")); List tokens = secureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -225,9 +217,9 @@ public void testSetScopedAuthenticationToken() throws Exception { secureStore.deleteAllScopedAuthenticationTokens(); memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("username", "password")); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username1", "password1")); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username2", "password2")); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username3", "password3")); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "username2", "password2")); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "username3", "password3")); tokens = memorySecureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -301,10 +293,8 @@ public void testSetInvalidAuthenticationToken() throws Exception { public void testDeleteAllScopedAuthenticationTokens() throws Exception { secureStore.setScopedAuthentication(new ScopedAuthenticationToken("username", "password")); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username1", "password1")); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username2", "password2")); - - secureStore.set("somethingElse", "value"); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "username2", "password2")); List tokens = secureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -313,15 +303,9 @@ public void testDeleteAllScopedAuthenticationTokens() throws Exception { tokens = secureStore.getAuthenticationTokens(); assertEquals(0, tokens.size()); - assertNotNull(secureStore.get("somethingElse")); - - secureStore.delete("somethingElse"); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("username", "password")); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username1", "password1")); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope2", "username2", "password2")); - - memorySecureStore.set("somethingElse", "value"); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.SAVE_AND_RESTORE, "username2", "password2")); tokens = memorySecureStore.getAuthenticationTokens(); assertEquals(3, tokens.size()); @@ -329,30 +313,26 @@ public void testDeleteAllScopedAuthenticationTokens() throws Exception { memorySecureStore.deleteAllScopedAuthenticationTokens(); tokens = memorySecureStore.getAuthenticationTokens(); assertEquals(0, tokens.size()); - - assertNotNull(memorySecureStore.get("somethingElse")); - - memorySecureStore.delete("somethingElse"); } @Test public void testOverwriteScopedAuthentication() throws Exception { - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("Scope1", "username1", "password1")); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); - ScopedAuthenticationToken token = secureStore.getScopedAuthenticationToken("Scope1"); + ScopedAuthenticationToken token = secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertEquals("username1", token.getUsername()); - secureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username2", "password1")); - token = secureStore.getScopedAuthenticationToken("scope1"); + secureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username2", "password1")); + token = secureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertEquals("username2", token.getUsername()); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("Scope1", "username1", "password1")); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username1", "password1")); - token = memorySecureStore.getScopedAuthenticationToken("Scope1"); + token = memorySecureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertEquals("username1", token.getUsername()); - memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken("scope1", "username2", "password1")); - token = memorySecureStore.getScopedAuthenticationToken("scope1"); + memorySecureStore.setScopedAuthentication(new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, "username2", "password1")); + token = memorySecureStore.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); assertEquals("username2", token.getUsername()); } } diff --git a/core/types/pom.xml b/core/types/pom.xml index 0b28cfe734..e4b6534b1a 100644 --- a/core/types/pom.xml +++ b/core/types/pom.xml @@ -4,13 +4,13 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/ui/pom.xml b/core/ui/pom.xml index 4ba9eaf56d..af0d150eb1 100644 --- a/core/ui/pom.xml +++ b/core/ui/pom.xml @@ -3,7 +3,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-ui @@ -59,27 +59,27 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-types - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-security - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT diff --git a/core/ui/src/main/java/org/phoebus/ui/Messages.java b/core/ui/src/main/java/org/phoebus/ui/Messages.java index bf516dff82..268639c251 100644 --- a/core/ui/src/main/java/org/phoebus/ui/Messages.java +++ b/core/ui/src/main/java/org/phoebus/ui/Messages.java @@ -14,47 +14,87 @@ */ public class Messages { - // Keep in alphabetical order, matching the order in messages.properties + /**Keep in alphabetical order, matching the order in messages.properties */ public static String Add; + /**AddColumn */ public static String AddColumn; + /**AddRow */ public static String AddRow; + /**Add_Tooltip */ public static String Add_Tooltip; + /**Apply */ public static String Apply; + /**Available */ public static String Available; + /**Clear */ public static String Clear; + /**Clear_Tooltip */ public static String Clear_Tooltip; + /**DefaultNewColumnName */ public static String DefaultNewColumnName; + /**DoNotShow */ public static String DoNotShow; + /**Format_Binary */ public static String Format_Binary; + /**Format_Compact */ public static String Format_Compact; + /**Format_Decimal */ public static String Format_Decimal; + /**Format_Default */ public static String Format_Default; + /**Format_Engineering */ public static String Format_Engineering; + /**Format_Exponential */ public static String Format_Exponential; + /**Format_Hexadecimal */ public static String Format_Hexadecimal; + /**Format_Sexagesimal */ public static String Format_Sexagesimal; + /**Format_SexagesimalDMS */ public static String Format_SexagesimalDMS; + /**Format_SexagesimalHMS */ public static String Format_SexagesimalHMS; + /**Format_String */ public static String Format_String; + /**InstallExamples */ public static String InstallExamples; + /**MagicLastRow */ public static String MagicLastRow; + /**MoveColumnLeft */ public static String MoveColumnLeft; + /**MoveColumnRight */ public static String MoveColumnRight; + /**MoveRowDown */ public static String MoveRowDown; + /**MoveRowUp */ public static String MoveRowUp; + /**NumberInputHdr */ public static String NumberInputHdr; + /**Num_Selected */ public static String Num_Selected; + /**Redo_TT */ public static String Redo_TT; + /**RemoveColumn */ public static String RemoveColumn; + /**RemoveRow */ public static String RemoveRow; + /**Remove_Tooltip */ public static String Remove_Tooltip; + /**RenameColumn */ public static String RenameColumn; + /**RenameColumnInfo */ public static String RenameColumnInfo; + /**RenameColumnTitle */ public static String RenameColumnTitle; + /**ReplaceExamplesWarningFMT */ public static String ReplaceExamplesWarningFMT; + /**Search */ public static String Search; + /**SearchAvailableItems */ public static String SearchAvailableItems; + /**Selected */ public static String Selected; + /**Undo_TT */ public static String Undo_TT; static diff --git a/core/ui/src/main/java/org/phoebus/ui/Preferences.java b/core/ui/src/main/java/org/phoebus/ui/Preferences.java index 4ff01491ea..62c450669d 100644 --- a/core/ui/src/main/java/org/phoebus/ui/Preferences.java +++ b/core/ui/src/main/java/org/phoebus/ui/Preferences.java @@ -14,33 +14,78 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") + public class Preferences { - public static final String SPLASH = "splash"; - + /** splash */ + public static final String SPLASH = "splash"; + /** default_apps */ @Preference public static String[] default_apps; + /** home_display */ @Preference public static String home_display; + /** top_resources */ @Preference public static String top_resources; + /** splash */ @Preference public static boolean splash; + /** welcome */ @Preference public static String welcome; + /** max_array_formatting */ @Preference public static int max_array_formatting; + /** ui_monitor_period */ @Preference public static int ui_monitor_period; + /** hide_spi_menu */ @Preference public static String[] hide_spi_menu; + /** status_show_user */ @Preference public static boolean status_show_user; + /** default_save_path */ @Preference public static String default_save_path; + /** layout_dir */ @Preference public static String layout_dir; + /** print_landscape */ @Preference public static boolean print_landscape; + /** ok_severity_text_color */ @Preference public static int[] ok_severity_text_color; + /** minor_severity_text_color */ @Preference public static int[] minor_severity_text_color; + /** major_severity_text_color */ @Preference public static int[] major_severity_text_color; + /** invalid_severity_text_color */ @Preference public static int[] invalid_severity_text_color; + /** undefined_severity_text_color */ @Preference public static int[] undefined_severity_text_color; + /** ok_severity_background_color */ @Preference public static int[] ok_severity_background_color; + /** minor_severity_background_color */ @Preference public static int[] minor_severity_background_color; + /** major_severity_background_color */ @Preference public static int[] major_severity_background_color; + /** invalid_severity_background_color */ @Preference public static int[] invalid_severity_background_color; + /** undefined_severity_background_color */ @Preference public static int[] undefined_severity_background_color; + // Alarm Area Panel Configuration: + /** ok_severity_text_color */ + @Preference public static int[] alarm_area_panel_ok_severity_text_color; + /** minor_severity_text_color */ + @Preference public static int[] alarm_area_panel_minor_severity_text_color; + /** major_severity_text_color */ + @Preference public static int[] alarm_area_panel_major_severity_text_color; + /** invalid_severity_text_color */ + @Preference public static int[] alarm_area_panel_invalid_severity_text_color; + /** undefined_severity_text_color */ + @Preference public static int[] alarm_area_panel_undefined_severity_text_color; + /** ok_severity_background_color */ + @Preference public static int[] alarm_area_panel_ok_severity_background_color; + /** minor_severity_background_color */ + @Preference public static int[] alarm_area_panel_minor_severity_background_color; + /** major_severity_background_color */ + @Preference public static int[] alarm_area_panel_major_severity_background_color; + /** invalid_severity_background_color */ + @Preference public static int[] alarm_area_panel_invalid_severity_background_color; + /** undefined_severity_background_color */ + @Preference public static int[] alarm_area_panel_undefined_severity_background_color; + /** cache_hint_for_picture_and_symbol_widgets */ + @Preference public static String cache_hint_for_picture_and_symbol_widgets; static { diff --git a/core/ui/src/main/java/org/phoebus/ui/application/ApplicationLauncherService.java b/core/ui/src/main/java/org/phoebus/ui/application/ApplicationLauncherService.java index 26067f9cf1..bcb5fad911 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/ApplicationLauncherService.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/ApplicationLauncherService.java @@ -24,7 +24,7 @@ * The service allows you to directly launch an application if you have the * application name. * - * It also queries the {@link ResourceHandlerService} and + * It also queries the @see ResourceHandlerService and * {@link ApplicationService} to provide the option to launch application from * their associated resources. * @@ -73,12 +73,11 @@ public static boolean openResource(final URI resource, final boolean prompt, fin /** * Open file * - * @param stage - * Parent stage + * @param file file * @param prompt * Prompt for application (if there are multiple options), or use * default app? - * @param stage + * @param stage Parent stage * If prompt is enabled, a selection dialog will be launched * positioned next to the provided stage. If null then the * default or first application will be used diff --git a/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuService.java b/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuService.java index ca6f25d5b2..8520addb99 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuService.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuService.java @@ -12,6 +12,10 @@ import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.spi.ContextMenuEntry; + +/** + * Class ContextMenuService + */ @SuppressWarnings("rawtypes") public class ContextMenuService { @@ -25,6 +29,7 @@ private ContextMenuService() { .unmodifiableList(loader.stream().map(Provider::get).collect(Collectors.toList())); } + /** @return instance */ public static synchronized ContextMenuService getInstance() { if (contextMenuService == null) { contextMenuService = new ContextMenuService(); @@ -35,7 +40,7 @@ public static synchronized ContextMenuService getInstance() { /** * Get the list of registered context menu providers * - * @return + * @return context menu entries */ public List listContextMenuEntries() { return contextMenuEntries; diff --git a/core/ui/src/main/java/org/phoebus/ui/application/MenuEntryService.java b/core/ui/src/main/java/org/phoebus/ui/application/MenuEntryService.java index dd02c422e2..0bfab7b5ab 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/MenuEntryService.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/MenuEntryService.java @@ -62,6 +62,7 @@ synchronized void populateMenuTree(MenuTreeNode root, List menuEntrie } } + /** @return instance */ public static synchronized MenuEntryService getInstance() { if (menuEntryService == null) { menuEntryService = new MenuEntryService(); @@ -87,17 +88,23 @@ public MenuTreeNode getMenuEntriesTree() { return this.menuEntryTree; } + /** Class MenuTreeNode */ public static class MenuTreeNode { private final String name; private List children; private List menuItems; + /** + * Constructor + * @param name of tree node + */ public MenuTreeNode(String name) { this.name = name; this.children = new ArrayList<>(); this.menuItems = new ArrayList<>(); } + /**add children @param children list of children nodes */ public void addChildren(MenuTreeNode... children) { this.children.addAll(Arrays.asList(children)); } diff --git a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java index 14332cdad5..e9a8cdb981 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java @@ -113,8 +113,13 @@ public class Messages public static String SaveAsPrompt; public static String SaveDlgErrHdr; public static String SaveDlgHdr; + public static String SaveAsFileAlreadyOpen_content; + public static String SaveAsFileAlreadyOpen_header; + public static String SaveAsFileAlreadyOpen_title; public static String SaveLayoutAs; public static String SaveLayoutOfContainingWindowAs; + public static String SaveLayoutWarningApplicationNoSaveFile; + public static String SaveLayoutWarningApplicationNoSaveFileTitle; public static String SaveSnapshot; public static String SaveSnapshotSelectFilename; public static String Saving; @@ -144,6 +149,26 @@ public class Messages public static String TimeYear; public static String TopResources; public static String UnLockPane; + public static String UnsavedChanges; + public static String UnsavedChanges_clearButtonText; + public static String UnsavedChanges_close; + public static String UnsavedChanges_discardButtonText_discardAnd; + public static String UnsavedChanges_exit; + public static String UnsavedChanges_mainWindow; + public static String UnsavedChanges_replace; + public static String UnsavedChanges_saveButtonText; + public static String UnsavedChanges_saveButtonText_saveAnd; + public static String UnsavedChanges_saved; + public static String UnsavedChanges_saving; + public static String UnsavedChanges_savingFailed; + public static String UnsavedChanges_secondaryWindow; + public static String UnsavedChanges_selectAllButtonText; + public static String UnsavedChanges_theFollowingApplicationInstancesHaveUnsavedChanges; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout; public static String WebBrowser; public static String Welcome; public static String Window; diff --git a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java index ed20e97dca..e2ca5467b4 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java @@ -5,14 +5,48 @@ import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import javafx.event.ActionEvent; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Dialog; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.jobs.JobMonitor; import org.phoebus.framework.jobs.SubJobMonitor; @@ -48,27 +82,13 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Node; -import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckMenuItem; -import javafx.scene.control.Dialog; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.ToolBar; -import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.Window; @@ -477,10 +497,10 @@ private MenuBar createMenu(final Stage stage) { top_resources_menu.setDisable(true); final MenuItem file_save = new MenuItem(Messages.Save, ImageCache.getImageView(getClass(), "/icons/save_edit.png")); - file_save.setOnAction(event -> JobManager.schedule(Messages.Save, monitor -> active_item_with_input.get().save(monitor))); + file_save.setOnAction(event -> JobManager.schedule(Messages.Save, monitor -> active_item_with_input.get().save(monitor, active_item_with_input.get().getTabPane().getScene().getWindow()))); final MenuItem file_save_as = new MenuItem(Messages.SaveAs, ImageCache.getImageView(getClass(), "/icons/saveas_edit.png")); - file_save_as.setOnAction(event -> JobManager.schedule(Messages.SaveAs, monitor -> active_item_with_input.get().save_as(monitor))); + file_save_as.setOnAction(event -> JobManager.schedule(Messages.SaveAs, monitor -> active_item_with_input.get().save_as(monitor, active_item_with_input.get().getTabPane().getScene().getWindow()))); final MenuItem exit = new MenuItem(Messages.Exit); exit.setOnAction(event -> closeMainStage()); @@ -581,6 +601,11 @@ private MenuBar createMenu(final Stage stage) { return menuBar; } + private List listOfLayouts = new LinkedList<>(); + protected List getListOfLayouts() { + return listOfLayouts; + } + /** * Create the load past layouts menu */ @@ -622,6 +647,7 @@ void createLoadLayoutsMenu() { } + listOfLayouts = new LinkedList<>(); // For every non default memento file create a menu item for the load layout menu. if (!layoutFiles.keySet().isEmpty()) { // Sort layout files alphabetically. @@ -635,6 +661,8 @@ void createLoadLayoutsMenu() { // Remove ".memento" filename = filename.substring(0, filename.length() - 8); + listOfLayouts.add(filename); + // Build the list of memento files. memento_files.add(filename); final MenuItem menuItem = new MenuItem(filename); @@ -774,9 +802,15 @@ private ToolBar createToolbar() { home_display_button.setTooltip(new Tooltip(Messages.HomeTT)); toolBar.getItems().add(home_display_button); - final TopResources homeResource = TopResources.parse(Preferences.home_display); - home_display_button.setOnAction(event -> openResource(homeResource.getResource(0), false)); + if (!Preferences.home_display.isEmpty()) { + final TopResources homeResource = TopResources.parse(Preferences.home_display); + home_display_button.setOnAction(event -> openResource(homeResource.getResource(0), false)); + } + else { + Welcome welcome = new Welcome(); + home_display_button.setOnAction(event -> welcome.create()); + } top_resources_button = new MenuButton(null, ImageCache.getImageView(getClass(), "/icons/fldr_obj.png")); top_resources_button.setTooltip(new Tooltip(Messages.TopResources)); @@ -1054,31 +1088,41 @@ private void replaceLayout(final MementoTree memento) { JobManager.schedule("Close all stages", monitor -> { - for (Stage stage : stages) - if (!DockStage.prepareToCloseItems(stage)) - return; + boolean shouldReplaceLayout = confirmationDialogWhenUnsavedChangesExist(stages, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout, + Messages.UnsavedChanges_replace, + main_stage, + monitor); - // All stages OK to close - Platform.runLater(() -> - { + if (shouldReplaceLayout) { for (Stage stage : stages) { - DockStage.closeItems(stage); - // Don't wait for Platform.runLater-based tab handlers - // that will merge splits and eventually close the empty panes, - // but close all non-main stages right away - if (stage != main_stage) - stage.close(); + if (!DockStage.prepareToCloseItems(stage)) { + return; + } } - // Go into the main stage and close all of the tabs. If any of them refuse, return. - final Node node = DockStage.getPaneOrSplit(main_stage); - if (!MementoHelper.closePaneOrSplit(node)) - return; + // All stages OK to close + Platform.runLater(() -> + { + for (Stage stage : stages) { + DockStage.closeItems(stage); + // Don't wait for Platform.runLater-based tab handlers + // that will merge splits and eventually close the empty panes, + // but close all non-main stages right away + if (stage != main_stage) + stage.close(); + } - // Allow handlers for tab changes etc. to run as everything closed. - // On next UI tick, load content from memento file. - Platform.runLater(() -> restoreState(memento)); - }); + // Go into the main stage and close all of the tabs. If any of them refuse, return. + final Node node = DockStage.getPaneOrSplit(main_stage); + if (!MementoHelper.closePaneOrSplit(node)) + return; + + // Allow handlers for tab changes etc. to run as everything closed. + // On next UI tick, load content from memento file. + Platform.runLater(() -> restoreState(memento)); + }); + } }); } @@ -1263,22 +1307,347 @@ private void closeMainStage() { JobManager.schedule("Close all stages", monitor -> { - // closeStages() - for (Stage stage : stages) - if (!DockStage.prepareToCloseItems(stage)) - return; + boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting, + Messages.UnsavedChanges_exit, + main_stage, + monitor); - // All stages OK to close - Platform.runLater(() -> - { + if (shouldExit) { for (Stage stage : stages) - DockStage.closeItems(stage); + if (!DockStage.prepareToCloseItems(stage)) { + return; + } - stop(); - }); + exitPhoebus(); + } }); } + private static SortedMap> listOfDockItems2ApplicationNameToDockItemsWithInput(List dockItems) { + SortedMap> applicationNameToDockItemsWithInput = new TreeMap<>(); + for (DockItem dockItem : dockItems) { + if (dockItem instanceof DockItemWithInput) { + DockItemWithInput dockItemWithInput = (DockItemWithInput) dockItem; + if (dockItemWithInput.isDirty()) { + String applicationName = dockItemWithInput.getApplication().getAppDescriptor().getDisplayName(); + if (!applicationNameToDockItemsWithInput.containsKey(applicationName)) { + applicationNameToDockItemsWithInput.put(applicationName, new LinkedList<>()); + } + + applicationNameToDockItemsWithInput.get(applicationName).add(dockItemWithInput); + } + } + } + return applicationNameToDockItemsWithInput; + } + + private static SortedMap>> stages2WindowNameToApplicationNameToDockItemsWithInput(List stages) { + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + { + int currentWindowNr = 1; + + for (Stage stage : stages) { + String currentWindowName; + if (stage == DockStage.getDockStages().get(0)) { + currentWindowName = Messages.UnsavedChanges_mainWindow; + } + else { + currentWindowName = Messages.UnsavedChanges_secondaryWindow + " " + currentWindowNr; + currentWindowNr++; + } + + List dockItems = DockStage.getDockPanes(stage).stream().flatMap(dockPane -> dockPane.getDockItems().stream()).collect(Collectors.toList()); + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + windowNameToApplicationNameToDockItemsWithInput.put(currentWindowName, applicationNameToDockItemsWithInput); + } + } + return windowNameToApplicationNameToDockItemsWithInput; + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(Stage stage, + String question, + String closeActionName, + JobMonitor monitor) throws ExecutionException, InterruptedException { + List dockItems = DockStage.getDockPanes(stage).stream().flatMap(dockPane -> dockPane.getDockItems().stream()).collect(Collectors.toList()); + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + windowNameToApplicationNameToDockItemsWithInput.put("Window", applicationNameToDockItemsWithInput); + + return confirmationDialogWhenUnsavedChangesExist(windowNameToApplicationNameToDockItemsWithInput, + question, + closeActionName, + stage, + monitor); + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(ArrayList dockItems, // "ArrayList" is used instead of "List" to prevent a conflict with "List" after type erasure. + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + windowNameToApplicationNameToDockItemsWithInput.put("Window", applicationNameToDockItemsWithInput); + + return confirmationDialogWhenUnsavedChangesExist(windowNameToApplicationNameToDockItemsWithInput, + question, + closeActionName, + stage, + monitor); + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(List stages, + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + return confirmationDialogWhenUnsavedChangesExist(stages2WindowNameToApplicationNameToDockItemsWithInput(stages), + question, + closeActionName, + stage, + monitor); + } + + private enum SaveStatus { + SUCCESS, + FAILURE, + NOTHING + }; + + public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap>> windowNrToApplicationNameToDockItemsWithInput, + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + + Stage stageToPositionTheConfirmationDialogOver; + if (stage != null) { + stageToPositionTheConfirmationDialogOver = stage; + } + else { + stageToPositionTheConfirmationDialogOver = INSTANCE.main_stage; + } + + ButtonType clearSelectionOfCheckboxes = new ButtonType(Messages.UnsavedChanges_clearButtonText); + ButtonType selectAllCheckboxes = new ButtonType(Messages.UnsavedChanges_selectAllButtonText); + ButtonType saveSelectedItems = new ButtonType(Messages.UnsavedChanges_saveButtonText); + ButtonType exitPhoebusWithoutSavingUnsavedChanges = new ButtonType(Messages.UnsavedChanges_discardButtonText_discardAnd + " " + closeActionName); + + FutureTask displayConfirmationWindow = new FutureTask(() -> { + Alert prompt = new Alert(AlertType.CONFIRMATION); + + prompt.getDialogPane().getButtonTypes().remove(ButtonType.OK); + ((ButtonBar) prompt.getDialogPane().lookup(".button-bar")).setButtonOrder(ButtonBar.BUTTON_ORDER_NONE); // Set the button order manually (since they are non-standard) + prompt.getDialogPane().getButtonTypes().add(clearSelectionOfCheckboxes); + prompt.getDialogPane().getButtonTypes().add(selectAllCheckboxes); + prompt.getDialogPane().getButtonTypes().add(saveSelectedItems); + prompt.getDialogPane().getButtonTypes().add(exitPhoebusWithoutSavingUnsavedChanges); + + Button cancel_button = (Button) prompt.getDialogPane().lookupButton(ButtonType.CANCEL); + cancel_button.setTooltip(new Tooltip(cancel_button.getText())); + + Button clearSelectionOfCheckboxes_button = (Button) prompt.getDialogPane().lookupButton(clearSelectionOfCheckboxes); + clearSelectionOfCheckboxes_button.setTooltip(new Tooltip(clearSelectionOfCheckboxes_button.getText())); + + Button selectAllCheckboxes_button = (Button) prompt.getDialogPane().lookupButton(selectAllCheckboxes); + selectAllCheckboxes_button.setTooltip(new Tooltip(selectAllCheckboxes_button.getText())); + + Button saveSelectedItems_button = (Button) prompt.getDialogPane().lookupButton(saveSelectedItems); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + + Button exitPhoebusWithoutSavingUnsavedChanges_button = (Button) prompt.getDialogPane().lookupButton(exitPhoebusWithoutSavingUnsavedChanges); + exitPhoebusWithoutSavingUnsavedChanges_button.setTooltip(new Tooltip(exitPhoebusWithoutSavingUnsavedChanges_button.getText())); + List> setCheckBoxStatusActions = new LinkedList<>(); + List> getCheckBoxStatusActions = new LinkedList<>(); + List> saveActions = new LinkedList<>(); + + Runnable enableAndDisableButtons = () -> { + if (getCheckBoxStatusActions.stream().anyMatch(getCheckBoxStatus -> getCheckBoxStatus.get())) { + clearSelectionOfCheckboxes_button.setDisable(false); + saveSelectedItems_button.setDisable(false); + exitPhoebusWithoutSavingUnsavedChanges_button.setDisable(true); + } + else { + clearSelectionOfCheckboxes_button.setDisable(true); + saveSelectedItems_button.setDisable(true); + exitPhoebusWithoutSavingUnsavedChanges_button.setDisable(false); + } + + if (getCheckBoxStatusActions.stream().allMatch(getCheckBoxStatus -> getCheckBoxStatus.get())) { + selectAllCheckboxes_button.setDisable(true); + saveSelectedItems_button.setText(Messages.UnsavedChanges_saveButtonText_saveAnd + " " + closeActionName); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + } + else { + selectAllCheckboxes_button.setDisable(false); + saveSelectedItems_button.setText(Messages.UnsavedChanges_saveButtonText); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + } + }; + + GridPane gridPane = new GridPane(); + gridPane.setVgap(4); + int currentRow = 0; + for (String windowName : windowNrToApplicationNameToDockItemsWithInput.keySet()) { + var applicationNameToDockItemsWithInput = windowNrToApplicationNameToDockItemsWithInput.get(windowName); + + if (applicationNameToDockItemsWithInput.size() > 0) { // Only print unsaved changes for a window if it actually containts any unsaved changes. + if (windowNrToApplicationNameToDockItemsWithInput.size() >= 2) { // Only print the window names if two or more windows are in the process of being closed. + Text windowTitle = new Text(windowName); + windowTitle.setStyle("-fx-font-size: 16; -fx-font-weight: bold"); + gridPane.add(windowTitle, 0, currentRow); + currentRow++; + } + + for (var applicationName : applicationNameToDockItemsWithInput.keySet()) { + for (var dockItemWithInput : applicationNameToDockItemsWithInput.get(applicationName)) { + CheckBox checkBox = new CheckBox(); + checkBox.selectedProperty().addListener((observableValue, old_value, new_value) -> enableAndDisableButtons.run()); + + Text applicationName_text = new Text(applicationName + ":"); + applicationName_text.setStyle("-fx-font-weight: bold"); + Text instanceName_text = new Text(dockItemWithInput.getLabel()); + + HBox hBox = new HBox(checkBox, applicationName_text, instanceName_text); + hBox.setSpacing(4); + gridPane.add(hBox, 0, currentRow); + + Consumer setCheckboxStatus = bool -> checkBox.setSelected(bool); + setCheckBoxStatusActions.add(setCheckboxStatus); + + Supplier getCheckBoxStatus = () -> checkBox.isSelected(); + getCheckBoxStatusActions.add(getCheckBoxStatus); + + hBox.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseEvent -> checkBox.setSelected(!checkBox.isSelected())); // Enable toggling checkbox by clicking on its label. + + Supplier saveIfCheckboxEnabled = () -> { + if (checkBox.isSelected()) { + + Text saving = new Text("[" + Messages.UnsavedChanges_saving + "]"); + saving.setFill(Color.ORANGE); + saving.setStyle("-fx-font-weight: bold;"); + hBox.getChildren().set(0, saving); + boolean saveSuccessful = dockItemWithInput.save(monitor, prompt.getDialogPane().getScene().getWindow()); + + if (saveSuccessful) { + // The functions setCheckboxStatus() and getCheckBoxStatus should not be available anymore: + setCheckBoxStatusActions.remove(setCheckboxStatus); + getCheckBoxStatusActions.remove(getCheckBoxStatus); + setCheckboxStatus.accept(false); + + Text saved = new Text("[" + Messages.UnsavedChanges_saved + "]"); + saved.setFill(Color.GREEN); + saved.setStyle("-fx-font-weight: bold;"); + hBox.getChildren().set(0, saved); + return SaveStatus.SUCCESS; + } + else { + Text savingFailed_text = new Text("[" + Messages.UnsavedChanges_savingFailed + "]"); + savingFailed_text.setFill(Color.RED); + savingFailed_text.setStyle("-fx-font-weight: bold;"); + + HBox savingFailed = new HBox(checkBox, savingFailed_text); + savingFailed.setSpacing(6); + hBox.getChildren().set(0, savingFailed); + return SaveStatus.FAILURE; + } + } + else { + return SaveStatus.NOTHING; + } + }; + saveActions.add(saveIfCheckboxEnabled); + + currentRow++; + } + } + } + } + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setContent(gridPane); + + prompt.getDialogPane().setContent(scrollPane); + + clearSelectionOfCheckboxes_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + setCheckBoxStatusActions.forEach(setCheckboxAction -> { + setCheckboxAction.accept(false); + }); + }); + + selectAllCheckboxes_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + setCheckBoxStatusActions.forEach(setCheckboxAction -> { + setCheckboxAction.accept(true); + }); + }); + + saveSelectedItems_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + List> saveActionsThatHaveBeenCompleted = new LinkedList<>(); + for (var saveAction : saveActions) { + SaveStatus result = saveAction.get(); + if (result == SaveStatus.SUCCESS) { + saveActionsThatHaveBeenCompleted.add(saveAction); + } + else if (result == SaveStatus.FAILURE) { + break; + } + // If result == SaveStatus.NOTHING, continue. + } + + for (var saveActionThatHasBeenCompleted : saveActionsThatHaveBeenCompleted) { + saveActions.remove(saveActionThatHasBeenCompleted); + } + + if (saveActions.size() == 0) { + exitPhoebusWithoutSavingUnsavedChanges_button.fire(); + } + }); + + // Initialize state of buttons: + enableAndDisableButtons.run(); + + prompt.setHeaderText(Messages.UnsavedChanges_theFollowingApplicationInstancesHaveUnsavedChanges + " " + question); + prompt.setTitle(Messages.UnsavedChanges); + + int prefWidth = 750; + int prefHeight = 400; + prompt.getDialogPane().setPrefSize(prefWidth, prefHeight); + prompt.getDialogPane().setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + prompt.setResizable(false); + + DialogHelper.positionDialog(prompt, stageToPositionTheConfirmationDialogOver.getScene().getRoot(), -prefWidth/2, -prefHeight/2); + + return prompt.showAndWait().orElse(ButtonType.CANCEL) == exitPhoebusWithoutSavingUnsavedChanges ? true : false; + }); + + if (windowNrToApplicationNameToDockItemsWithInput.isEmpty() || windowNrToApplicationNameToDockItemsWithInput.values().stream().allMatch(sortedMap -> sortedMap.values().stream().allMatch(Collection::isEmpty))) { + // No unsaved changes. + return true; + } + else { + Platform.runLater(displayConfirmationWindow); + boolean shouldClose = (boolean) displayConfirmationWindow.get(); + return shouldClose; + } + } + + private void exitPhoebus() { + Platform.runLater(() -> + { + for (Stage stage : DockStage.getDockStages()) { + DockStage.closeItems(stage); + } + stop(); + }); + }; + /** * Start all applications * @@ -1328,16 +1697,21 @@ public static void closeAllTabs(){ final List stages = DockStage.getDockStages(); JobManager.schedule("Close All Tabs", monitor -> { - for (Stage stage : stages){ - if (!DockStage.prepareToCloseItems(stage)){ - return; + boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stages, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs, + Messages.UnsavedChanges_close, + PhoebusApplication.INSTANCE.main_stage, + monitor); + + if (shouldCloseTabs) { + for (Stage stage : stages){ + if (!DockStage.prepareToCloseItems(stage)){ + return; + } } - } - Platform.runLater(() -> - { - stages.forEach(stage -> DockStage.closeItems(stage)); - }); + Platform.runLater(() -> stages.forEach(stage -> DockStage.closeItems(stage))); + } }); } } diff --git a/core/ui/src/main/java/org/phoebus/ui/application/SaveLayoutHelper.java b/core/ui/src/main/java/org/phoebus/ui/application/SaveLayoutHelper.java index edc2c78a3e..b933336c9f 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/SaveLayoutHelper.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/SaveLayoutHelper.java @@ -9,12 +9,25 @@ import java.io.File; import java.text.MessageFormat; +import java.util.LinkedList; import java.util.List; -import javafx.scene.control.*; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextInputDialog; +import org.phoebus.framework.autocomplete.Proposal; +import org.phoebus.framework.autocomplete.ProposalProvider; +import org.phoebus.framework.autocomplete.ProposalService; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.workbench.Locations; +import org.phoebus.ui.autocomplete.AutocompleteMenu; import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockItemWithInput; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.docking.DockStage; import org.phoebus.ui.internal.MementoHelper; import javafx.scene.control.Alert.AlertType; @@ -42,11 +55,67 @@ private static boolean validateFilename(final String filename) */ public static boolean saveLayout(List stagesToSave, String titleText) { + List applicationsWithNoAssociatedSaveFile = new LinkedList<>(); + for (Stage stage : stagesToSave) { + for (DockPane dockPane : DockStage.getDockPanes(stage)) { + for (DockItem dockItem : dockPane.getDockItems()) { + if (dockItem instanceof DockItemWithInput) { + DockItemWithInput dockItemWithInput = (DockItemWithInput) dockItem; + if (dockItemWithInput.getInput() == null) { + applicationsWithNoAssociatedSaveFile.add(dockItemWithInput.getApplication().getAppDescriptor().getDisplayName()); + } + } + } + } + } + if (applicationsWithNoAssociatedSaveFile.size() > 0) { + String warningText = Messages.SaveLayoutWarningApplicationNoSaveFile; + for (String applicationName : applicationsWithNoAssociatedSaveFile) { + warningText += "\n - " + applicationName; + } + Alert dialog = new Alert(AlertType.CONFIRMATION, warningText, ButtonType.YES, ButtonType.NO); + ((Button) dialog.getDialogPane().lookupButton(ButtonType.YES)).setDefaultButton(false); + ((Button) dialog.getDialogPane().lookupButton(ButtonType.NO)).setDefaultButton(true); + dialog.setTitle(Messages.SaveLayoutWarningApplicationNoSaveFileTitle); + dialog.getDialogPane().setPrefSize(550, 320); + dialog.setResizable(true); + positionDialog(dialog, stagesToSave.get(0)); + + ButtonType response = dialog.showAndWait().orElse(ButtonType.NO); + + if (response == ButtonType.NO) { + return false; + } + } + final TextInputDialog prompt = new TextInputDialog(); prompt.setTitle(titleText); prompt.setHeaderText(Messages.SaveDlgHdr); positionDialog(prompt, stagesToSave.get(0)); + { + ProposalProvider proposalProvider = new ProposalProvider() { + @Override + public String getName() { + return "Existing Layouts"; + } + + @Override + public List lookup(String text) { + List listOfProposals = new LinkedList<>(); + for (String layout : PhoebusApplication.INSTANCE.getListOfLayouts()) { + if (layout.startsWith(text)) { + listOfProposals.add(new Proposal(layout)); + } + } + return listOfProposals; + } + }; + ProposalService proposalService = new ProposalService(proposalProvider); + AutocompleteMenu autocompleteMenu = new AutocompleteMenu(proposalService); + autocompleteMenu.attachField(prompt.getEditor()); + } + while (true) { final String filename = prompt.showAndWait().orElse(null); diff --git a/core/ui/src/main/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialog.java b/core/ui/src/main/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialog.java index 0c269b789c..c91be76d9c 100644 --- a/core/ui/src/main/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialog.java +++ b/core/ui/src/main/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialog.java @@ -7,14 +7,23 @@ ******************************************************************************/ package org.phoebus.ui.dialog; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.scene.control.TextArea; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; /** Dialog that shows error message with exception detail * @author Kay Kasemir @@ -22,6 +31,8 @@ @SuppressWarnings("nls") public class ExceptionDetailsErrorDialog { + private static final String LINE_SEPARATOR = System.lineSeparator(); + /** Open dialog that shows detail of error * *

      May be called from non-UI thread @@ -33,7 +44,22 @@ public class ExceptionDetailsErrorDialog */ public static void openError(final Node node, final String title, final String message, final Exception exception) { - Platform.runLater(() -> doOpenError(node, title, message, exception)); + Platform.runLater(() -> doOpenError(node, title, message, exception, true)); + } + + /** Open dialog that shows detail of error + * + *

      May be called from non-UI thread + * + * @param node Node relative to which the dialog will be positioned + * @param title Title + * @param message Message, may have multiple lines + * @param exception Exception + * @param appendStacktraceMsgs Append a summary for all the exceptions in the stacktrace + */ + public static void openError(final Node node, final String title, final String message, final Exception exception, final boolean appendStacktraceMsgs) + { + Platform.runLater(() -> doOpenError(node, title, message, exception, appendStacktraceMsgs)); } /** Open dialog that shows detail of error @@ -46,14 +72,47 @@ public static void openError(final Node node, final String title, final String m */ public static void openError(final String title, final String message, final Exception exception) { - openError(null, title, message, exception); + openError(null, title, message, exception, true); } - private static void doOpenError(final Node node, final String title, final String message, final Exception exception) + /** Open dialog that shows detail of error, the message/header text is constructed using the messages from the + * exception's stacktrace + * + *

      May be called from non-UI thread + * + * @param title Title + * @param exception Exception + */ + public static void openError(final String title, final Exception exception) { + openError(null, title, "", exception, true); + } + + private static void doOpenError(final Node node, final String title, final String message, final Exception exception, final boolean append_stacktrace_msgs) + { + StringBuilder messageBuilder = new StringBuilder(message).append(LINE_SEPARATOR); + if(append_stacktrace_msgs) + { + messageBuilder.append(exception.getMessage() != null ? exception.getMessage() : exception.getClass()).append(LINE_SEPARATOR).append("Cause:").append(LINE_SEPARATOR); + Throwable cause = exception.getCause(); + int exceptionIndex = 1; + // maintain a list of 'causes' to handle CIRCULAR REFERENCE s + final List throwableCausesList = new ArrayList<>(); + while (cause != null && !throwableCausesList.contains(cause)) { + throwableCausesList.add(cause); + messageBuilder.append("[" + exceptionIndex + "] ").append(cause.getMessage() != null ? cause.getMessage() : cause.getClass().getName()).append(LINE_SEPARATOR); + exceptionIndex++; + cause = cause.getCause(); + } + } + final Alert dialog = new Alert(AlertType.ERROR); dialog.setTitle(title); - dialog.setHeaderText(message); + dialog.setHeaderText(messageBuilder.toString()); + + // A custom button which copies the message to clipboard + final ButtonType copyMessage = new ButtonType("Copy Msg & Close", ButtonBar.ButtonData.RIGHT); + dialog.getButtonTypes().add(copyMessage); if (exception != null) { @@ -78,6 +137,14 @@ private static void doOpenError(final Node node, final String title, final Strin if (node != null) DialogHelper.positionDialog(dialog, node, -400, -200); - dialog.showAndWait(); + Optional result = dialog.showAndWait(); + if (result.isPresent() && result.get() == copyMessage) { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(message); + clipboard.setContent(content); + } } + + } diff --git a/core/ui/src/main/java/org/phoebus/ui/dialog/PopOver.java b/core/ui/src/main/java/org/phoebus/ui/dialog/PopOver.java index 16b0798b04..0bed7f0a65 100644 --- a/core/ui/src/main/java/org/phoebus/ui/dialog/PopOver.java +++ b/core/ui/src/main/java/org/phoebus/ui/dialog/PopOver.java @@ -173,7 +173,7 @@ private Side determineSide(final Bounds owner_bounds) * the popup will hide. * * @param owner Owner node relative to which the PopOver will be located - * @see {@link PopupControl#hide()} + * @see PopupControl hide methode */ public void show(final Region owner) { diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java index 134c8c26ce..e44a4ee891 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java @@ -24,11 +24,13 @@ import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.event.Event; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.spi.AppDescriptor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.security.authorization.AuthorizationService; import org.phoebus.ui.application.Messages; +import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.application.SaveLayoutHelper; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; @@ -175,9 +177,40 @@ public DockItem(final String label) createContextMenu(); + setOnCloseRequest(event -> handleCloseRequest(event)); setOnClosed(event -> handleClosed()); } + private void handleCloseRequest(Event event) { + // For now, prevent closing + event.consume(); + + // Invoke all the ok-to-close checks in background threads + // since those that save files might take time. + JobManager.schedule("Close " + getLabel(), monitor -> + { + boolean shouldClose = this instanceof DockItemWithInput ? ((DockItemWithInput) this).okToClose().get() : true; + + if (shouldClose) { + var dockPane = getDockPane(); + Platform.runLater(() -> { + // Select the previously selected tab: + dockPane.tabsInOrderOfFocus.remove(this); + + if (dockPane.tabsInOrderOfFocus.size() > 0) { + var tabToSelect = dockPane.tabsInOrderOfFocus.getFirst(); + var selectionModel = dockPane.getSelectionModel(); + selectionModel.select(tabToSelect); + } + }); + + // Close the tab: + prepareToClose(); + Platform.runLater(() -> close()); + } + }); + } + /** This tab should be in a DockPane, not a plain TabPane * @return DockPane that holds this tab */ @@ -210,23 +243,25 @@ private void createContextMenu() if (stagesContainingActiveDockPane.size() == 1) { SaveLayoutHelper.saveLayout(stagesContainingActiveDockPane, Messages.SaveLayoutOfContainingWindowAs); } - else if (stagesContainingActiveDockPane.size() == 1) { - logger.log(Level.SEVERE, "No stage contains the active dock pane!"); + else if (stagesContainingActiveDockPane.size() == 0) { + logger.log(Level.SEVERE, "No stage contains the active dock pane! Unable to save the layout of the containing window."); } else { - logger.log(Level.SEVERE, "More than one stage contains the active dock pane!"); + logger.log(Level.SEVERE, "More than one stage contains the active dock pane! Unable to save the layout of the containing window."); } }); final MenuItem close = new MenuItem(Messages.DockClose, new ImageView(DockPane.close_icon)); - close.setOnAction(event -> close(List.of(this))); + ArrayList arrayList = new ArrayList(); + arrayList.add(this); + close.setOnAction(event -> close(arrayList)); final MenuItem close_other = new MenuItem(Messages.DockCloseOthers, new ImageView(close_many_icon)); close_other.setOnAction(event -> { // Close all other tabs in non-fixed panes of this window final Stage stage = (Stage) getDockPane().getScene().getWindow(); - final List tabs = new ArrayList<>(); + final ArrayList tabs = new ArrayList<>(); for (DockPane pane : getDockPanes(stage)) if (! pane.isFixed()) for (Tab tab : new ArrayList<>(pane.getTabs())) @@ -240,7 +275,7 @@ else if (stagesContainingActiveDockPane.size() == 1) { { // Close all tabs in non-fixed panes of this window final Stage stage = (Stage) getDockPane().getScene().getWindow(); - final List tabs = new ArrayList<>(); + final ArrayList tabs = new ArrayList<>(); for (DockPane pane : getDockPanes(stage)) if (! pane.isFixed()) for (Tab tab : new ArrayList<>(pane.getTabs())) @@ -285,19 +320,28 @@ else if (stagesContainingActiveDockPane.size() == 1) { } /** @param tabs Tabs to prepare and then close */ - private static void close(final List tabs) + private void close(final ArrayList tabs) { JobManager.schedule("Close", monitor -> { - for (DockItem tab : tabs) - if (! tab.prepareToClose()) - return; - - Platform.runLater(() -> - { + Window window = getDockPane().getScene().getWindow(); + boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(tabs, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs, + Messages.UnsavedChanges_close, + window instanceof Stage ? (Stage) window : null, + monitor); + + if (shouldCloseTabs) { for (DockItem tab : tabs) - tab.close(); - }); + if (! tab.prepareToClose()) + return; + + Platform.runLater(() -> + { + for (DockItem tab : tabs) + tab.close(); + }); + } }); } @@ -593,21 +637,6 @@ public void select() */ public void addCloseCheck(final Supplier> ok_to_close) { - if (getOnCloseRequest() == null) - setOnCloseRequest(event -> - { - // For now, prevent closing - event.consume(); - - // Invoke all the ok-to-close checks in background threads - // since those that save files might take time. - JobManager.schedule("Close " + getLabel(), monitor -> - { - if (prepareToClose()) - Platform.runLater(() -> close()); - }); - }); - close_check.add(ok_to_close); } @@ -691,14 +720,22 @@ protected void handleClosed() setContent(null); // Remove "application" entry which otherwise holds on to application data model getProperties().remove(KEY_APPLICATION); + + // Ensure that the tab is removed from dockPane.tabsInOrderOfFocus + // to avoid memory leaks. (The tab may have been closed without + // calling the OnCloseRequest event-handler.) + var dockPane = getDockPane(); + if (dockPane != null) { + Platform.runLater(() -> { + dockPane.tabsInOrderOfFocus.remove(this); + }); + } } /** Programmatically close this tab * - *

      Should be called after {@link #prepareToClose) has been used - * to allow "save". + * Should be called after @see prepareToClose has been used to allow save. * - * @return true if tab closed, false if it remained open */ public void close() { diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index 2b8c7d05a5..fba63d64e6 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -11,20 +11,21 @@ import java.io.File; import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.scene.Scene; +import javafx.scene.layout.Region; import javafx.stage.Window; +import org.apache.commons.io.FilenameUtils; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.jobs.JobMonitor; import org.phoebus.framework.jobs.JobRunnable; @@ -103,8 +104,6 @@ public DockItemWithInput(final AppInstance application, final Node content, fina this.file_extensions = file_extensions; this.save_handler = save_handler; setInput(input); - - addCloseCheck(this::okToClose); } // Override to include 'dirty' tab @@ -169,7 +168,8 @@ public void setInput(final URI input) name_tab.setTooltip(new Tooltip(Messages.DockNotSaved)); else { - name_tab.setTooltip(new Tooltip(input.toString())); + String decodedInputURI = URLDecoder.decode(input.toString(), StandardCharsets.UTF_8); + name_tab.setTooltip(new Tooltip(decodedInputURI)); setLabel(name); } }); @@ -209,13 +209,13 @@ public boolean isSaveAsSupported() /** Called when user tries to close the tab * @return Should the tab close? Otherwise it stays open. */ - private Future okToClose() + public Future okToClose() { if (! isDirty()) return CompletableFuture.completedFuture(true); final FutureTask promptToSave = new FutureTask(() -> { - final String text = MessageFormat.format(Messages.DockAlertMsg, getLabel()); + final String text = MessageFormat.format(Messages.DockAlertMsg, getApplication().getAppDescriptor().getDisplayName(), getLabel()); final Alert prompt = new Alert(AlertType.NONE, text, ButtonType.NO, ButtonType.CANCEL, ButtonType.YES); @@ -245,7 +245,7 @@ private Future okToClose() final CompletableFuture done = new CompletableFuture<>(); JobManager.schedule(Messages.Save, monitor -> { - save(monitor); + save(monitor, getTabPane().getScene().getWindow()); // Indicate if we may close, or need to stay open because of error done.complete(!isDirty()); }); @@ -263,7 +263,7 @@ private Future okToClose() * @param monitor {@link JobMonitor} for reporting progress * @return true on success */ - public final boolean save(final JobMonitor monitor) + public final boolean save(final JobMonitor monitor, Window parentWindow) { // 'final' because any save customization should be possible // inside the save_handler @@ -274,7 +274,7 @@ public final boolean save(final JobMonitor monitor) // call save_as to prompt for file File file = ResourceParser.getFile(getInput()); if (file == null) - return save_as(monitor); + return save_as(monitor, parentWindow); if (file.exists() && !file.canWrite()) @@ -293,7 +293,7 @@ public final boolean save(final JobMonitor monitor) // If user doesn't want to overwrite, abort the save if (response.get() == ButtonType.OK) - return save_as(monitor); + return save_as(monitor, getTabPane().getScene().getWindow()); return false; } @@ -374,7 +374,7 @@ private static File setFileExtension(final File file, final List valid) * @param monitor {@link JobMonitor} for reporting progress * @return true on success */ - public final boolean save_as(final JobMonitor monitor) + public final boolean save_as(final JobMonitor monitor, Window parentWindow) { // 'final' because any save customization should be possible // inside the save_handler @@ -382,7 +382,7 @@ public final boolean save_as(final JobMonitor monitor) { // Prompt for file final File initial = ResourceParser.getFile(getInput()); - final File file = new SaveAsDialog().promptForFile(getTabPane().getScene().getWindow(), + final File file = new SaveAsDialog().promptForFile(parentWindow, Messages.SaveAs, initial, file_extensions); if (file == null) return false; @@ -402,7 +402,8 @@ public final boolean save_as(final JobMonitor monitor) file, valid.stream().collect(Collectors.joining(", ")), suggestion); - Platform.runLater(() -> + + Runnable confirmFileExtension = () -> { final Alert dialog = new Alert(AlertType.CONFIRMATION, prompt, ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); dialog.setTitle(Messages.SaveAs); @@ -419,16 +420,52 @@ else if (response == ButtonType.NO) actual_file.complete(file); else actual_file.complete(null); - }); + }; + + if (Platform.isFxApplicationThread()) { + confirmFileExtension.run(); + } + else { + Platform.runLater(confirmFileExtension); + } + // In background thread, wait for the result if (actual_file.get() == null) return false; } - // Update input - setInput(ResourceParser.getURI(actual_file.get())); - // Save in that file - return save(monitor); + URI newInput = ResourceParser.getURI(actual_file.get()); + DockItemWithInput existingInstanceWithInput = DockStage.getDockItemWithInput(newInput); + if (existingInstanceWithInput == null || (input != null && newInput.getPath().equals(input.getPath()))) { + // Update input + setInput(ResourceParser.getURI(actual_file.get())); + // Save in that file + return save(monitor, getTabPane().getScene().getWindow()); + } + else { + CompletableFuture waitForDialogToClose = new CompletableFuture<>(); + Platform.runLater(() -> { + String filename = FilenameUtils.getName(newInput.getPath()); + + final Alert dialog = new Alert(AlertType.INFORMATION); + dialog.setTitle(Messages.SaveAsFileAlreadyOpen_title); + String headerText = MessageFormat.format(Messages.SaveAsFileAlreadyOpen_header, filename); + dialog.setHeaderText(headerText); + String contentText = MessageFormat.format(Messages.SaveAsFileAlreadyOpen_content, existingInstanceWithInput.getApplication().getAppDescriptor().getDisplayName(), filename); + dialog.setContentText(contentText); + int width = 550; + int height = 200; + dialog.getDialogPane().setPrefSize(width, height); + dialog.getDialogPane().setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + dialog.setResizable(false); + DialogHelper.positionDialog(dialog, getTabPane(), -width/2, -height/2); + dialog.showAndWait(); + waitForDialogToClose.complete(true); + }); + + waitForDialogToClose.get(); + save_as(monitor, getTabPane().getScene().getWindow()); + } } catch (Exception ex) { diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java index e08435de32..b6926e4d8b 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java @@ -9,7 +9,11 @@ import java.lang.ref.WeakReference; import java.text.MessageFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.logging.Level; @@ -18,8 +22,10 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.stage.Window; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; +import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.Styles; @@ -219,8 +225,19 @@ public static void alwaysShowTabs(final boolean do_show_single_tabs) getTabs().addListener((InvalidationListener) change -> handleTabChanges()); setOnContextMenuRequested(this::showContextMenu); + + getSelectionModel().selectedItemProperty().addListener((observable, previous_item, new_item) -> { + Platform.runLater(() -> { + // Keep track of the order of focus of tabs: + if (new_item != null) { + tabsInOrderOfFocus.remove(new_item); + tabsInOrderOfFocus.push((DockItem) new_item); + } + }); + }); } + protected LinkedList tabsInOrderOfFocus = new LinkedList<>(); private void showContextMenu(final ContextMenuEvent event) { @@ -332,8 +349,13 @@ private void handleGlobalKeys(final KeyEvent event) if (item instanceof DockItemWithInput) { final DockItemWithInput active_item_with_input = (DockItemWithInput) item; - if (active_item_with_input.isDirty()) - JobManager.schedule(Messages.Save, monitor -> active_item_with_input.save(monitor)); + + if (event.isShiftDown()) { + JobManager.schedule(Messages.SaveAs, monitor -> active_item_with_input.save_as(monitor, active_item_with_input.getTabPane().getScene().getWindow())); + } + else if (active_item_with_input.isDirty()) { + JobManager.schedule(Messages.Save, monitor -> active_item_with_input.save(monitor, active_item_with_input.getTabPane().getScene().getWindow())); + } } event.consume(); } @@ -343,8 +365,12 @@ else if (key == KeyCode.W) { JobManager.schedule("Close " + item.getLabel(), monitor -> { - if (item.prepareToClose()) + boolean shouldClose = item instanceof DockItemWithInput ? ((DockItemWithInput) item).okToClose().get() : true; + + if (shouldClose) { + item.prepareToClose(); Platform.runLater(item::close); + } }); } event.consume(); diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java index b4a1efb195..5aa70fb99e 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java @@ -16,9 +16,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.UUID; import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.scene.input.MouseEvent; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.workbench.Locations; import org.phoebus.ui.application.Messages; @@ -108,7 +112,6 @@ static String createID(final String what) * * @param stage Stage, should be empty * @param tabs Zero or more initial {@link DockItem}s - * @throws Exception on error * * @return {@link DockPane} that was added to the {@link Stage} */ @@ -122,14 +125,22 @@ public static DockPane configureStage(final Stage stage, final DockItem... tabs) *

      Adds a Scene with a BorderPane layout and a DockPane in the center * * @param stage Stage, should be empty - * @param geometry_spec A geometry specification "{width}x{height}+{x}+{y}", see {@link Geometry} + * @param geometry A geometry specification "{width}x{height}+{x}+{y}", see {@link Geometry} * @param tabs Zero or more initial {@link DockItem}s - * @throws Exception on error * * @return {@link DockPane} that was added to the {@link Stage} */ public static DockPane configureStage(final Stage stage, final Geometry geometry, final DockItem... tabs) { + stage.addEventFilter(MouseEvent.MOUSE_MOVED, mouseEvent -> { + // Filtering MOUSE_MOVED events from unfocused windows prevents tooltips + // from displaying in unfocused windows. This, in turn, prevents unfocused + // windows from receiving the focus as a consequence on Windows and Mac OS. + if (!stage.focusedProperty().get()) { + mouseEvent.consume(); + } + }); + stage.getProperties().put(KEY_ID, createID("DockStage")); final DockPane pane = new DockPane(tabs); @@ -191,8 +202,18 @@ else if(layout.getChildren().get(0) instanceof SplitPane){ // and on success close them JobManager.schedule("Close " + stage.getTitle(), monitor -> { - if (prepareToCloseItems(stage)) + boolean shouldCloseStage = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stage, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow, + Messages.UnsavedChanges_close, + monitor); + + if (shouldCloseStage) { + if (!DockStage.prepareToCloseItems(stage)) { + return; + } + Platform.runLater(() -> closeItems(stage)); + } }); }); @@ -366,6 +387,25 @@ public static DockItemWithInput getDockItemWithInput(final String application_na return null; } + /** Locate DockItemWithInput with input + * @param input Input, must not be null + * @return {@link DockItemWithInput} or null if not found + */ + public static DockItemWithInput getDockItemWithInput(URI input) + { + Objects.requireNonNull(input); + for (Stage stage : getDockStages()) + for (DockPane pane : getDockPanes(stage)) + for (DockItem tab : pane.getDockItems()) + if (tab instanceof DockItemWithInput) + { + DockItemWithInput item = (DockItemWithInput) tab; + if (input.equals(item.getInput())) + return item; + } + return null; + } + /** Locate any 'fixed' {@link DockPane}s and un-fix them * @param stage Stage where to clear 'fixed' panes */ diff --git a/core/ui/src/main/java/org/phoebus/ui/internal/MementoHelper.java b/core/ui/src/main/java/org/phoebus/ui/internal/MementoHelper.java index 47104e8a87..7bd2a5f275 100644 --- a/core/ui/src/main/java/org/phoebus/ui/internal/MementoHelper.java +++ b/core/ui/src/main/java/org/phoebus/ui/internal/MementoHelper.java @@ -147,7 +147,7 @@ private static void saveDockItem(final MementoTree memento, final DockItem item) } /** Restore state of Stage from memento - * @param memento + * @param stage_memento * @param stage * @return true if any tab item was restored */ diff --git a/core/ui/src/main/java/org/phoebus/ui/javafx/LineNumberTableCellFactory.java b/core/ui/src/main/java/org/phoebus/ui/javafx/LineNumberTableCellFactory.java index 6a875e51ce..9de6f4e77b 100644 --- a/core/ui/src/main/java/org/phoebus/ui/javafx/LineNumberTableCellFactory.java +++ b/core/ui/src/main/java/org/phoebus/ui/javafx/LineNumberTableCellFactory.java @@ -19,8 +19,8 @@ * A generic cell factory you can simply use to display the row number in a * table. * - * @param T The type of the TableView generic type (i.e. T == TableView) - * @param E The type of the content in all cells in this TableColumn. + * @param The type of the TableView generic type (i.e. {@literal == TableView}) + * @param The type of the content in all cells in this TableColumn. * @author claudiorosati, European Spallation Source ERIC * @version 1.0.0 24 Jan 2018 */ diff --git a/core/ui/src/main/java/org/phoebus/ui/javafx/NonCachingScrollPane.java b/core/ui/src/main/java/org/phoebus/ui/javafx/NonCachingScrollPane.java new file mode 100644 index 0000000000..b44c44b522 --- /dev/null +++ b/core/ui/src/main/java/org/phoebus/ui/javafx/NonCachingScrollPane.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.ui.javafx; + +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Skin; +import javafx.scene.control.skin.ScrollPaneSkin; +import javafx.scene.layout.StackPane; + +import java.lang.reflect.Field; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link ScrollPane} subclass disabling caching on the (probably) viewport. Motivation is that + * under certain circumstances the contents of a {@link ScrollPane} gets blurred when zooming. + * See this OpenJDK bug and this + * Github issue. + * The implementation - based on the OpenJDK bug ticket - depends on reflection to access a private field + * in {@link ScrollPaneSkin}. As per November 2023 the issue is not fixed in JavaFX version 21. + *

      + * In case the addressed field is not available or accessible (e.g. due JavaFX updates), exceptions are logged, + * but not propagated, in which case this subclass inherits the behavior of {@link ScrollPane}. + *

      + */ +public class NonCachingScrollPane extends ScrollPane { + + public NonCachingScrollPane(Node content) { + super(content); + } + + @Override + protected Skin createDefaultSkin() { + try { + return new NonCachingScrollPaneSkin(this); + } catch (Exception e) { + // Java reflection magic failed, fall back to default skin. + return super.createDefaultSkin(); + } + } + + /** + * {@link ScrollPaneSkin} subclass that attempts to retrieve the private field viewRect + * of the {@link ScrollPaneSkin} class, and then set the caching of that skin to false. + * If the field cannot be accessed, a {@link RuntimeException} is thrown in the constructor. + */ + private static class NonCachingScrollPaneSkin extends ScrollPaneSkin { + + public NonCachingScrollPaneSkin(final ScrollPane scrollpane) { + super(scrollpane); + StackPane viewRect; + try { + Field viewRectField = ScrollPaneSkin.class.getDeclaredField("viewRect"); + viewRectField.setAccessible(true); + viewRect = (StackPane) viewRectField.get(this); + viewRect.setCache(false); + } + catch (Exception e) { + Logger.getLogger(NonCachingScrollPane.class.getName()).log(Level.WARNING, + "Unable to find field viewRect via reflection", + e); + throw new RuntimeException(e); + } + } + } +} diff --git a/core/ui/src/main/java/org/phoebus/ui/javafx/StringTable.java b/core/ui/src/main/java/org/phoebus/ui/javafx/StringTable.java index e857af92f4..9618757721 100644 --- a/core/ui/src/main/java/org/phoebus/ui/javafx/StringTable.java +++ b/core/ui/src/main/java/org/phoebus/ui/javafx/StringTable.java @@ -1006,10 +1006,9 @@ public void setCellColors(final List> colors) } } - /** Get background color for a specific cell + /** set background color for a specific cell * @param row Table row * @param col Table column - * @return Color of that cell, null for default */ public void setCellColor(final int row, final int col, final Color color) { diff --git a/core/ui/src/main/java/org/phoebus/ui/javafx/Tracker.java b/core/ui/src/main/java/org/phoebus/ui/javafx/Tracker.java index 8449ae2dd2..a21583fa6f 100644 --- a/core/ui/src/main/java/org/phoebus/ui/javafx/Tracker.java +++ b/core/ui/src/main/java/org/phoebus/ui/javafx/Tracker.java @@ -179,7 +179,7 @@ void hookEvents() // Keep the keyboard focus to actually get key events. // The RTImagePlot will also listen to mouse moves and try to keep the focus, // so the active tracker uses an event filter to have higher priority - tracker.addEventFilter(MouseEvent.MOUSE_MOVED, event -> + tracker.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { event.consume(); tracker.requestFocus(); diff --git a/core/ui/src/main/java/org/phoebus/ui/pv/PVList.java b/core/ui/src/main/java/org/phoebus/ui/pv/PVList.java index 2258f6b5a4..6957ba77ce 100644 --- a/core/ui/src/main/java/org/phoebus/ui/pv/PVList.java +++ b/core/ui/src/main/java/org/phoebus/ui/pv/PVList.java @@ -140,6 +140,7 @@ private void createTableColumns() name_col.setCellValueFactory(cell -> cell.getValue().name); name_col.setMaxWidth(400.0); table.getColumns().add(name_col); + table.getSortOrder().setAll(name_col); // By default, sort according to PV-name. final TableColumn ref_col = new TableColumn<>(Messages.PVListTblReferences); ref_col.setCellValueFactory(cell -> cell.getValue().references); @@ -211,6 +212,7 @@ private void triggerRefresh() { table.getItems().clear(); table.getItems().addAll(items); + table.sort(); }); }); } diff --git a/core/ui/src/main/java/org/phoebus/ui/time/TemporalAmountPane.java b/core/ui/src/main/java/org/phoebus/ui/time/TemporalAmountPane.java index eeb8e040ed..5a3d0ade63 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/TemporalAmountPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/TemporalAmountPane.java @@ -64,7 +64,7 @@ public enum Type }; /** Constructor - * @param include_now Should pane include 'now'? + * @param type Should pane include 'now'? */ public TemporalAmountPane(final Type type) { diff --git a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java index f9aac27a22..276a759f6f 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java @@ -201,7 +201,7 @@ public void setStart(final Instant instant) start_spec.setText(TimestampFormats.SECONDS_FORMAT.format(instant)); } - /** @param instant Relative start time span */ + /** @param amount Relative start time span */ public void setStart(final TemporalAmount amount) { rel_start.setTimespan(amount); @@ -219,7 +219,7 @@ public void setEnd(final Instant instant) end_spec.setText(TimestampFormats.SECONDS_FORMAT.format(instant)); } - /** @param instant Relative end time span */ + /** @param amount Relative end time span */ public void setEnd(final TemporalAmount amount) { rel_end.setTimespan(amount); diff --git a/core/ui/src/main/java/org/phoebus/ui/undo/UndoableActionManager.java b/core/ui/src/main/java/org/phoebus/ui/undo/UndoableActionManager.java index 8d3de88665..6cc01406af 100644 --- a/core/ui/src/main/java/org/phoebus/ui/undo/UndoableActionManager.java +++ b/core/ui/src/main/java/org/phoebus/ui/undo/UndoableActionManager.java @@ -98,7 +98,7 @@ public void add(final UndoableAction action) } /** Undo the last command - * @returns Action that was un-done + * @return Action that was un-done */ public UndoableAction undoLast() { @@ -122,7 +122,7 @@ public UndoableAction undoLast() } /** Re-do the last command - * @returns Action that was re-done + * @return Action that was re-done */ public UndoableAction redoLast() { diff --git a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties index 67b0b33430..2c9a3c4882 100644 --- a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties +++ b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties @@ -9,7 +9,7 @@ CloseAllTabs=Close All Tabs DeleteLayouts=Delete Layouts... DeleteLayoutsConfirmFmt=Delete {0} selected layouts? DeleteLayoutsInfo=Select layouts to delete -DockAlertMsg=The {0} has been modified.\n\nSave before closing? +DockAlertMsg=The {0} instance \"{1}\" has been modified.\n\nSave before closing? DockAlertTitle=Save File DockAll=All DockAppName=Application Name: @@ -99,8 +99,13 @@ SaveAsHdr=Use this file extension? SaveAsPrompt=The file name\n {0}\ndoes not have the suggested file extension\ni.e. {1}.\n\nYes: Save as {2}\nNo: Continue with original name\nCancel: Do not save SaveDlgErrHdr=Name must only contain alphanumeric characters, space, underscore or '-'.\nEnter a valid layout name. SaveDlgHdr=Enter a file name to save the layout as. +SaveAsFileAlreadyOpen_content=An instance of {0} associated with {1} is already running in a different tab. A different file path must be chosen for the save operation. +SaveAsFileAlreadyOpen_header=The file {0} is already associated with another tab. +SaveAsFileAlreadyOpen_title=File is already associated with another tab SaveLayoutAs=Save Layout As... SaveLayoutOfContainingWindowAs=Save Layout of Containing Window As... +SaveLayoutWarningApplicationNoSaveFile=The following application(s) do not have associated with them save file(s). No save file association(s) will be stored for the application instance(s) in question when proceeding to save the layout. Proceed?\n\nThe application(s) in question are:\n +SaveLayoutWarningApplicationNoSaveFileTitle=Warning: application(s) are not associated with save file(s) SaveSnapshot=Save Snapshot... SaveSnapshotSelectFilename=Enter *.png name for screenshot Saving=Saving {0}... @@ -130,6 +135,26 @@ TimeTime=Time: TimeYear=Year: TopResources=Top Resources UnLockPane=Un-lock Pane +UnsavedChanges=Unsaved changes +UnsavedChanges_clearButtonText=Clear +UnsavedChanges_close=close +UnsavedChanges_discardButtonText_discardAnd=Discard & +UnsavedChanges_exit=exit +UnsavedChanges_mainWindow=Main window +UnsavedChanges_replace=replace +UnsavedChanges_saveButtonText=Save +UnsavedChanges_saveButtonText_saveAnd=Save & +UnsavedChanges_saved=Saved +UnsavedChanges_saving=Saving... +UnsavedChanges_savingFailed=Saving failed +UnsavedChanges_secondaryWindow=Secondary window +UnsavedChanges_selectAllButtonText=Select all +UnsavedChanges_theFollowingApplicationInstancesHaveUnsavedChanges=The following application instances have unsaved changes. +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs=Would you like to save any changes before closing all tabs? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs=Would you like to save any changes before closing the tabs? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow=Would you like to save any changes before closing the window? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting=Would you like to save any changes before exiting? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout=Would you like to save any changes before replacing the layout? WebBrowser=Web Browser Welcome=Welcome Window=Window diff --git a/core/ui/src/main/resources/phoebus_ui_preferences.properties b/core/ui/src/main/resources/phoebus_ui_preferences.properties index 4af7515ec4..cb5180f426 100644 --- a/core/ui/src/main/resources/phoebus_ui_preferences.properties +++ b/core/ui/src/main/resources/phoebus_ui_preferences.properties @@ -88,3 +88,43 @@ invalid_severity_background_color=255,255,255 # Color for text and the background for 'UNDEFINED' alarm severity undefined_severity_text_color=200,0,200,200 undefined_severity_background_color=255,255,255 + +# Color Configuration for the application "Alarm Area Panel" (R,G,B or R,G,B,A values in range 0..255): +alarm_area_panel_ok_severity_text_color=255,255,255 +alarm_area_panel_ok_severity_background_color=0,255,0 + +alarm_area_panel_minor_severity_text_color=255,255,255 +alarm_area_panel_minor_severity_background_color=255,128,0 + +alarm_area_panel_major_severity_text_color=255,255,255 +alarm_area_panel_major_severity_background_color=255,0,0 + +alarm_area_panel_invalid_severity_text_color=255,255,255 +alarm_area_panel_invalid_severity_background_color=255,0,255 + +alarm_area_panel_undefined_severity_text_color=192,192,192 +alarm_area_panel_undefined_severity_background_color=200,0,200,200 + +# When Picture- and/or Symbol widgets are present in an OPI, +# zooming in under Windows using the D3D graphics library can +# cause excessive VRAM usage. Setting a cache hint can work as +# a workaround. Since it has been observed that the cache hints +# also can cause graphical errors, the setting of a cache hint +# is a configurable option, which must explicitly be set to +# have effect. +# +# The setting defaults to the default caching behavior. +# +# Valid options are: +# "" (the empty string) or "NONE" - The default caching behavior: caching is DISABLED, and the cache hint is set to "CacheHint.DEFAULT". +# "DEFAULT" - Caching is ENABLED, and the cache hint is set to "CacheHint.DEFAULT". +# "SPEED" - Based on very limited testing, this option seems to work the best as a workaround for the excessive VRAM usage. +# "QUALITY" +# "SCALE" - This option has been observed to cause graphical errors on several systems: rotated widgets have been observed to be translated instead of rotated. +# "ROTATE" +# "SCALE_AND_ROTATE" +# +# If an invalid option is entered, a warning is logged, and the +# default caching behavior is used (i.e., caching is DISABLED, +# and the cache hint is set to "CacheHint.DEFAULT"). +cache_hint_for_picture_and_symbol_widgets= diff --git a/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialogDemo.java b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialogDemo.java index e5f4b7aa0c..a5f4dc7f2e 100644 --- a/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialogDemo.java +++ b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionDetailsErrorDialogDemo.java @@ -20,7 +20,8 @@ public class ExceptionDetailsErrorDialogDemo extends ApplicationWrapper @Override public void start(final Stage stage) { - ExceptionDetailsErrorDialog.openError("Test", "This is a test\nAnother line", new Exception("This is a test")); + Exception ex = new Exception("This is a test"); + ExceptionDetailsErrorDialog.openError("Test", "This is a test\nAnother line", ex); } public static void main(String[] args) diff --git a/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceDetailsErrorDialogDemo.java b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceDetailsErrorDialogDemo.java new file mode 100644 index 0000000000..9c59926508 --- /dev/null +++ b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceDetailsErrorDialogDemo.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2017 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.phoebus.ui.dialog; + +import javafx.stage.Stage; +import org.phoebus.ui.javafx.ApplicationWrapper; + +/** Demo of the error dialog with a summary of the stack trace + * @author Kay Kasemir, Kunal Shroff + */ +@SuppressWarnings("nls") +public class ExceptionStacktraceDetailsErrorDialogDemo extends ApplicationWrapper +{ + @Override + public void start(final Stage stage) + { + Exception rootCause = new Exception("The ROOT cause of the test exception"); + Exception cause = new Exception("The cause of the test exception", rootCause); + Exception ex = new Exception("This is a test", cause); + ExceptionDetailsErrorDialog.openError("Test", ex); + } + + public static void main(String[] args) + { + launch(ExceptionStacktraceDetailsErrorDialogDemo.class, args); + } +} diff --git a/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceWithNullErrorDialogDemo.java b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceWithNullErrorDialogDemo.java new file mode 100644 index 0000000000..f5b37034a2 --- /dev/null +++ b/core/ui/src/test/java/org/phoebus/ui/dialog/ExceptionStacktraceWithNullErrorDialogDemo.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2017 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + ******************************************************************************/ +package org.phoebus.ui.dialog; + +import javafx.stage.Stage; +import org.phoebus.ui.javafx.ApplicationWrapper; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** Demo of the error dialog, with a summary of the stack trace containing exceptions with null messages and a cyclic + * chain + * @author Kay Kasemir, Kunal Shroff + */ +@SuppressWarnings("nls") +public class ExceptionStacktraceWithNullErrorDialogDemo extends ApplicationWrapper +{ + @Override + public void start(final Stage stage) + { + Exception rootCause = new FileNotFoundException(); + Exception cause = new IOException(); + cause.initCause(rootCause); + rootCause.initCause(cause); + Exception ex = new Exception("This is a test", cause); + ExceptionDetailsErrorDialog.openError(null,"Test", "", ex, true); + } + + public static void main(String[] args) + { + launch(ExceptionStacktraceWithNullErrorDialogDemo.class, args); + } +} diff --git a/core/util/pom.xml b/core/util/pom.xml index 750d1b29b0..6c1c162381 100644 --- a/core/util/pom.xml +++ b/core/util/pom.xml @@ -3,7 +3,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT core-util diff --git a/core/util/src/main/java/org/phoebus/util/FileExtensionUtil.java b/core/util/src/main/java/org/phoebus/util/FileExtensionUtil.java index 4fae31ce6f..d32253f261 100644 --- a/core/util/src/main/java/org/phoebus/util/FileExtensionUtil.java +++ b/core/util/src/main/java/org/phoebus/util/FileExtensionUtil.java @@ -20,6 +20,9 @@ import java.io.File; +/** + * Class FileExtensionUtil + */ public class FileExtensionUtil { /** diff --git a/core/util/src/main/java/org/phoebus/util/indexname/IndexNameHelper.java b/core/util/src/main/java/org/phoebus/util/indexname/IndexNameHelper.java index 2e0b1be44e..34003f3313 100644 --- a/core/util/src/main/java/org/phoebus/util/indexname/IndexNameHelper.java +++ b/core/util/src/main/java/org/phoebus/util/indexname/IndexNameHelper.java @@ -16,7 +16,6 @@ * Helper class for calculating the index name for time based indices. * @author Evan Smith */ -@SuppressWarnings("nls") public class IndexNameHelper { private String baseIndexName; @@ -66,9 +65,9 @@ public IndexNameHelper(final String baseIndexName, final boolean useDatedIndexNa } /** - * Return a time based index name for the given time. If the dateSpanValue is 0 then returns the base index name. - * @param time - * @return index_name + * Return a time based index name for the given time + * @param time given time + * @return index_name If the dateSpanValue is 0 then returns the base index name */ public String getIndexName(Instant time) { @@ -136,11 +135,13 @@ private String parseCurrentDateSpan() return fullDate.split(DELIMITER)[0]; } + /** @return current date span start */ public Instant getCurrentDateSpanStart() { return spanStart; } + /** @return current date span end */ public Instant getCurrentDateSpanEnd() { return spanEnd; diff --git a/core/util/src/main/java/org/phoebus/util/shell/CommandShell.java b/core/util/src/main/java/org/phoebus/util/shell/CommandShell.java index 4ae4f4f213..d035900a8d 100644 --- a/core/util/src/main/java/org/phoebus/util/shell/CommandShell.java +++ b/core/util/src/main/java/org/phoebus/util/shell/CommandShell.java @@ -20,10 +20,11 @@ * * @author Kay Kasemir */ -@SuppressWarnings("nls") public class CommandShell { - public interface CommandHandler + /** + * interface CommandHandler */ + public interface CommandHandler { /** Invoked when user entered a command * @param args Entered command, split at spaces, or null when user entered 'Ctrl-D' to close shell @@ -64,13 +65,16 @@ public void stop() thread.interrupt(); } - /** Get the prompt string. */ + /** @return the prompt string. */ public String getPrompt() { return prompt; } - /** Set the prompt string. */ + /** + * Set the prompt string + * @param newPrompt the new prompt + */ public void setPrompt(final String newPrompt) { this.prompt = newPrompt; diff --git a/core/util/src/main/java/org/phoebus/util/text/CompareNatural.java b/core/util/src/main/java/org/phoebus/util/text/CompareNatural.java index e0d4b65f92..fe7a6f6655 100644 --- a/core/util/src/main/java/org/phoebus/util/text/CompareNatural.java +++ b/core/util/src/main/java/org/phoebus/util/text/CompareNatural.java @@ -17,8 +17,15 @@ */ public class CompareNatural { - public static final Comparator INSTANCE = (a, b) -> compareTo(a, b); - + /** Singleton */ + public static final Comparator INSTANCE = (a, b) -> compareTo(a, b); + + /** + * Compare two string + * @param s1 string 1 + * @param s2 string 2 + * @return result of comparison + */ public static int compareTo(final String s1, final String s2) { // Skip all identical characters diff --git a/core/util/src/main/java/org/phoebus/util/time/SecondsParser.java b/core/util/src/main/java/org/phoebus/util/time/SecondsParser.java index 06812474ac..7e0f7b3ce7 100644 --- a/core/util/src/main/java/org/phoebus/util/time/SecondsParser.java +++ b/core/util/src/main/java/org/phoebus/util/time/SecondsParser.java @@ -34,10 +34,10 @@ public class SecondsParser * Special cases: * One overall '-' is allowed to specify negative seconds. * + * @param text time in string * @return The seconds parsed from the string. * @throws Exception on parse error. */ - @SuppressWarnings("nls") public static double parseSeconds(String text) throws Exception { final char sep = ':'; diff --git a/core/util/src/main/java/org/phoebus/util/time/TimeDuration.java b/core/util/src/main/java/org/phoebus/util/time/TimeDuration.java index 70ad59e419..1e17d7cbaf 100644 --- a/core/util/src/main/java/org/phoebus/util/time/TimeDuration.java +++ b/core/util/src/main/java/org/phoebus/util/time/TimeDuration.java @@ -22,14 +22,20 @@ public class TimeDuration { private TimeDuration() { } + /** + * Create a Duration + * @param sec seconds + * @param nanoSec nanoseconds + * @return duration + */ public static Duration createDuration(long sec, int nanoSec) { return Duration.ofSeconds(sec).plusNanos(nanoSec); } /** - * A new duration in hours. + * A new duration in days. * - * @param hour hours + * @param day day * @return a new duration */ public static Duration ofDays(double day) { @@ -109,7 +115,7 @@ public static double toSecondsDouble(Duration duration){ /** * The number of seconds concatenated with the number of nanoseconds (12.500000000 * for 12.5 seconds). - * + * @param duration the duration * @return the string representation */ public static String toSecondString(Duration duration){ diff --git a/core/util/src/main/java/org/phoebus/util/time/TimeParser.java b/core/util/src/main/java/org/phoebus/util/time/TimeParser.java index f35b7cef9d..63da4f0662 100644 --- a/core/util/src/main/java/org/phoebus/util/time/TimeParser.java +++ b/core/util/src/main/java/org/phoebus/util/time/TimeParser.java @@ -34,7 +34,6 @@ * * @author shroffk */ -@SuppressWarnings("nls") public class TimeParser { /** Text for the relative {@link TemporalAmount} of size 0 */ public static final String NOW = "now"; @@ -74,7 +73,7 @@ public static Instant getInstant(String time) { /** * Return a {@link TimeInterval} between this instant represented by the string and NOW - * @param time + * @param time in string * @return TimeInterval */ @Deprecated @@ -99,8 +98,8 @@ public static TimeInterval getTimeInterval(String time) { * * e.g. parseDuraiton("5h 3min 34s"); * - * @param string - * @return + * @param string time in string + * @return duration * @deprecated use {@link #parseTemporalAmount(String)} */ @Deprecated @@ -368,7 +367,7 @@ else if (period.getDays() > 0) } /** Try to parse text as absolute or relative time - * @param text + * @param text temporal amount * @return {@link Instant}, {@link TemporalAmount} or null */ public static Object parseInstantOrTemporalAmount(final String text) diff --git a/core/util/src/main/java/org/phoebus/util/time/TimeRelativeInterval.java b/core/util/src/main/java/org/phoebus/util/time/TimeRelativeInterval.java index f2bde08797..a7a246774b 100644 --- a/core/util/src/main/java/org/phoebus/util/time/TimeRelativeInterval.java +++ b/core/util/src/main/java/org/phoebus/util/time/TimeRelativeInterval.java @@ -28,7 +28,6 @@ * * @author carcassi, shroffk */ -@SuppressWarnings("nls") public class TimeRelativeInterval { private final Object start; @@ -229,6 +228,10 @@ public Optional getRelativeEnd() { } } + /** + * @param reference time + * @return absolute interval + */ public TimeInterval toAbsoluteInterval(Instant reference) { Instant absoluteStart; Instant absoluteEnd; @@ -288,6 +291,7 @@ public TimeInterval toAbsoluteInterval(Instant reference) { return TimeInterval.between(absoluteStart, absoluteEnd); } + /** @return time interval */ public TimeInterval toAbsoluteInterval() { return toAbsoluteInterval(Instant.now()); } diff --git a/core/util/src/main/java/org/phoebus/util/time/package-info.java b/core/util/src/main/java/org/phoebus/util/time/package-info.java index 3bf5a9505e..71532f7135 100644 --- a/core/util/src/main/java/org/phoebus/util/time/package-info.java +++ b/core/util/src/main/java/org/phoebus/util/time/package-info.java @@ -4,7 +4,7 @@ */ /** * Contains basic common classes to handle time at nanosecond precision. - *

      JSR 310 compatibility

      + * JSR 310 compatibility * Java 8 has introduced a better time definition that is going to be very * similar to these class. The plan is to phase out this package * and use the standard where possible. diff --git a/core/vtype/pom.xml b/core/vtype/pom.xml index 7e0205261e..31d375aea1 100644 --- a/core/vtype/pom.xml +++ b/core/vtype/pom.xml @@ -4,7 +4,7 @@ org.phoebus core - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -24,5 +24,10 @@ 1.3 test + + org.phoebus + core-pva + 4.7.4-SNAPSHOT + diff --git a/app/save-and-restore/common/src/main/java/org/phoebus/applications/saveandrestore/common/VDisconnectedData.java b/core/vtype/src/main/java/org/phoebus/core/vtypes/VDisconnectedData.java similarity index 93% rename from app/save-and-restore/common/src/main/java/org/phoebus/applications/saveandrestore/common/VDisconnectedData.java rename to core/vtype/src/main/java/org/phoebus/core/vtypes/VDisconnectedData.java index c1e248dcee..4c02ab9250 100644 --- a/app/save-and-restore/common/src/main/java/org/phoebus/applications/saveandrestore/common/VDisconnectedData.java +++ b/core/vtype/src/main/java/org/phoebus/core/vtypes/VDisconnectedData.java @@ -15,7 +15,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package org.phoebus.applications.saveandrestore.common; +package org.phoebus.core.vtypes; import org.epics.vtype.VType; @@ -26,7 +26,7 @@ * @author Jaka Bobnar * */ -public final class VDisconnectedData extends VType{ +public final class VDisconnectedData extends VType { private static final long serialVersionUID = -2399970529728581034L; diff --git a/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java b/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java index d746b57416..843db11ae8 100644 --- a/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java +++ b/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java @@ -8,11 +8,13 @@ package org.phoebus.core.vtypes; import java.time.Instant; +import java.util.ArrayList; import java.util.List; +import java.util.Collection; -import org.epics.util.array.ListBoolean; -import org.epics.util.array.ListInteger; -import org.epics.util.array.ListNumber; +import org.epics.util.array.*; +import org.epics.pva.data.*; +import org.epics.pva.data.nt.PVATable; import org.epics.vtype.Alarm; import org.epics.vtype.AlarmSeverity; import org.epics.vtype.Array; @@ -506,4 +508,119 @@ public static boolean toBoolean(VType vtype) { } return false; } + /** + * @param name Data item name + * @param object The object subject to conversion + * @return Converted object + */ + public static Object toPVArrayType(String name, Object object) { + if (object instanceof ListBoolean) { + ListBoolean listBoolean = (ListBoolean) object; + boolean[] booleans = new boolean[listBoolean.size()]; + for (int i = 0; i < listBoolean.size(); i++) { + booleans[i] = listBoolean.getBoolean(i); + } + return new PVABoolArray(name, booleans); + } else if (object instanceof ListNumber) { + ListNumber listNumber = (ListNumber) object; + if (object instanceof ArrayByte || object instanceof ArrayUByte) { + byte[] bytes = new byte[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + bytes[i] = listNumber.getByte(i); + } + return new PVAByteArray(name, object instanceof ArrayUByte, bytes); + } else if (object instanceof ArrayShort || object instanceof ArrayUShort) { + short[] shorts = new short[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + shorts[i] = listNumber.getShort(i); + } + return new PVAShortArray(name, object instanceof ArrayUShort, shorts); + } else if (object instanceof ArrayInteger || object instanceof ArrayUInteger) { + int[] ints = new int[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + ints[i] = listNumber.getInt(i); + } + return new PVAIntArray(name, object instanceof ArrayUInteger, ints); + } else if (object instanceof ArrayLong || object instanceof ArrayULong) { + long[] longs = new long[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + longs[i] = listNumber.getLong(i); + } + return new PVALongArray(name, object instanceof ArrayULong, longs); + } else if (object instanceof ArrayFloat) { + float[] floats = new float[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + floats[i] = listNumber.getFloat(i); + } + return new PVAFloatArray(name, floats); + } else if (object instanceof ArrayDouble) { + double[] doubles = new double[listNumber.size()]; + for (int i = 0; i < listNumber.size(); i++) { + doubles[i] = listNumber.getDouble(i); + } + return new PVADoubleArray(name, doubles); + } else { + throw new IllegalArgumentException("Conversion of type " + object.getClass().getCanonicalName() + " not supported"); + } + } else { // Assume this always is for string arrays + Collection list = (Collection) object; + String[] strings = new String[list.size()]; + strings = list.toArray(strings); + return new PVAStringArray(name, strings); + } + } + + /** + * Extracts the raw value from the given data object. The raw value is either one of the primitive wrappers or some + * kind of a list type if the value is an {@link Array}. + * + * @param type the value to extract the raw data from + * @return the raw data + */ + public static Object toObject(VType type) { + if (type == null) { + return null; + } + if (type instanceof VNumberArray) { + if (type instanceof VIntArray || type instanceof VUIntArray) { + return VTypeHelper.toIntegers(type); + } else if (type instanceof VDoubleArray) { + return VTypeHelper.toDoubles(type); + } else if (type instanceof VFloatArray) { + return VTypeHelper.toFloats(type); + } else if (type instanceof VLongArray || type instanceof VULongArray) { + return VTypeHelper.toLongs(type); + } else if (type instanceof VShortArray || type instanceof VUShortArray) { + return VTypeHelper.toShorts(type); + } else if (type instanceof VByteArray || type instanceof VUByteArray) { + return VTypeHelper.toBytes(type); + } + } else if (type instanceof VEnumArray) { + var indexes = ((VEnumArray) type).getIndexes(); + var array = new int[indexes.size()]; + return indexes.toArray(array); + } else if (type instanceof VStringArray) { + List data = ((VStringArray) type).getData(); + return data.toArray(new String[data.size()]); + } else if (type instanceof VBooleanArray) { + return VTypeHelper.toBooleans(type); + } else if (type instanceof VNumber) { + return ((VNumber) type).getValue(); + } else if (type instanceof VEnum) { + return ((VEnum) type).getIndex(); + } else if (type instanceof VString) { + return ((VString) type).getValue(); + } else if (type instanceof VBoolean) { + return ((VBoolean) type).getValue(); + } else if (type instanceof VTable) { + VTable vTable = (VTable) type; + int columnCount = vTable.getColumnCount(); + List dataArrays = new ArrayList(); + for (int i = 0; i < columnCount; i++) { + dataArrays.add(toPVArrayType("Col " + i, vTable.getColumnData(i))); + } + return new PVAStructure(PVATable.STRUCT_NAME, "", dataArrays); + } + return null; + } } diff --git a/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java b/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java index 50f43eaa78..6c7f6d1393 100644 --- a/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java +++ b/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java @@ -22,43 +22,21 @@ import org.epics.util.array.ListUInteger; import org.epics.util.array.ListULong; import org.epics.util.array.ListUShort; -import org.epics.vtype.Alarm; -import org.epics.vtype.Array; -import org.epics.vtype.Display; -import org.epics.vtype.EnumDisplay; -import org.epics.vtype.Time; -import org.epics.vtype.VBoolean; -import org.epics.vtype.VBooleanArray; -import org.epics.vtype.VByteArray; -import org.epics.vtype.VDouble; -import org.epics.vtype.VDoubleArray; -import org.epics.vtype.VEnum; -import org.epics.vtype.VEnumArray; -import org.epics.vtype.VFloatArray; -import org.epics.vtype.VInt; -import org.epics.vtype.VIntArray; -import org.epics.vtype.VLongArray; -import org.epics.vtype.VNumber; -import org.epics.vtype.VNumberArray; -import org.epics.vtype.VShortArray; -import org.epics.vtype.VStatistics; -import org.epics.vtype.VString; -import org.epics.vtype.VStringArray; -import org.epics.vtype.VTable; -import org.epics.vtype.VType; -import org.epics.vtype.VUIntArray; -import org.epics.vtype.VULongArray; -import org.epics.vtype.VUShortArray; -import org.junit.jupiter.api.Test; + +import org.epics.vtype.*; +import org.epics.pva.data.*; import java.time.Instant; import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; public class VTypeHelperTest { @@ -564,4 +542,98 @@ public void testArrayToBooleansWithValidIndex() { assertTrue(VTypeHelper.toBooleans(VDouble.of(7.0, alarm, time, display)).length == 0); } + + @Test + public void testToObject() { + Alarm alarm = Alarm.none(); + Display display = Display.none(); + Time time = Time.now(); + + assertNull(VTypeHelper.toObject(null)); + + VType val = VDouble.of(5d, alarm, time, display); + Object d = VTypeHelper.toObject(val); + assertTrue(d instanceof Double); + assertEquals(5.0, d); + + val = VFloat.of(5f, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Float); + assertEquals(5.0f, d); + + val = VLong.of(5L, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Long); + assertEquals(5L, d); + + val = VInt.of(5, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Integer); + assertEquals(5, d); + + val = VShort.of((short) 5, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Short); + assertEquals((short) 5, d); + + val = VByte.of((byte) 5, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Byte); + assertEquals((byte) 5, d); + + val = VEnum.of(1, EnumDisplay.of("first", "second", "third"), alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Integer); + assertEquals(1, d); + + val = VString.of("third", alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof String); + assertEquals("third", d); + + ArrayDouble arrayDouble = ArrayDouble.of(1, 2, 3, 4, 5); + val = VDoubleArray.of(arrayDouble, alarm, time, display); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof double[]); + for (int i = 0; i < ((double[]) d).length; i++) { + assertEquals(arrayDouble.getDouble(i), ((double[]) d)[i], 0); + } + + val = VStringArray.of(Arrays.asList("a", "b", "c"), alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof String[]); + + val = VBooleanArray.of(ArrayBoolean.of(true, false, true), alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof boolean[]); + assertTrue(((boolean[]) d)[0]); + assertFalse(((boolean[]) d)[1]); + + val = VEnumArray.of(ArrayInteger.of(0, 1, 2, 3, 4), EnumDisplay.of("a", "b", "c", "d", "e"), alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof int[]); + assertEquals(0, ((int[]) d)[0]); + assertEquals(4, ((int[]) d)[4]); + + val = VBoolean.of(true, alarm, time); + d = VTypeHelper.toObject(val); + assertTrue(d instanceof Boolean); + assertTrue(((Boolean) d)); + + List> types = Arrays.asList(Integer.TYPE, Integer.TYPE, Integer.TYPE); + List values = Arrays.asList(ArrayInteger.of(-1, 2, 3), ArrayInteger.of(1, 2, 3), ArrayUInteger.of(11, 22, 33)); + List names = Arrays.asList("a", "b", "c"); + VTable vTable = VTable.of(types, names, values); + + d = VTypeHelper.toObject(vTable); + assertInstanceOf(PVAStructure.class, d); + PVAStructure pvaStructure = (PVAStructure) d; + assertEquals(3, pvaStructure.get().size()); + assertInstanceOf(PVAIntArray.class, pvaStructure.get().get(0)); + assertInstanceOf(PVAIntArray.class, pvaStructure.get().get(1)); + assertInstanceOf(PVAIntArray.class, pvaStructure.get().get(2)); + + assertNull(VTypeHelper.toObject(VDisconnectedData.INSTANCE)); + } + } \ No newline at end of file diff --git a/dependencies/ant_settings.xml b/dependencies/ant_settings.xml index e7028ca21f..5ef8f50726 100644 --- a/dependencies/ant_settings.xml +++ b/dependencies/ant_settings.xml @@ -1,7 +1,7 @@ - + @@ -52,23 +52,27 @@ - + + - + - + + + + - + - + - + - + - + - + - + - + - --> diff --git a/dependencies/phoebus-target/pom.xml b/dependencies/phoebus-target/pom.xml index 9ca9f62b97..b7df3148d1 100644 --- a/dependencies/phoebus-target/pom.xml +++ b/dependencies/phoebus-target/pom.xml @@ -3,7 +3,7 @@ org.phoebus dependencies - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT phoebus-target @@ -65,7 +65,7 @@ org.phoebus install-jars - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.junit.jupiter @@ -353,7 +353,7 @@ org.controlsfx controlsfx - 11.0.3 + 11.1.2 org.openjfx @@ -501,7 +501,22 @@ org.eclipse.jgit org.eclipse.jgit - 5.0.3.201809091024-r + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.archive + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.ssh.jsch + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 6.6.0.202305301015-r diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 3e4506aa9f..aa979143e2 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -5,7 +5,7 @@ org.phoebus parent - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT install-jars diff --git a/docs/source/applications.rst b/docs/source/applications.rst deleted file mode 100644 index 3ef06e77ac..0000000000 --- a/docs/source/applications.rst +++ /dev/null @@ -1,33 +0,0 @@ -Applications -============ - -The following sections describe details of specific application features. - - -.. toctree:: - :maxdepth: 1 - - core/ui/doc/index - core/pv/doc/index - core/framework/doc/index - core/formula/doc/index - app/perfmon/doc/index - app/credentials-management/doc/index - app/errlog/doc/index - app/update/doc/index - app/logbook/olog/ui/doc/index - app/databrowser/doc/index - app/3d-viewer/doc/index - app/pace/doc/index - app/log-configuration/doc/index - app/save-and-restore/app/doc/index - app/channel/views/doc/index - app/imageviewer/doc/index - app/display/navigation/doc/index - app/display/convert-medm/doc/index - app/display/editor/doc/index - app/pvtable/doc/index - app/alarm/ui/doc/index - app/alarm/datasource/doc/index - app/alarm/logging-ui/doc/index - app/pvtree/doc/index diff --git a/docs/source/architecture.rst b/docs/source/architecture.rst index a4460ef694..191730307a 100644 --- a/docs/source/architecture.rst +++ b/docs/source/architecture.rst @@ -1,7 +1,7 @@ Architecture ============ -.. figure:: architecture.png +.. figure:: phoebus_architecture.png The fundamental phoebus architecture consists of **core** modules, user-interface related **core-ui** modules, and **app** modules. diff --git a/docs/source/eclipse_debugging.rst b/docs/source/eclipse_debugging.rst index e49135da79..2e3277e5ca 100644 --- a/docs/source/eclipse_debugging.rst +++ b/docs/source/eclipse_debugging.rst @@ -16,7 +16,7 @@ Check Eclipse Preferences:: Debugging with Eclipse -This assumes the project has been imported as a maven project into Eclipse(see instructions in README):: +This assumes the project has been imported as a maven project into Eclipse (see instructions in README):: 1. Open Eclipse 2. Go to `Run->External Tools->External Run COnfigurations` @@ -29,6 +29,12 @@ This assumes the project has been imported as a maven project into Eclipse(see i --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar path_to_repo/phoebus/phoebus-product/target/product-4.6.6-SNAPSHOT.jar ``` + or + ``` + --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true + -agentlib:jdwp=transport=dt_socket,server=y,address=0.0.0.0:5005,suspend=n -jar path_to_repo/phoebus/phoebus-product/target/product-4.6.6-SNAPSHOT.jar + ``` + 6. Click `Run`. The Eclipse console should output a port number. Write it down; we'll use it for debugging later on. 7. Go to `Debug Configurations` diff --git a/docs/source/index.rst b/docs/source/index.rst index a8da225ca0..49f500a3a7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,6 +22,7 @@ User Documentation: authorization applications services + services_architecture Developer Documentation: @@ -37,6 +38,7 @@ Developer Documentation: preference_properties changelog docker + trouble_shooting eclipse_debugging gui_testing diff --git a/docs/source/phoebus_architecture.png b/docs/source/phoebus_architecture.png new file mode 100644 index 0000000000..0a622625ec Binary files /dev/null and b/docs/source/phoebus_architecture.png differ diff --git a/docs/source/preference_properties.rst b/docs/source/preference_properties.rst deleted file mode 100644 index 63005796cb..0000000000 --- a/docs/source/preference_properties.rst +++ /dev/null @@ -1,1846 +0,0 @@ -:orphan: - -.. _preference_settings: - -Preferences Listing -=================== - -The following preference settings are available for the various application features. -To use them in your settings file, remember to prefix each setting with the package name. - - -alarm ------ - -File ../../app/alarm/model/src/main/resources/alarm_preferences.properties:: - - # -------------------------------------- - # Package org.phoebus.applications.alarm - # -------------------------------------- - - # Kafka Server host:port - server=localhost:9092 - - # A file to configure the properites of kafka clients - kafka_properties= - - # Name of alarm tree root - config_name=Accelerator - - # Names of selectable alarm configurations - # The `config_name` will be used as the default for newly opened tools, - # and if `config_names` is empty, it remains the only option. - # When one or more comma-separated configurations are listed, - # the UI shows the selected name and allows switching - # between them. - config_names=Accelerator, Demo - - # Timeout in seconds for initial PV connection - connection_timeout=30 - - - ## Area Panel - - # Item level for alarm area view: - # 1 - Root element - # 2 - Top-level "area" elements just below root - # 3 - Show all the items at level 3 - alarm_area_level=2 - - # Number of columns in the alarm area view - alarm_area_column_count=3 - - # Gap between alarm area panel items - alarm_area_gap=5 - - # Font size for the alarm area view - alarm_area_font_size=15 - - # Limit for the number of context menu items. - # Separately applied to the number of 'guidance', - # 'display' and 'command' menu entries. - alarm_menu_max_items=10 - - # Initial Alarm Tree UI update delay [ms] - # - # The initial flurry of alarm tree updates can be slow - # to render. By allowing the alarm client to accumulate - # alarm tree information for a little time and then - # performing an initial bulk representation, the overall - # alarm tree startup can be faster, especially when - # the UI is viewed via a remote desktop - # - # Set to 0 for original implementation where - # all alarm tree items are added to the model - # as they are received in initial flurry of updates. - alarm_tree_startup_ms=2000 - - # Order of columns in alarm table - # Allows re-ordering as well as omitting columns - alarm_table_columns=Icon, PV, Description, Alarm Severity, Alarm Status, Alarm Time, Alarm Value, PV Severity, PV Status - - # By default, the alarm table uses the common alarm severity colors - # for both the text color and the background of cells in the "Severity" column. - # - # Older implementations always used the background to indicate alarm severity, - # and this options emulates that by using the alarm severity text(!) color - # for the background, automatically using black or white for the text - # based on brightness. - alarm_table_color_legacy_background=true - - # Alarm table row limit - # If there are more rows, they're suppressed - alarm_table_max_rows=2500 - - # Directory used for executing commands - # May use Java system properties like this: $(prop_name) - command_directory=$(user.home) - - # The threshold of messages that must accumulate before the annunciator begins to simply state: "There are X Alarm messages." - annunciator_threshold=3 - - # The number of messages the annunciator will retain before popping messages off the front of the message queue. - annunciator_retention_count=100 - - # Timeout in seconds at which server sends idle state updates - # for the 'root' element if there's no real traffic. - # Client will wait 3 times this long and then declare a timeout. - idle_timeout=10 - - # Name of the sender, the 'from' field of automated email actions - automated_email_sender=Alarm Notifier - - # Comma-separated list of automated actions on which to follow up - # Options include mailto:, cmd: - automated_action_followup=mailto:, cmd: - - # Optional heartbeat PV - # When defined, alarm server will set it to 1 every heartbeat_secs - #heartbeat_pv=Demo:AlarmServerHeartbeat - heartbeat_pv= - - # Heartbeat PV period in seconds - heartbeat_secs=10 - - # Period for repeated annunciation - # - # If there are active alarms, i.e. alarms that have not been acknowleded, - # a message "There are 47 active alarms" will be issued - # - # Format is HH:MM:SS, for example 00:15:00 to nag every 15 minutes. - # Set to 0 to disable - nag_period=00:15:00 - - # Connection validation period in seconds - # - # Server will check the Kafka connection at this period. - # After re-establishing the connection, it will - # re-send the state of every alarm tree item. - # Set to 0 to disable. - connection_check_secs=5 - - # To turn on disable notifications feature, set the value to true - disable_notify_visible=false - - # Options for the "Disable until.." shortcuts in the PV config dialog - # - # Comma separated, each option needs to comply with TimeParser.parseTemporalAmount(): - # 30 seconds, 5 minutes, 1 hour, 6 hours, 1 day, 30 days, ... - shelving_options=1 hour, 6 hours, 12 hours, 1 day, 7 days, 30 days - - # Macros for UI display, command or web links - # - # Format: M1=Value1, M2=Value2 - macros=TOP=/home/controls/displays,WEBROOT=http://localhost/controls/displays - - -alarm.logging.ui ----------------- - -File ../../app/alarm/logging-ui/src/main/resources/alarm_logging_preferences.properties:: - - # ------------------------------------------------- - # Package org.phoebus.applications.alarm.logging.ui - # ------------------------------------------------- - - service_uri = http://localhost:9000 - results_max_size = 10000 - - -archive -------- - -File ../../services/archive-engine/src/main/resources/archive_preferences.properties:: - - # ---------------------------- - # Package org.csstudio.archive - # ---------------------------- - - # RDB URL for archived data - # - # Oracle example - # url=jdbc:oracle:thin:user/password@//172.31.73.122:1521/prod - # - # PostgreSQL example - # url=jdbc:postgresql://localhost/archive - # - # MySQL example - url=jdbc:mysql://localhost/archive?rewriteBatchedStatements=true - - # RDB user and password - # Some applications also provide command-line option to override. - user=archive - password=$archive - - # Schema name. Used with an added "." as prefix for table names. - # For now this is only used with Oracle URLs and ignored for MySQL - schema= - - # Timeout [seconds] for certain SQL queries - # Fundamentally, the SQL queries for data take as long as they take - # and any artificial timeout just breaks queries that would otherwise - # have returned OK few seconds after the timeout. - # We've seen Oracle lockups, though, that caused JDBC to hang forever - # because the SAMPLE table was locked. No error/exception, just hanging. - # A timeout is used for operations other than getting the actual data, - # for example the channel id-by-name query which _should_ return within - # a shot time, to catch that type of RDB lockup. - # timeout_secs=120 - # With PostgreSQL, the setQueryTimeout API is not implemented, - # and calling it results in an exception. - # Setting the timeout to 0 disables calls to setQueryTimeout. - timeout_secs=0 - - # Use a blob to read/write array samples? - # - # The original SAMPLE table did not contain an ARRAY_VAL column - # for the array blob data, but instead used a separate ARRAY_VAL table. - # When running against an old database, this parameter must be set to false. - use_array_blob=true - - # Name of sample table for writing - write_sample_table=sample - - # Maximum length of text samples written to SAMPLE.STR_VAL - max_text_sample_length=80 - - # Use postgres copy instead of insert - use_postgres_copy=false - - # Channel names use a prefix ca://, pva://, loc://, ... - # to select the type of PV or network protocol. - # The preference setting - # - # org.phoebus.pv/default=ca - # - # determines the default type when no prefix is provided. - # - # With EPICS IOCs from release 7 on, the PVs - # "xxx", "ca://xxx" and "pva://xxx" all refer - # to the same record "xxx" on the IOC. - # - # The archive configuration stores the PV name as given. - # It is used as such when connecting to the live data source, - # resulting in "ca://.." or "pva://.." connections as requested. - # Samples are written to the archive under that channel name. - # - # This archive engine preference setting establishes one or more prefixes - # as equal when importing an engine configuration. - # For example, assume - # - # equivalent_pv_prefixes=ca, pva - # - # When adding a PV "pva://xxx" to the configuration, - # we check if the archive already contains a channel "xxx", "ca://xxx" or "pva://xxx". - # If any of them are found, the `-import` will consider "pva://xxx" as a duplicate. - # - # When importing a PV "pva://xxx" into a sample engine configuration that already - # contains the channel "ca://xxx" or "xxx", the channel will be renamed, - # so that engine will from now on use "pva://xxx". - # - # When importing a PV "pva://xxx" into a configuration that already - # contains a different engine setup with the channel "ca://xxx" or "xxx", - # the channel will by default rename unchanged, so "ca://xxx" or "xxx" - # will remain in their original engine setup, "pva://xxx" will be skipped. - # - # When using `-import` with the additional `-steal_channels` option, - # the existing "...xxx" channel will be renamed to "pva://xxx" and moved - # to the imported engine configuration. - # - # When `equivalent_pv_prefixes` is empty, - # any PV name is used as is without looking for equivalent names. - # So "xxx", "ca://xxx" and "pva://xxx" can then all be imported - # as separate channels, which is likely wrong because it would simply - # store data from the same underlying record more than once. - # - # This default should be the most practical setting when adding - # EPICS 7 IOCs and starting to transition towards "pva://..". - # Existing "xxx" or "ca://xxx" channels can thus be renamed - # to "pva://xxx" while retaining their sample history. - # - # Note that the data browser has a similar `equivalent_pv_prefixes` - # setting to search for a channel name in several variants. - equivalent_pv_prefixes=ca, pva - - # Seconds between log messages for Not-a-Number, futuristic, back-in-time values, buffer overruns - # 24h = 24*60*60 = 86400 - log_trouble_samples=86400 - log_overrun=86400 - - # Write period in seconds - write_period=30 - - # Maximum number of repeat counts for scanned channels - max_repeats=60 - - # Write batch size - batch_size=500 - - # Buffer reserve (N times what's ideally needed) - buffer_reserve=2.0 - - # Samples with time stamps this far ahead of the local time - # are ignored - # 24*60*60 = 86400 = 1 day - ignored_future=86400 - - -archive.reader.appliance ------------------------- - -File ../../app/databrowser/src/main/resources/appliance_preferences.properties:: - - # ---------------------------------------- - # Package org.phoebus.archive.reader.appliance - # ---------------------------------------- - - useStatisticsForOptimizedData=true - useNewOptimizedOperator=true - - # Use 'https://..' instead of plain 'http://..' ? - useHttps=false - - -archive.reader.channelarchiver ------------------------------- - -File ../../app/databrowser/src/main/resources/channelarchiver_preferences.properties:: - - # -------------------------------------------------- - # Package org.phoebus.archive.reader.channelarchiver - # -------------------------------------------------- - - # Use 'https://..' instead of plain 'http://..' ? - use_https=false - - -archive.reader.rdb ------------------- - -File ../../app/databrowser/src/main/resources/archive_reader_rdb_preferences.properties:: - - --------------------------------------- - # Package org.phoebus.archive.reader.rdb - # -------------------------------------- - - # User and password for reading archived data - user=archive - password=$archive - - # Table prefix - # For Oracle, this is typically the schema name, - # including "." - prefix= - - # Timeout [seconds] for certain SQL queries - # Fundamentally, the SQL queries for data take as long as they take - # and any artificial timeout just breaks queries that would otherwise - # have returned OK a few seconds after the timeout. - # We've seen Oracle lockups, though, that caused JDBC to hang forever - # because the SAMPLE table was locked. No error/exception, just hanging. - # A timeout is used for operations other than getting the actual data, - # for example the channel id-by-name query which _should_ return within - # a shot time, to catch that type of RDB lockup. - timeout_secs=120 - # Setting the timeout to 0 disables calls to setQueryTimeout, - # which may be required for PostgreSQL where the setQueryTimeout API is not implemented. - # timeout_secs=0 - - - # Use a BLOB to read array samples? - # - # The original SAMPLE table did not contain an ARRAY_VAL column - # for the array blob data, but instead used a separate ARRAY_VAL table. - # When running against an old database, this parameter must be set to false. - use_array_blob=true - - # Use stored procedures and functions for 'optimized' data readout? - # Set to procedure name, or nothing to disable stored procedure. - stored_procedure= - starttime_function= - - # MySQL: - # stored_procedure=archive.get_browser_data - - # PostgreSQL - # stored_procedure=public.get_browser_data - - # Oracle: - # stored_procedure=chan_arch.archive_reader_pkg.get_browser_data - # starttime_function=SELECT chan_arch.archive_reader_pkg.get_actual_start_time (?, ?, ?) FROM DUAL - - - # JDBC Statement 'fetch size': - # Number of samples to read in one network transfer. - # - # For Oracle, the default is 10. - # Tests resulted in a speed increase up to fetch sizes of 1000. - # On the other hand, bigger numbers can result in java.lang.OutOfMemoryError. - fetch_size=1000 - - -archive.ts ----------- - -File ../../app/databrowser-timescale/src/main/resources/archive_ts_preferences.properties:: - - -------------------------------- - # Package org.csstudio.archive.ts - # ------------------------------- - - # User and password for reading archived data - user=report - password=$report - - # Timeout [seconds] for certain SQL queries, 0 to disable timeout. - # Fundamentally, the SQL queries for data take as long as they take - # and any artificial timeout just breaks queries that would otherwise - # have returned OK a few seconds after the timeout. - # A timeout is used for operations other than getting the actual data, - # for example the channel id-by-name query which _should_ return within - # a short time. - timeout_secs=120 - - # JDBC Statement 'fetch size': - # Number of samples to read in one network transfer. - # Speed tends to increase with fetch size. - # On the other hand, bigger numbers can result in java.lang.OutOfMemoryError. - fetch_size=10000 - - -channel.views.ui ----------------- - -File ../../app/channel/views/src/main/resources/cv_preferences.properties:: - - # -------------------------------------- - # Package org.phoebus.channel.views.ui - # -------------------------------------- - - # Show the active PVs only - show_active_cb=false - - -channelfinder -------------- - -File ../../app/channel/channelfinder/src/main/resources/channelfinder_preferences.properties:: - - # ---------------------------------------- - # Package org.phoebus.channelfinder - # ---------------------------------------- - - serviceURL=http://localhost:8080/ChannelFinder - username=admin - password=adminPass - - rawFiltering=false - - -console -------- - -File ../../app/console/src/main/resources/console_preferences.properties:: - - # ---------------------------------------- - # Package org.phoebus.applications.console - # ---------------------------------------- - - # Number of output lines to keep. - # Older output is dropped. - output_line_limit=100 - - # Number of lines to keep in input history, - # accessible via up/down cursor keys - history_size=20 - - # Font name and size - font_name=Liberation Mono - font_size=14 - - # Prompt (may include trailing space) - prompt=>>>\ - - # Prompt (input field) info - prompt_info=Enter console command - - # 'Shell' to execute. - # - # Examples: - # /usr/bin/python -i - # /usr/bin/python -i /path/to/some/initial_file.py - # /bin/bash - # - # Value may include properties. - shell=/usr/bin/python -i - - # Folder where the shell process should be started - # - # Value may include properties. - directory=$(user.home) - - -display.builder.editor ----------------------- - -File ../../app/display/editor/src/main/resources/display_editor_preferences.properties:: - - # ---------------------------------------- - # Package org.csstudio.display.builder.editor - # ---------------------------------------- - - # Widget types to hide from the palette - # - # Comma separated list of widget types that will not be shown - # in the palette. - # Existing displays that use these widgets can still be edited - # and executed, but widgets do not appear in the palette to - # discourage adding them to new displays. - - # Hiding widgets where representation has not been imported because of dependencies - hidden_widget_types=linear-meter,knob,gauge,clock,digital_clock - # - # - # GUI Menu action Applications / Display / New Display opens the following template - new_display_template=examples:/initial.bob - - # Size of undo stack. Defaults to 50 if not set. - undo_stack_size=50 - - -display.builder.model ---------------------- - -File ../../app/display/model/src/main/resources/display_model_preferences.properties:: - - # ---------------------------------------- - # Package org.csstudio.display.builder.model - # ---------------------------------------- - - - # Widget classes - # One or more *.bcf files, separated by ';' - # Defaults to built-in copy of examples/classes.bcf - class_files=examples:classes.bcf - - # Named colors - # One or more *.def files, separated by ';' - # Defaults to built-in copy of examples/color.def - color_files=examples:color.def - - # Named fonts - # One or more *.def files, separated by ';' - # Defaults to built-in copy of examples/font.def - font_files=examples:font.def - - # Global macros, used for all displays. - # - # Displays start with these macros, - # and can then add new macros or overwrite - # the values of these macros. - # - # Format: - # Entries where the XML tag name is the macro name, - # and the XML content is the macro value. - # The macro name must be a valid XML tag name: - # * Must start with character - # * May then contain characters or numbers - # * May also contain underscores - # - macros=Value from Preferencestrue - - - # Timeout [ms] for loading files: Displays, but also color, font, widget class files - read_timeout=10000 - - # Timeout [sec] for caching files loaded from a URL - cache_timeout=60 - - - # 'BOY' *.opi files provide the font size in 'points'. - # All other positions and sizes are in 'pixels'. - # A point is meant to represent 1/72th of an inch. - # The actual on-screen size display settings. - # Plugging a different monitor into the computer can - # potentially change the DPI settings of the graphics driver, - # resulting in different font sizes. - # The display builder uses fonts in pixels to avoid such changes. - # - # When reading legacy display files, we do not know the DPI - # scaling that was used to create the display. - # This factor is used to translate legacy font sizes - # from 'points' into 'pixel': - # - # legacy_points = pixel * legacy_font_calibration - # - # The test program - # org.csstudio.display.builder.representation.swt.SWTFontCalibation - # can be used to obtain the factor when executed on the original - # platform where the legacy display files were created. - # - # When loading legacy files, - # _increasing_ the legacy_font_calibration will - # result in _smaller_ fonts in the display builder - legacy_font_calibration=1.01 - - # Maximum re-parse operations - # - # When reading legacy *.opi files and for example - # finding a "TextUpdate" widget that has no , - # it will be changed into a "Label" widget and then re-parsed. - # If more than a certain number of re-parse operations are triggered - # within one 'level' of the file (number of widgets at the root of the display, - # or number of childred for a "Group" widget), - # the parser assumes that it entered an infinite re-parse loop - # and aborts. - max_reparse_iterations=5000 - - # When writing a display file, skip properties that are still at default values? - skip_defaults=true - - -display.builder.representation ------------------------------- - -File ../../app/display/representation/src/main/resources/display_representation_preferences.properties:: - - # --------------------------------------------------- - # Package org.csstudio.display.builder.representation - # --------------------------------------------------- - - ## Representation Tuning - # - # The representation 'throttles' updates to widgets. - # When a widget requests an update, a little accumulation time - # allows more updates to accumulate before actually performing - # the queued update requests on the UI thread. - # - # An update delay then suppresses further updates to prevent - # flooding the UI thread. - # - # Update runs that last longer than a threshold can be logged - - # Time waited after a trigger to allow for more updates to accumulate - update_accumulation_time = 20 - - # Pause between updates to prevent flooding the UI thread - update_delay = 100 - - # Period in seconds for logging update performance - performance_log_period_secs = 5 - - # UI thread durations above this threshold are logged - performance_log_threshold_ms = 20 - - # Pause between updates of plots (XY, lines) - # Limit to 250ms=4 Hz - plot_update_delay = 250 - - # Pause between updates of image plots - # Limit to 250ms=4 Hz - image_update_delay = 250 - - # Length limit for tool tips - # Tool tips that are too long can be a problem - # on some window systems. - tooltip_length=150 - - # Timeout for load / unload of Embedded Widget content [ms] - embedded_timeout=5000 - - -display.builder.representation.javafx -------------------------------------- - -File ../../app/display/representation-javafx/src/main/resources/jfx_repr_preferences.properties:: - - # ---------------------------------------------------------- - # Package org.csstudio.display.builder.representation.javafx - # ---------------------------------------------------------- - - # When clicking on the 'slider' widget 'track', - # should the value increment/decrement, - # matching the behavior of EDM, BOY, ...? - # Otherwise, jump to the clicked value right away. - inc_dec_slider=true - - # How does mouse need to hover until tool tip appears? - tooltip_delay_ms=250 - - # Once displayed, how long does the tool tip remain visible? - tooltip_display_sec=30 - - # Note that for historic reasons tool tips are also influenced - # by the property `org.csstudio.display.builder.disable_tooltips`. - # When `true`, tool tips are disabled. - - -display.builder.runtime ------------------------ - -File ../../app/display/runtime/src/main/resources/display_runtime_preferences.properties:: - - # -------------------------------------------- - # Package org.csstudio.display.builder.runtime - # -------------------------------------------- - - # Search path for Jython scripts used by the display runtime. - # Note that format depends on the OS. - # On UNIX systems, path entries are separated by ':', on Windows by ';'. - # python_path=/home/controls/displays/scripts:/home/fred/my_scripts - python_path= - - # PV Name Patches - # - # Translate PV names based on regular expression pattern and replacement - # - # Format: pattern@replacement@pattern@replacement - # - # Setting must contain a sequence of pattern & replacement pairs, - # all separated by '@'. - # - # The regular expression for the pattern can includes "( )" groups, - # which are then used in the replacement via "$1", "$2", .. - # - # If the item separator character '@' itself is required within the pattern or replacement, - # use '[@]' to distinguish it from the item separator, i.e. - # - # [@]work@[@]home - # - # will patch "be@work" -> "be@home" - # - # Patches are applied in the order they're listed in the preference, i.e. - # later patches are applied to names already patched by earlier ones. - # - # Example: - # Remove PVManager's longString modifier, 'some_pv {"longString":true}' -> 'some_pv' - # turn constant formula into constant local variable, '=42' -> 'loc://const42(42)' - # as well as constant name into constant local var, '="Fred"' -> 'loc://strFred("Fred")' - pv_name_patches=\\{"longString":true\\}"@@^="([a-zA-Z]+)"@loc://str$1("$1") - - # PV update throttle in millisecs - # 250ms = 4 Hz - update_throttle=250 - - # "Probe Display" - # Added to context menu for ProcessVariables, - # invoked with macro PV set to the PV name. - # When left empty, the "Probe Display" - # context menu entry is disabled. - probe_display=examples:/probe.bob - - -display.converter.edm ---------------------- - -File ../../app/display/convert-edm/src/main/resources/edm_converter_preferences.properties:: - - # ------------------------------------------ - # Package org.csstudio.display.converter.edm - # ------------------------------------------ - - # Path to the directory where the auto-converter will - # generate auto-converted files. - # May include system properties like $(user.home). - # Target directory must be in the file system. - # The folder is created if it doesn't exist. - # - # When left empty, the auto-converter is disabled. - auto_converter_dir= - - # Path (prefix) that will be stripped from the original - # EDM file name before converting. - # When empty, the complete path will be stripped. - # - # For example, assume we need to convert - # /path/to/original/vacuum/segment1/vac1.edl - # - # With an empty auto_converter_strip, - # this will be converted into {auto_converter_dir}/vac1.edl - # - # With auto_converter_strip=/path/to/original, - # it will be converted into {auto_converter_dir}/vacuum/segment1/vac1.edl - auto_converter_strip= - - # EDM colors.list file - # Must be defined to use converter. - # May be a file system path or http:/.. link - colors_list= - - # Font mappings - # - # Format: EDMFontPattern=DisplayBuilderFont,Pattern=Font,... - # EDMFontPattern is regular expression for the name used by EDM - # - # Patterns are checked in the order in which they're listed in here, - # so a catch-all ".*" pattern should be at the end - font_mappings=helvetica=Liberation Sans,courier=Liberation Mono,times=Liberation Serif,.*=Liberation Sans - - # Path to text file that lists EDM search paths. - # May be a file system path or http:/.. link. - # - # In the file, each line in the text file contains a path, - # which may be a file system path or a http:// link. - # When trying to open an *.edl file, - # converter will try each path in the order - # listed in the file. - # Lines starting with "#" are ignored. - # - # When the edm_paths_config is left empty, - # the converter won't find files. - edm_paths_config= - - # Pattern and replacement for patching paths to *.stp (StripTool) files - # - # 'Shell Command' buttons in EDM that invoke a command of the form - # - # StripTool /some/path/to/plot.stp - # - # are converted into ActionButtons which open the `/some/path/to/plot.stp` file. - # Data Browser will then open the file when the action is invoked. - # - # The following regular expression pattern and replacement can be used - # to patch `/some/path/to/plot.stp`. - # By default, both are empty, so the path remains unchanged. - # - # Example for transforming all absolute paths into a web location: - # - # stp_path_patch_pattern=^(/) - # stp_path_patch_replacement=https://my_web_server/stripcharts$1 - # - # Note how the pattern may include group markers (..) - # and the replacement can reference them via $1, $2, ... - stp_path_patch_pattern= - stp_path_patch_replacement= - - -email ------ - -File ../../core/email/src/main/resources/email_preferences.properties:: - - # ------------------------- - # Package org.phoebus.email - # ------------------------- - - # smtp host - # When set to "DISABLE", email support is disabled - mailhost=smtp.bnl.gov - - # smtp port - mailport=25 - - # User and password for connecting to the mail host, usually left empty - username= - password= - - # Default address to be used for From: - # if it is left empty then the last used from address is used - from= - - -errlog ------- - -File ../../app/errlog/src/main/resources/errlog_preferences.properties:: - - # --------------------------------------- - # Package org.phoebus.applications.errlog - # --------------------------------------- - - # Number of lines to keep in error log - max_lines = 500 - - -eslog ------ - -File ../../app/eslog/src/main/resources/eslog_preferences.properties:: - - # -------------------------------------- - # Package org.phoebus.applications.eslog - # -------------------------------------- - es_url= - es_index=messagelog - - jms_url= - jms_user - jms_password - jms_topic=LOG - - -filebrowser ------------ - -File ../../app/filebrowser/src/main/resources/filebrowser_preferences.properties:: - - # -------------------------------------------- - # Package org.phoebus.applications.filebrowser - # -------------------------------------------- - - # Initial root directory for newly opened file browser - # May use system properties like "$(user.home)". - # At runtime, user can select a different base directory, - # but pressing the "Home" button reverts to this one. - default_root=$(user.home) - - # Show hidden files (File.isHidden)? - show_hidden=false - - -framework.autocomplete ----------------------- - -File ../../core/framework/src/main/resources/autocomplete_preferences.properties:: - - # ------------------------------------------ - # Package org.phoebus.framework.autocomplete - # ------------------------------------------ - - # Enable the built-in PV proposal providers? - enable_loc_pv_proposals=true - enable_sim_pv_proposals=true - enable_sys_pv_proposals=true - enable_pva_pv_proposals=true - enable_mqtt_pv_proposals=false - enable_formula_proposals=true - - # Site-specific proposal providers can be added via PVProposalProvider SPI, - # and disabled by removing the contribution. - - -framework.workbench -------------------- - -File ../../core/framework/src/main/resources/workbench_preferences.properties:: - - # --------------------------------------- - # Package org.phoebus.framework.workbench - # --------------------------------------- - - # External applications - # - # Defines applications to use for specific file extensions - # - # Format: - # - # Each definition consists of name, file extensions, command. - # - # Name is the name of the definition, used to register the application. - # File extensions is a '|'-separated list of file extensions (not including the 'dot'). - # Command is the path to the command. - # The command will be invoked with the full path to the resource as an argument. - # - # Each definition must use a key that starts with "external_app_" - - # Examples: - # - # Start 'gedit' for text files - # external_app_text=Text Editor,txt|dat|py|ini|db|xml|xsl|css|cmd|sh|st|log|out|md|shp,gedit - # - # Start 'eog' for images, 'firefox' for PDF files - # external_app_image=Image Viewer,png|jpg|gif|jpeg,eog - # - # Start 'firefox' to view PDFs - # external_app_pdf=PDF Viewer,pdf,firefox - # - # Example for some site-specific tool that opens 'alog' files - # external_app_alog=Alignment Log,alog,/path/to/alog_viewer - - # Directory where external applications are started - # May use system properties - external_apps_directory=$(user.home) - - -javafx.rtplot -------------- - -File ../../app/rtplot/src/main/resources/rt_plot_preferences.properties:: - - # ---------------------------------- - # Package org.csstudio.javafx.rtplot - # ---------------------------------- - - # Coloring used to shade plot region beyond 'now' - # in time-based plots. RGBA (all values 0..255) - # Painted on on top of grid, before traces are drawn. - # - # Half-transparent, average of black & white, - # works for both white and black backgrounds - shady_future=128, 128, 128, 128 - - # If you prefer a rose-colored future - # shady_future=255, 128, 128, 25 - - # If you prefer to not highlight the plot region beyond 'now' - # shady_future=128, 128, 128, 0 - - -logbook -------- - -File ../../core/logbook/src/main/resources/logbook_preferences.properties:: - - # ------------------------------ - # Package org.phoebus.logbook - # ------------------------------ - - # Site specific log book client implementation name. - # When empty, logbook submissions are disabled - logbook_factory=inmemory - - # Determines if a log entry created from context menu (e.g. display or data browser) - # should auto generate a title (e.g. "Display Screenshot..."). - auto_title=true - - # Determines if a log entry created from context menu (e.g. display or data browser) - # should auto generate properties (e.g. "resources.file"). - auto_property=false - - -logbook.olog.ui ---------------- - -File ../../app/logbook/olog/ui/src/main/resources/log_olog_ui_preferences.properties:: - - # ------------------------------ - # Package org.phoebus.logbook.olog.ui - # ------------------------------ - - # Comma-separated list of default logbooks for new log entries. - default_logbooks=Scratch Pad - - # The default query for logbook applications - default_logbook_query=desc=*&start=12 hours&end=now - - # Whether or not to save user credentials to file so they only have to be entered once when making log entries. - save_credentials=false - - # Stylesheet for the items in the log calendar view - calendar_view_item_stylesheet=Agenda.css - - # Text to render for the "Level" field of a log entry. Sites may wish to customize this with respect to - # its wording and its implied purpose. - level_field_name=Level: - - # Name of markup help. Language resolution and file extension is handled on service. - markup_help=CommonmarkCheatsheet - - # Root URL of the Olog web client, if one exists. Set this to the empty string - # to suppress rendering of the "Copy URL" button for a log entry. - web_client_root_URL= - - # Log entry groups support. If set to false user will not be able to create replies - # to log entries, and consequently UI elements and views related to log entry - # groups will not be shown. - log_entry_groups_support=false - - # Comma separated list of "hidden" properties. For instance, properties that serve internal - # business logic, but should not be rendered in the properties view. - hidden_properties=Log Entry Group - - # Log Entry Table display name. If non-empty it overrides default "Log Entry Table" - log_entry_table_display_name= - - # Log Entry Calendar display name. If non-empty it overrides default "Log Entry Calendar" - log_entry_calendar_display_name= - - # Log Entry property attribute types. - # The preference should be a URL pointing to an attribute_type.properties file. - # e.g. log_attribute_desc=file:///C:/phoebus/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/log_property_attributes.properties - # Classpath resource is supported if specified like log_attribute_desc=classpath:my_attr.properties. In this - # example the my_attr.properties file must be bundled as a classpath resource in the package org.phoebus.logbook.olog.ui. - # This optional file describing special types associated with some property attributes. - # - log_attribute_desc= - - # Limit used in "paginated" search, i.e. the number of search results per page - search_result_page_size=30 - - # Number of queries maintained by the OlogQueryManager. To make sense: must be >= 5 and <=30. - query_list_size=15 - - # Name of the search help content. Language resolution and file extension is handled on service. - search_help=SearchHelp - - -logbook.ui ----------- - -File ../../app/logbook/ui/src/main/resources/log_ui_preferences.properties:: - - # ------------------------------ - # Package org.phoebus.logbook.ui - # ------------------------------ - - # Comma-separated list of default logbooks for new log entries. - default_logbooks=Scratch Pad - - # The default query for logbook applications - default_logbook_query=search=*&start=12 hours&end=now - - # Whether or not to save user credentials to file so they only have to be entered once when making log entries. - save_credentials=false - - # Stylesheet for the items in the log calendar view - calendar_view_item_stylesheet=Agenda.css - - # Text to render for the "Level" field of a log entry. Sites may wish to customize this with respect to - # its wording and its implied purpose. - level_field_name=Level: - - -olog.api --------- - -File ../../app/logbook/olog/client/src/main/resources/olog_preferences.properties:: - - # -------------------------------------- - # Package org.phoebus.olog.api - # -------------------------------------- - - # The olog url - olog_url=localhost:9092 - - # User credentials for olog - username=user - password=**** - - # Enable debugging of http request and resposnsed - debug=false - - # The connection timeout for the Jersey client, in ms. 0 = infinite. - connectTimeout=0 - - -olog.es.api ------------ - -File ../../app/logbook/olog/client-es/src/main/resources/olog_es_preferences.properties:: - - # -------------------------------------- - # Package org.phoebus.olog.es.api - # -------------------------------------- - - # The olog url - olog_url=http://localhost:8080/Olog - - # User credentials for olog - username=admin - password=1234 - - # Enable debugging of http request and responses - debug=false - - # The connection timeout for the Jersey client, in ms. 0 = infinite. - connectTimeout=0 - - # Comma separated list of "Levels" in the create logbook entry UI. - # Sites may wish to customize (and localize) this. - levels=Urgent,Suggestion,Info,Request,Problem - - -pv --- - -File ../../core/pv/src/main/resources/pv_preferences.properties:: - - # ---------------------- - # Package org.phoebus.pv - # ---------------------- - - # Default PV Type - default=ca - - - -pv.ca ------ - -File ../../core/pv/src/main/resources/pv_ca_preferences.properties:: - - # ------------------------- - # Package org.phoebus.pv.ca - # ------------------------- - - # Channel Access address list - addr_list= - - auto_addr_list=true - - max_array_bytes=100000000 - - server_port=5064 - - repeater_port=5065 - - beacon_period=15 - - connection_timeout=30 - - # Support variable length arrays? - # auto, true, false - variable_length_array=auto - - # Connect at lower priority for arrays - # with more elements than this threshold - large_array_threshold= 100000 - - # Is the DBE_PROPERTY subscription supported - # to monitor for changes in units, limits etc? - dbe_property_supported=false - - # Mask to use for subscriptions - # VALUE, ALARM, ARCHIVE - monitor_mask=VALUE - - # Name server list - name_servers= - - -pv.formula ----------- - -File ../../core/pv/src/main/resources/pv_formula_preferences.properties:: - - # ------------------------------ - # Package org.phoebus.pv.formula - # ------------------------------ - - # Update throttle for input PVs - throttle_ms=500 - - -pv.mqtt -------- - -File ../../core/pv/src/main/resources/pv_mqtt_preferences.properties:: - - # --------------------------- - # Package org.phoebus.pv.mqtt - # --------------------------- - - # MQTT Broker - # All "mqtt://some/tag" PVs will use this broker - mqtt_broker=tcp://localhost:1883 - - -pv.pva ------- - -File ../../core/pv/src/main/resources/pv_pva_preferences.properties:: - - # ------------------------- - # Package org.phoebus.pv.pva - # ------------------------- - # By default, these preference settings are empty, - # and the PVA library will then honor the commonly used - # environment variables like EPICS_PVA_ADDR_LIST, - # EPICS_PVA_AUTO_ADDR_LIST etc. - # Defining preference values will override the environment - # variables which allows consolidating PVA settings - # with all the CS-Studio preference settings. - # - # - # Network clients typically need to configure the first - # three settings to successfully connect to PVA servers - # on the local network. - - # PVAccess address list - epics_pva_addr_list - - # PVAccess auto address list - true/false - epics_pva_auto_addr_list - - # Name servers used for TCP name resolution - epics_pva_name_servers - - # The following parameters should best be left - # at their default. - # - # For details, see PVASettings in PV Access library. - - # Port used for UDP name searches and beacons - epics_pva_broadcast_port - - # PV server's first TCP port - epics_pva_server_port - - # Connection timeout in seconds - epics_pva_conn_tmo - - # Maximum number of array elements shown when printing data - epics_pva_max_array_formatting - - # TCP buffer size for sending data - epics_pva_send_buffer_size - - # Timeout used by plain "put" type of write - # when checking success or failure. - # Note this is not used with asyncWrite, - # the "put-callback" which returns a Future - # for awaiting the completion, - # but only with the plain "put" that returns ASAP - epics_pva_write_reply_timeout_ms=1000 - - -pvtable -------- - -File ../../app/pvtable/src/main/resources/pv_table_preferences.properties:: - - # ---------------------------------------- - # Package org.phoebus.applications.pvtable - # ---------------------------------------- - - # Should all BYTE[] values be considered "long strings" - treat_byte_array_as_string=true - - # Show the units when displaying values? - show_units=true - - # Show a "Description" column that reads xxx.DESC? - show_description=true - - # Default tolerance for newly added items - tolerance=0.1 - - # Maximum update period for PVs in millisecs - max_update_period=500 - - -pvtree ------- - -File ../../app/pvtree/src/main/resources/pv_tree_preferences.properties:: - - # --------------------------------------- - # Package org.phoebus.applications.pvtree - # --------------------------------------- - - # The channel access DBR_STRING has a length limit of 40 chars. - # Since EPICS base R3.14.11, reading fields with an added '$' returns - # their value as a char[] without length limitation. - # For older IOCs, this will however fail, so set this option - # only if all IOCs are at least version R3.14.11 - read_long_fields=true - - # For each record type, list the fields to read and trace as 'links'. - # Format: record_type (field1, field2) ; record_type (...) - # - # Fields can simply be listed as 'INP', 'DOL'. - # The syntax INPA-L is a shortcut for INPA, INPB, INPC, ..., INPL - # The syntax INP001-128 is a shortcut for INP001, INP002, ..., INP128 - # The general syntax is "FIELDxxx-yyy", - # where "xxx" and "yyy" are the initial and final value. - # "xxx" and "yyy" need to be of the same length, i.e. "1-9" or "01-42", NOT "1-42". - # For characters, only single-char "A-Z" is supported, NOT "AA-ZZ", - # where it's also unclear if that should turn into AA, AB, AC, .., AZ, BA, BB, BC, .., ZZ - # or AA, BB, .., ZZ - # - # bigASub is a CSIRO/ASCAP record type, doesn't hurt to add that to the shared configuration - # - # scalcout is a bit unfortunate since there is no shortcut for INAA-INLL. - # - # alarm record has INP1-10. 1-9 handled by pattern, INP10 listed - - fields=aai(INP);ai(INP);bi(INP);compress(INP);longin(INP);int64in(INP);mbbi(INP);mbbiDirect(INP);mbboDirect(INP);stringin(INP);lsi(INP);subArray(INP);waveform(INP);aao(DOL);ao(DOL);bo(DOL);fanout(DOL);longout(DOL);int64out(DOL);mbbo(DOL);stringout(DOL);sub(INPA-L);genSub(INPA-L);calc(INPA-L);calcout(INPA-L);aSub(INPA-U);seq(SELN);bigASub(INP001-128);scalcout(INPA-L,INAA,INBB,INCC,INDD,INEE,INFF,INGG,INHH,INII,INJJ,INKK,INLL);alarm(INP1-9,INP10) - - - # Max update period in seconds - update_period=0.5 - - -saveandrestore --------------- - -File ../../app/save-and-restore/service/src/main/resources/client_preferences.properties:: - - # - # Copyright (C) 2020 European Spallation Source ERIC. - # - # This program is free software; you can redistribute it and/or - # modify it under the terms of the GNU General Public License - # as published by the Free Software Foundation; either version 2 - # of the License, or (at your option) any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU General Public License for more details. - # - # You should have received a copy of the GNU General Public License - # along with this program; if not, write to the Free Software - # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - # - - # ----------------------------------------------- - # Package org.phoebus.applications.saveandrestore - # ----------------------------------------------- - - # The URL to the save-and-restore service - jmasar.service.url=http://localhost:8080 - - # Read timeout (in ms) used by the Jersey client - httpClient.readTimeout=1000 - - # Connect timeout in (ms) used by the Jersey client - httpClient.connectTimeout=1000 - - -scan.client ------------ - -File ../../app/scan/client/src/main/resources/scan_client_preferences.properties:: - - # ---------------------------------------- - # Package org.csstudio.scan.client - # ---------------------------------------- - - # Name of host where scan server is running - host=localhost - - # TCP port of scan server REST interface - port=4810 - - # Poll period [millisecs] of the scan client (scan monitor, plot, ...) - poll_period=1000 - - -scan.ui -------- - -File ../../app/scan/ui/src/main/resources/scan_ui_preferences.properties:: - - # ---------------------------- - # Package org.csstudio.scan.ui - # ---------------------------- - - # Show scan monitor status bar? - monitor_status=false - - -security --------- - -File ../../core/security/src/main/resources/phoebus_security_preferences.properties:: - - # ---------------------------- - # Package org.phoebus.security - # ---------------------------- - - # Authorization file - # - # If left empty, the built-in core/security/authorization.conf is used. - # - # When specifying a plain file name like "authorization.conf", - # the install location (Locations.install()) is searched for that file name. - # - # The file name can also be an absolute path like /some/path/auth.conf. - # - # Finally, the file name may use a system property like $(auth_file) - # which in turn could be set to either BUILTIN, a file in the install location, - # or an absolute path. - # - # When set to an invalid file, the user will have no authorizations at all. - - # Use built-in core/security/authorization.conf - authorization_file= - - # Use authorization.conf in the install location - #authorization_file=authorization.conf - - # Secure store underlying implementation. - # Can be 'FILE' or 'IN_MEMORY' - secure_store_target=FILE - - - -trends.databrowser3 -------------------- - -File ../../app/databrowser/src/main/resources/databrowser_preferences.properties:: - - # ---------------------------------------- - # Package org.csstudio.trends.databrowser3 - # ---------------------------------------- - - # Default auto scale value - # Possible values are: true to enable the automatic calculation of the min/max Y-axis, or false to use min/max fixed values. - use_auto_scale=false - - # Default time span displayed in plot in seconds - time_span=3600 - - # Default scan period in seconds. 0 for 'monitor' - scan_period=0.0 - - # Default plot update period in seconds - update_period=3.0 - - # .. elements in live sample buffer - live_buffer_size=5000 - - # Default line width - line_width=2 - - # Opacity of 'area' - # 0%: Area totally transparent (invisible) - # 20%: Area quite transparent - # 100%: Area uses solid color - opacity=40 - - # Default trace type for newly created traces. - # Allowed values are defined by org.csstudio.trends.databrowser3.model.TraceType: - # AREA, ERROR_BARS, SINGLE_LINE, AREA_DIRECT, SINGLE_LINE_DIRECT, SQUARES, ... - trace_type=AREA - - # Delay in milliseconds that delays archive requests when - # the user moves the time axis to avoid a flurry of archive requests - # while interactively zooming and panning - archive_fetch_delay=500 - - # Number of concurrent archive fetch requests. - # When more requests are necessary, the background jobs - # will wait until the previously submitted jobs complete, - # to limit the number of concurrent requests. - # - # Ideally, the number can be high, but to limit the number - # of concurrent requests to for example an RDB, - # this value can be lowered. - # - # Note that this does not apply to 'exporting' data - # in spreadsheet form, where data for N channels is still - # collected by reading from N concurrent archive readers. - concurrent_requests=1000 - - # Number of binned samples to request for optimized archive access. - # Negative values scale the display width, - # i.e. -3 means: 3 times Display pixel width. - plot_bins=-3 - - # Suggested data servers - # Format: *| - # List of URLs, separated by '*'. - # Each URL may be followed by an "|alias" - # - # RDB URLs - # jdbc:mysql://localhost/archive - # - # Archive Appliance - # pbraw\://arcapp01.site.org:17668/retrieval - # - # Channel Archiver Network Data Server - # xnds://localhost/archive/cgi/ArchiveDataServer.cgi - # - # Channel Archiver index file (binary) or index.xml (list of indices) - # cadf:/path/to/index - # cadf:/path/to/index.xml - urls=jdbc:mysql://localhost/archive|RDB*xnds://localhost/archive/cgi/ArchiveDataServer.cgi - - # Default data sources for newly added channels - # Format: Same as 'urls' - archives=jdbc:mysql://localhost/archive|RDB*xnds://localhost/archive/cgi/ArchiveDataServer.cgi - - # When opening existing data browser plot, - # use archive data sources specified in the configuration file (original default) - # or ignore saved data sources and instead use the preference settings? - use_default_archives=false - - # If there is an error in retrieving archived data, - # should that archive data source be dropped from the channel? - # This is meant to avoid needless queries to archives that cannot be accessed. - # Note that archive data sources which clearly report a channel as "not found" - # will still be dropped. This option only configures if data sources which - # return an error (cannot connect, ...) should be queried again for the given channel. - drop_failed_archives=true - - # With EPICS IOCs from release 7 on, the PVs - # "xxx", "ca://xxx" and "pva://xxx" all refer - # to the same record "xxx" on the IOC. - # - # When the plot requests "pva://xxx", the archive might still - # trace that channel as "ca://xxx" or "xxx". - # Alternatively, the archive might already track the channel - # as "pva://xxx" while data browser plots still use "ca://xxx" - # or just "xxx". - # This preference setting instructs the data browser - # to try all equivalent variants. If any types are listed, - # just "xxx" without any prefix will also be checked in addition - # to the listed types. - # - # The default of setting of "ca, pva" supports the seamless - # transition between the key protocols. - # - # When `equivalent_pv_prefixes` is empty, - # the PV name is used as is without looking for any equivalent names. - equivalent_pv_prefixes=ca, pva - - # Re-scale behavior when archived data arrives: NONE, STAGGER - archive_rescale=STAGGER - - # Shortcuts offered in the Time Axis configuration - # Format: - # Text for shortcut,start_spec|Another shortcut,start_spec - time_span_shortcuts=30 Minutes,-30 min|1 Hour,-1 hour|12 Hours,-12 hour|1 Day,-1 days|7 Days,-7 days - - #It is a path to the directory where the PLT files for WebDataBrowser are placed. - plt_repository=/opt/codac/opi/databrowser/ - - # Automatically refresh history data when the liver buffer is full - # This will prevent the horizontal lines in the shown data when the buffer - # is too small to cover the selected time range - automatic_history_refresh=true - - # Scroll step, i.e. size of the 'jump' left when scrolling, in seconds. - # (was called 'future_buffer') - scroll_step = 5 - - # Display the trace names on the Value Axis - # the default value is "true". "false" to not show the trace names on the Axis - use_trace_names = true - - # Prompt / warn when trying to request raw data? - prompt_for_raw_data_request = true - - # Prompt / warn when making trace invisible? - prompt_for_visibility = true - - # Shortcuts offered in the Time Axis configuration - # Format: - # Text for shortcut,start_spec|Another shortcut,start_spec - time_span_shortcuts=30 Minutes,-30 min|1 Hour,-1 hour|12 Hours,-12 hour|1 Day,-1 days|7 Days,-7 days - - # Determines if the plot runtime config dialog is supported. Defaults to false as the Data Browser - # offers the same functionality through its configuration tabs. - config_dialog_supported=false - - -ui --- - -File ../../core/ui/src/main/resources/phoebus_ui_preferences.properties:: - - # ---------------------- - # Package org.phoebus.ui - # ---------------------- - - # Show the splash screen? - # Can also be set via '-splash' resp. '-nosplash' command line options - splash=true - - # 'Welcome' URL - # - # When left empty, the built-in welcome.html resource is used. - # Site-specific products can set this to their desired URL, - # which may include Java system properties to bundle content - # with the product, for example - # file:$(phoebus.install)/welcome_to_hawkins_labs.html - welcome= - - # Default applications - # - # When there are multiple applications that handle - # a resource, the setting determines the one used by default. - # - # Format is comma-separated list with sub-text of default application names. - # For example, "run, exe" would pick "display_runtime" over "display_editor", - # and "foo_executor" over "foo_creator". - # The patterns "edit, creat" would inversely open the editor-type apps. - # - # This makes the display_runtime and the 3d_viewer default apps, - # using display_editor and a potentially configured text editor for *.shp files secondary - default_apps=run,3d,convert_edm - - # Hide SPI-provided menu entries - # Comma-separated list of class names - hide_spi_menu=org.phoebus.ui.monitoring.FreezeUI - - # Top resources to show in "File" menu and toolbar - # - # Format: - # uri1 | uri2,Display name 2 | uri3,Display name 3 - top_resources=examples:/01_main.bob?app=display_runtime,Example Display | pv://?sim://sine&app=probe,Probe Example | pv://?sim://sine&loc://x(10)&app=pv_table,PV Table Example | http://www.google.com?app=web, Google - - # Home display file. "Home display" button will navigate to this display. - home_display=examples:/01_main.bob?app=display_runtime,Example Display - - # How many array elements to show when formatting as text? - max_array_formatting=256 - - # UI Responsiveness Monitor Period - # Period between tests [millisec], - # i.e. the minimum detected UI freeze duration - # Set to 0 to disable - ui_monitor_period=500 - - # Show user ID in status bar? - status_show_user=true - - # Set default save path - default_save_path= - - # Set the path to a folder with default layouts - layout_dir= - - # Compute print scaling in 'landscape' mode? - # Landscape mode is generally most suited for printouts - # of displays or plots, because the monitor tends to be 'wide'. - # At least on Mac OS X, however, the printing always appears to use - # portrait mode, so print layouts computed in landscape mode - # get cropped. - # Details can also depend on the printer driver. - print_landscape=true - - # Color for text and the background for 'OK' alarm severity (R,G,B or R,G,B,A values in range 0..255) - ok_severity_text_color=0,255,0 - ok_severity_background_color=255,255,255 - - # Color for text and the background for 'MINOR' alarm severity - minor_severity_text_color=255,128,0 - minor_severity_background_color=255,255,255 - - # Color for text and the background for 'MAJOR' alarm severity - major_severity_text_color=255,0,0 - major_severity_background_color=255,255,255 - - # Color for text and the background for 'INVALID' alarm severity - invalid_severity_text_color=255,0,255 - invalid_severity_background_color=255,255,255 - - # Color for text and the background for 'UNDEFINED' alarm severity - undefined_severity_text_color=200,0,200,200 - undefined_severity_background_color=255,255,255 - - -update ------- - -File ../../app/update/src/main/resources/update_preferences.properties:: - - # ---------------------------------------- - # Package org.phoebus.applications.update - # ---------------------------------------- - - # Time to wait [seconds] for update check - # to allow more important tools to start - delay=10 - - # Version time/date - # - # If the distribution found at the `update_url` - # is later than this date, an update will be performed. - # - # The updated distribution must contain a new value for - # the org.phoebus.applications.update/current_version setting. - # - # By for example publishing updates with a 'current_version' - # that's one month ahead, you can suppress minor updates - # for a month. - # - # Format: YYYY-MM-DD HH:MM - #current_version=2018-06-18 13:10 - current_version= - - - # Location where updates can be found - # - # The file:, http: or https: URL is checked. - # If it exists, and its modification time is after `current_version`, - # the updated distribution is downloaded - # and the current Locations.install() is replaced. - # - # Location may include system properties - # and $(arch) will be replaced by "linux", "mac" or "win" - # to allow locations specific to each architecture. - update_url= - # update_url=https://controlssoftware.sns.ornl.gov/css_phoebus/nightly/product-sns-$(arch).zip - - #gitlab_api_url=https://HOST/api/v4 - #gitlab_project_id= - gitlab_package_name=phoebus-$(arch) - #gitlab_token= - - # List of regular expressions, comma-separated, which will be - # removed from the ZIP file entry. - # If result is empty string, the entry is skipped. - # - # The update ZIP file can have various formats. - # - # Basic ZIP file: - # phoebus-{site, version}/* - # - # => Remove 'phoebus-.*' from entry name - # to install _content_ of zip into install_location - # without creating yet another subdir - # - # ZIP that's packaged for Windows, including JDK: - # product-sns-0.0.1/* - # jdk/* - # - # => Remove 'product-sns-*' from entry name, - # skip 'jdk'. - # - # ZIP that's packaged for Mac: Either - # phoebus.app/product-sns-0.0.1/* => Remove .../ - # phoebus.app/jdk/* => Skip - # phoebus.app/Contents/* => Skip - # or: - # CSS_Phoebus.app/product-sns-0.0.1/* => Remove .../ - # CSS_Phoebus.app/jdk/* => Skip - # CSS_Phoebus.app/Contents/* => Skip - # - # Example: - # phoebus\.app/ - Strip Mac "phoebus.app/" from entries - # so they look more like the Windows example - # - # phoebus-[^/]+/ - Strip phoebus product name from ZIP entry - # - # jdk/.* - Remove complete jdk entry to skip it - removals=CSS_Phoebus\\.app/Contents/.*,CSS_Phoebus\\.app/,phoebus\\.app/Contents/.*,phoebus\\.app/,phoebus-[^/]+/,product-[^/]+/,jdk/.* - - -viewer3d --------- - -File ../../app/3d-viewer/src/main/resources/3d_viewer_preferences.properties:: - - # -------------------------------- - # Package org.phoebus.app.viewer3d - # -------------------------------- - - # Time out for reading from a URI - read_timeout=10000 - - # Default directory for the file chooser. - default_dir=$(user.home) - - # Cone is approximated with these many faces. - # 3: Triangular base, most minimalistic - # 8: Looks pretty good - # Higher: Approaches circular base, - # but adds CPU & memory usage - # and doesn't really look much better - cone_faces=8 - - diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt new file mode 100644 index 0000000000..9a1114393a --- /dev/null +++ b/docs/source/requirements.txt @@ -0,0 +1,3 @@ +sphinx==7.2.6 +sphinx_rtd_theme==2.0.0 +readthedocs-sphinx-search==0.3.2 diff --git a/docs/source/services.png b/docs/source/services.png new file mode 100644 index 0000000000..90e060a120 Binary files /dev/null and b/docs/source/services.png differ diff --git a/docs/source/services.rst b/docs/source/services.rst deleted file mode 100644 index 926c8fe3f1..0000000000 --- a/docs/source/services.rst +++ /dev/null @@ -1,14 +0,0 @@ -Services -======== - -The following sections describe available services. - - -.. toctree:: - :maxdepth: 1 - - services/archive-engine/doc/index - services/alarm-config-logger/doc/index - services/save-and-restore/doc/index - services/alarm-server/doc/index - services/alarm-logger/doc/index diff --git a/docs/source/services_architecture.rst b/docs/source/services_architecture.rst new file mode 100644 index 0000000000..3591654b12 --- /dev/null +++ b/docs/source/services_architecture.rst @@ -0,0 +1,49 @@ +Services Architecture +===================== + +Architecture diagram +-------------------- +.. figure:: services.png + +| This architecture diagram shows the main services used by phoebus and their port. +| Port 808x means : 8080 is used by default. It must be changed if running on the same server as the other services. + + +Settings configuration +---------------------- + +Example of settings.ini + +**Alarm Server** + +| org.phoebus.email/mailhost=smtp.com +| org.phoebus.applications.alarm/server=localhost:9092 +| org.phoebus.applications.alarm/enable_slot_time=false + +**Alarm Logger** + +| org.phoebus.applications.alarm.logging.ui/service_uri=http://localhost:8082 +| org.phoebus.applications.alarm.logging.ui/results_max_size=10000 + +**Olog** + +| #Logbook +| org.phoebus.logbook.ui/logbook_factory=olog-es +| org.phoebus.logbook/logbook_factory=olog-es +| org.phoebus.logbook.olog.ui/save_credentials=true +| #Olog +| org.phoebus.olog.es.api/olog_url=http://localhost:8081/Olog +| org.phoebus.olog.es.api/username=admin +| org.phoebus.olog.es.api/password=adminPass + +**Archive Appliance** + +| org.csstudio.trends.databrowser3/urls=pbraw://localhost:17668/retrieval + +**Save & Restore** + +| org.phoebus.applications.saveandrestore.datamigration.git/jmasar.service.url=http://localhost:8084 + +**Channel Finder** + +| org.phoebus.channelfinder/serviceURL=http://localhost:8080/ChannelFinder diff --git a/docs/source/trouble_shooting.rst b/docs/source/trouble_shooting.rst new file mode 100644 index 0000000000..6370930b40 --- /dev/null +++ b/docs/source/trouble_shooting.rst @@ -0,0 +1,131 @@ +Troubleshooting +=============== + +.. contents:: Here is a non exhaustive list of issues and the corresponding procedures to fix them. + +Slow running and latency behavior +--------------------------------- +**symptoms** + +| Phoebus is slow or nearly freezing when there are a lot of views opened +| or when a view is connected to a lot of process variables. + +**procedure** + +| Increase the Java Heap Size allocation. It works for any Java Application (Eclipse, CS-Studio ...) +| Edit launching scripts phoebus.sh or phoebus.bat +| and configure JVM options Xms and Xmx (Java Heap Minimum Size and Java Heap Maximum Size) + +.. code-block:: shell + + java -Xms2048m -Xmx2048m + + +Impossible to run Phoebus under linux +------------------------------------- +**symptoms** + +| If you get the following error message: + +.. code-block:: kmsg + + /bin/java : permission denied + +| or + +.. code-block:: kmsg + + Error initializing QuantumRenderer: no suitable pipeline found + java.lang.RuntimeException: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found + at com.sun.javafx.tk.quantum.QuantumRenderer.getInstance(QuantumRenderer.java:283) + at com.sun.javafx.tk.quantum.QuantumToolkit.init(QuantumToolkit.java:253) + at com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:268) + at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:291) + at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:163) + at com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:659) + at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:679) + at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:196) + at java.base/java.lang.Thread.run(Thread.java:831) + Caused by: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found + at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.init(QuantumRenderer.java:95) + at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:125) + ... 1 more + Exception in thread "main" java.lang.RuntimeException: No toolkit found + at com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:280) + at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:291) + at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:163) + at com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:659) + at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:679) + at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:196) + at java.base/java.lang.Thread.run(Thread.java:831) + + +**procedure** + +| Change the jdk and javafx folders rights : +| *chmod -R 755 Phoebus_install* + + +Choose OS target when building Phoebus +-------------------------------------- +**symptoms** + +| Phoebus is built on an OS (Linux for instance) but will run on another one (Windows for instance) + +**procedure** + +| It is possible to do a cross-build by specifying -Djavafx.platform= on the Maven command line, where os is linux, win or mac. + + +Cannot modify Phoebus layout anymore +------------------------------------ +**symptoms** + +| Impossible to close some views or replace them, even after restarting Phoebus. + +**procedure** + +| All the view settings are stored in a file named memento. +| To reset all the settings, you must delete this file : + +* under linux : /home/user/.phoebus/memento +* under windows : C:\\users\\.phoebus\\memento + +Start alarm services without the console +---------------------------------------- +**symptoms** + +| Phoebus Alarm Server or Phoebus Alarm Logger starts with a console. + +**procedure** + +| The services can also be started without any prompt. +| Start the service with *-noshell* argument + +.. code-block:: systemd + + #Phoebus alarm server + ExecStart=/opt/alarm-phoebus-server/current/alarm-server.sh -settings ${SERVER}/settings.ini -config ${CONFIG} -noshell + +.. code-block:: systemd + + #Phoebus alarm logger + ExecStart=/opt/alarm-logger/current/alarm-logger.sh -properties ./application.properties -noshell + + +No PV found by Alarm Server +--------------------------- +**symptoms** + +Phoebus Alarm Server does not find any PV. + +**procedure** + +| Phoebus Alarm Server doesn't use the environment variable EPICS_CA_ADDR_LIST. +| It uses the parameter org.phoebus.pv.ca/addr_list in settings.ini to find the Channel Access list. +| The path to the settings.ini can be given by the --settings argument + +.. code-block:: systemd + + ExecStart=/opt/alarm-phoebus-server/current/alarm-server.sh -settings ${SERVER}/settings.ini -config ${CONFIG} -noshell + diff --git a/phoebus-product/.classpath b/phoebus-product/.classpath index f181748f32..ffb088f71b 100644 --- a/phoebus-product/.classpath +++ b/phoebus-product/.classpath @@ -18,7 +18,6 @@ - @@ -55,6 +54,7 @@ diff --git a/phoebus-product/README.md b/phoebus-product/README.md index 957eb1f3b3..96fb1a4d96 100644 --- a/phoebus-product/README.md +++ b/phoebus-product/README.md @@ -28,7 +28,7 @@ For site-specific examples, see The following use cases have been verified: - * MacOS version 10.15.7, dmg and pkg. + * MacOS versions 10,11,12,13, dmg and pkg. * Windows 10, msi only. #### Prerequisites @@ -85,7 +85,7 @@ Consider either of the following workarounds: * Copy the installer from a file share. This is apparently considered more safe than HTTP download, and works for both Windows and MacOS. * Add a digital signature using a trusted certificate. `jpackage` supports application signing, so it may be incorporated -into the `jpackage` build process. NOTE: this has not been verified. +into the `jpackage` build process. See also below. * Distribute installers - and updates! - using IT management tools. This is the current setup used for Windows and MacOS at the European Spallation Source. @@ -96,6 +96,20 @@ target runtime selection may impact the end result, i.e. the Phoebus application For instance, while the Java runtime Adopt JDK 11.0.9 can be bundled into a working installation, Adopt JDK 11.0.12 will not work when Phoebus is launched. On MacOS Adopt JDK 11.0.12 works fine. +### Application signing +Starting from MacOS 13.2 (possibly from 13.0), installer packages must be signed for a hassle-free installation process. +To include signing in the `jpackage` build, add the following in step 1: +`--mac-sign +--mac-package-identifier org.phoebus.product.Launcher +--mac-package-name CSS-Phoebus +--mac-signing-keychain "/Library/Keychains/System.keychain" +--mac-signing-key-user-name 'Developer ID Application: European Spallation Source Eric (W2AG9MPZ43)'`. +Here the `--mac-signing-key-user-name` value identifies a certificate installed on the Mac OS host. Note that the +certificate type **must** be of type `Developer ID Application`. Users or organizations enrolled in the Apple +Developer Program may request/create such certificates. + + +` \ No newline at end of file diff --git a/phoebus-product/pom.xml b/phoebus-product/pom.xml index 25a44a206d..c9091a0e73 100644 --- a/phoebus-product/pom.xml +++ b/phoebus-product/pom.xml @@ -1,347 +1,422 @@ - 4.0.0 - product + 4.0.0 + product - - 3.0.0 - + + 3.0.0 + - - - org.phoebus - core-launcher - 4.7.2-SNAPSHOT - - - org.phoebus - app-diag - 4.7.2-SNAPSHOT - - - org.phoebus - app-filebrowser - 4.7.2-SNAPSHOT - - - org.phoebus - app-probe - 4.7.2-SNAPSHOT - - - org.phoebus - app-logbook-inmemory - 4.7.2-SNAPSHOT - - - org.phoebus - app-logbook-olog-ui - 4.7.2-SNAPSHOT - true - - - org.phoebus - app-logbook-elog - 4.7.2-SNAPSHOT - true - - - org.phoebus - app-logbook-olog-client - 4.7.2-SNAPSHOT - true - - - org.phoebus - app-logbook-olog-client-es - 4.7.2-SNAPSHOT - true - - - org.phoebus - app-pvtable - 4.7.2-SNAPSHOT - - - org.phoebus - app-pvtree - 4.7.2-SNAPSHOT - - - org.phoebus - app-log-configuration - 4.7.2-SNAPSHOT - - - org.phoebus - app-email-ui - 4.7.2-SNAPSHOT - - - org.phoebus - app-errlog - 4.7.2-SNAPSHOT - - - org.phoebus - app-rtplot - 4.7.2-SNAPSHOT - - - org.phoebus - app-databrowser - 4.7.2-SNAPSHOT - - - org.phoebus - app-databrowser-timescale - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-representation-javafx - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-fonts - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-runtime - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-thumbwheel - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-editor - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-navigation - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-adapters - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-convert-medm - 4.7.2-SNAPSHOT - - - org.phoebus - app-display-convert-edm - 4.7.2-SNAPSHOT - - - org.phoebus - app-scan-ui - 4.7.2-SNAPSHOT - - - org.phoebus - app-alarm-ui - 4.7.2-SNAPSHOT - - - org.phoebus - app-alarm-logging-ui - 4.7.2-SNAPSHOT - - - org.phoebus - app-alarm-datasouce - 4.7.2-SNAPSHOT - - - org.phoebus - app-update - 4.7.2-SNAPSHOT - - - org.phoebus - app-3d-viewer - 4.7.2-SNAPSHOT - - - org.phoebus - app-perfmon - 4.7.2-SNAPSHOT - - - org.phoebus - app-console - 4.7.2-SNAPSHOT - - - org.phoebus - app-imageviewer - 4.7.2-SNAPSHOT - - - org.phoebus - app-eslog - 4.7.2-SNAPSHOT - + + + org.phoebus + core-launcher + 4.7.4-SNAPSHOT + + + org.phoebus + app-diag + 4.7.4-SNAPSHOT + + + org.phoebus + app-filebrowser + 4.7.4-SNAPSHOT + + + org.phoebus + app-probe + 4.7.4-SNAPSHOT + + + org.phoebus + app-logbook-inmemory + 4.7.4-SNAPSHOT + + + org.phoebus + app-logbook-olog-ui + 4.7.4-SNAPSHOT + true + + + org.phoebus + app-logbook-elog + 4.7.4-SNAPSHOT + true + + + org.phoebus + app-logbook-olog-client + 4.7.4-SNAPSHOT + true + + + org.phoebus + app-logbook-olog-client-es + 4.7.4-SNAPSHOT + true + + + org.phoebus + app-pvtable + 4.7.4-SNAPSHOT + + + org.phoebus + app-pvtree + 4.7.4-SNAPSHOT + + + org.phoebus + app-log-configuration + 4.7.4-SNAPSHOT + + + org.phoebus + app-email-ui + 4.7.4-SNAPSHOT + + + org.phoebus + app-errlog + 4.7.4-SNAPSHOT + + + org.phoebus + app-rtplot + 4.7.4-SNAPSHOT + + + org.phoebus + app-databrowser + 4.7.4-SNAPSHOT + + + org.phoebus + app-databrowser-timescale + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-representation-javafx + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-fonts + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-runtime + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-thumbwheel + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-linearmeter + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-editor + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-navigation + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-adapters + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-convert-medm + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-convert-edm + 4.7.4-SNAPSHOT + + + org.phoebus + app-scan-ui + 4.7.4-SNAPSHOT + + + org.phoebus + app-alarm-ui + 4.7.4-SNAPSHOT + + + org.phoebus + app-alarm-logging-ui + 4.7.4-SNAPSHOT + + + org.phoebus + app-alarm-datasouce + 4.7.4-SNAPSHOT + + + org.phoebus + app-update + 4.7.4-SNAPSHOT + + + org.phoebus + app-3d-viewer + 4.7.4-SNAPSHOT + + + org.phoebus + app-perfmon + 4.7.4-SNAPSHOT + + + org.phoebus + app-console + 4.7.4-SNAPSHOT + + + org.phoebus + app-imageviewer + 4.7.4-SNAPSHOT + + + org.phoebus + app-eslog + 4.7.4-SNAPSHOT + - - - org.phoebus - app-trends-rich-adapters - 4.7.2-SNAPSHOT - true - - - org.phoebus - app-channel-views - 4.7.2-SNAPSHOT - - - org.phoebus - app-channel-channelfinder - 4.7.2-SNAPSHOT - - - org.phoebus - save-and-restore - 4.7.2-SNAPSHOT - - - org.phoebus - save-and-restore-logging - 4.7.2-SNAPSHOT - - - org.phoebus - app-credentials-management - 4.7.2-SNAPSHOT - - - ${project.groupId} - phoebus-target - ${project.version} - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - prepare-package - - copy-dependencies - - - ${project.build.directory}/lib - false - false - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.0 - - - - true - lib - org.phoebus.product.Launcher - - - - + + + org.phoebus + app-trends-rich-adapters + 4.7.4-SNAPSHOT + true + + + org.phoebus + app-channel-views + 4.7.4-SNAPSHOT + + + org.phoebus + app-channel-channelfinder + 4.7.4-SNAPSHOT + + + org.phoebus + save-and-restore + 4.7.4-SNAPSHOT + + + org.phoebus + save-and-restore-logging + 4.7.4-SNAPSHOT + + + org.phoebus + app-credentials-management + 4.7.4-SNAPSHOT + + + ${project.groupId} + phoebus-target + ${project.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/lib + false + false + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + lib + org.phoebus.product.Launcher + + + + - - - maven-antrun-plugin - - - verify - - true - - - - - - - - - - - - - - - - - - - - - - - - - run - - - - - - - - maven-assembly-plugin - 3.2.0 - - - src/assembly/package.xml - - posix - false - - - - make-assembly - package - - single - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - - true - - - - - - org.phoebus - parent - 4.7.2-SNAPSHOT - + + + maven-antrun-plugin + 3.0.0 + + + verify + + true + + + + + + + run + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + true + + + + + + org.phoebus + parent + 4.7.4-SNAPSHOT + + + + + mac + + + mac + + + + + + maven-assembly-plugin + 3.2.0 + + + tar.gz + + + src/assembly/package.xml + + posix + false + + + + make-assembly + package + + single + + + + + + + + + linux + + + + unix + + + + + + maven-assembly-plugin + 3.2.0 + + + tar.gz + + + src/assembly/package.xml + + posix + false + + + + make-assembly + package + + single + + + + + + + + + windows + + + windows + + + + + + maven-assembly-plugin + 3.2.0 + + + zip + + + src/assembly/package.xml + + posix + false + + + + make-assembly + package + + single + + + + + + + + diff --git a/pom.xml b/pom.xml index 87dddf6e63..b118f42d76 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.phoebus parent - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT pom phoebus (parent) A framework and set of tools to monitor and operate large scale control systems, such as the ones in the accelerator community. @@ -41,6 +41,12 @@ European Spallation Source https://github.com/georgweiss + + Katy Saintin + katy.saintin@cea.fr + CEA Paris-Saclay + https://github.com/katysaintin + @@ -58,13 +64,14 @@ - 7.0.8 - 1.0.5 + 2024-01-10T19:23:58Z + 7.0.10 + 1.0.7 19 2.12.3 1.14 2.23.4 - 42.2.9 + 42.6.0 8.0.30 9.4.30.v20200611 @@ -75,7 +82,7 @@ UTF-8 UTF-8 true - 31.0.1-jre + 32.1.1-jre 2.17.1 10.16.1.1 2.7.3 @@ -127,7 +134,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 + 3.2.0 public @@ -136,13 +143,20 @@ org.apache.maven.plugins maven-release-plugin - 3.0.0-M5 + 3.0.1 v@{project.version} true releases + + org.apache.maven.plugins + maven-jar-plugin + + 3.3.0 + diff --git a/services/alarm-config-logger/pom.xml b/services/alarm-config-logger/pom.xml index ff62bd5d37..e921b0fec3 100644 --- a/services/alarm-config-logger/pom.xml +++ b/services/alarm-config-logger/pom.xml @@ -3,7 +3,7 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 2.7.3 @@ -44,7 +44,7 @@ org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.apache.kafka @@ -59,7 +59,22 @@ org.eclipse.jgit org.eclipse.jgit - 5.0.3.201809091024-r + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.archive + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.ssh.jsch + 6.6.0.202305301015-r + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 6.6.0.202305301015-r @@ -68,20 +83,35 @@ 1.7.28 + + + + executable-jar + + + !skip-executable-jar + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.9 + + + + repackage + + + + + + + + - - org.springframework.boot - spring-boot-maven-plugin - 2.1.6.RELEASE - - - - repackage - - - - maven-assembly-plugin 3.2.0 diff --git a/services/alarm-config-logger/src/main/java/org/phoebus/alarm/logging/AlarmConfigLogger.java b/services/alarm-config-logger/src/main/java/org/phoebus/alarm/logging/AlarmConfigLogger.java index 66c763f1b1..3d486eb269 100644 --- a/services/alarm-config-logger/src/main/java/org/phoebus/alarm/logging/AlarmConfigLogger.java +++ b/services/alarm-config-logger/src/main/java/org/phoebus/alarm/logging/AlarmConfigLogger.java @@ -39,9 +39,14 @@ import org.eclipse.jgit.api.PushCommand; import org.eclipse.jgit.api.RemoteRemoveCommand; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.transport.sshd.JGitKeyCache; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder; import org.eclipse.jgit.util.FS; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.xml.XmlModelWriter; @@ -67,6 +72,9 @@ public class AlarmConfigLogger implements Runnable { // The alarm tree model which holds the current state of the alarm server private final AlarmClient model; + private SshdSessionFactory sshdSessionFactory; + private UsernamePasswordCredentialsProvider usernamePasswordCredentialsProvider; + public AlarmConfigLogger(String topic, String location, String remoteLocation) { super(); this.topic = topic; @@ -106,6 +114,30 @@ private void initialize() { logger.log(Level.WARNING, "Failed to initiate the git repo", e); } } + // Set up the ssh keys if used + if(props.containsKey("use_ssh_keys") && Boolean.parseBoolean(props.getProperty("use_ssh_keys"))) { + try { + File sshDir = new File(FS.DETECTED.userHome(), ".ssh"); + JGitKeyCache cache = new JGitKeyCache(); + SshdSessionFactoryBuilder builder = new SshdSessionFactoryBuilder(); + if(props.containsKey("private_key")) { + File key = new File(props.getProperty("private_key")); + builder.setDefaultKeysProvider(file -> new CachingKeyPairProvider(List.of(key.getAbsoluteFile().toPath()), cache)); + } + builder.setHomeDirectory(FS.DETECTED.userHome()); + builder.setSshDirectory(sshDir); + sshdSessionFactory = builder.build(cache); + } catch (NullPointerException e) { + logger.log(Level.WARNING, "Failed to open .ssh/private_key", e); + } + + } + // Setup basic username/password auth + if (props.containsKey("username") && props.containsKey("password")) { + usernamePasswordCredentialsProvider = new UsernamePasswordCredentialsProvider( + props.getProperty("username"), + props.getProperty("password")); + } // Check if it is configured with the appropriate remotes if (remoteLocation != null && !remoteLocation.isEmpty()) { try { @@ -199,14 +231,14 @@ public void run() { /** * Process a single alarm configuration event * - * @param path + * @param rawPath * @param alarm_config * @param commit */ private synchronized void processAlarmConfigMessages(String rawPath, String alarm_config, boolean commit) { try { - if (rawPath.contains("config:/")) { - String path = (rawPath.split("config:/"))[1]; + if (rawPath.contains("config:/")) { + String path = (rawPath.split("config:/"))[1]; logger.log(Level.INFO, "processing message:" + path + ":" + alarm_config); if (alarm_config != null) { path = path.replaceAll("[:|?*]", "_"); @@ -223,17 +255,17 @@ private synchronized void processAlarmConfigMessages(String rawPath, String alar } else { path = path.replaceAll("[:|?*]", "_"); Path directory = Paths.get(root.getParent(), path); - if(directory.toFile().exists()) { + if (directory.toFile().exists()) { Files.walk(directory).map(Path::toFile).forEach(File::delete); directory.toFile().delete(); } } writeAlarmModel(); - if(commit) { - // Commit the initialized git repo + if (commit) { + // Commit the initialized git repo try (Git git = Git.open(root)) { git.add().addFilepattern(".").call(); - git.commit().setAll(true).setMessage("Alarm config update "+path).call(); + git.commit().setAll(true).setMessage("Alarm config update " + path).call(); // Check if it is configured with the appropriate remotes if (remoteLocation != null && !remoteLocation.isEmpty()) { @@ -241,17 +273,20 @@ private synchronized void processAlarmConfigMessages(String rawPath, String alar PushCommand pushCommand = git.push(); pushCommand.setRemote(REMOTE_NAME); pushCommand.setForce(true); - pushCommand.setCredentialsProvider( - new UsernamePasswordCredentialsProvider( - props.getProperty("username"), - props.getProperty("password")) - ); + if (Boolean.parseBoolean(props.getProperty("use_ssh_keys"))) { + pushCommand.setTransportConfigCallback(transport -> { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(sshdSessionFactory); + }); + } else if (usernamePasswordCredentialsProvider != null) { + pushCommand.setCredentialsProvider(usernamePasswordCredentialsProvider); + } pushCommand.call(); } } catch (GitAPIException | IOException e) { logger.log(Level.WARNING, "Failed to commit the configuration changes", e); } - } + } } } catch (final Exception ex) { logger.log(Level.WARNING, "Alarm state check error for path " + rawPath + ", config " + alarm_config, ex); diff --git a/services/alarm-config-logger/src/main/resources/alarm_config_logger.properties b/services/alarm-config-logger/src/main/resources/alarm_config_logger.properties index 93e44ec80f..f3186fcdae 100644 --- a/services/alarm-config-logger/src/main/resources/alarm_config_logger.properties +++ b/services/alarm-config-logger/src/main/resources/alarm_config_logger.properties @@ -8,7 +8,17 @@ local.location=/tmp/alarm_repo # location of the remote git repo, # The complete URI of the remote is created using the remote.location -# it is recomended that your remote url end in the alarm topic assigned to this config service alarm_topic -remote.location=https://remote.git/repo +# it is recommended that your remote url end in the alarm topic assigned to this config service alarm_topic +# e.g. remote URL +# ssh://shroffk@localhost:2001/home/shroffk/git/alarm-config +# git@github.com:shroffk/alarm-config.git +remote.location=git@github.com:site-org/alarm-config.git + +# use ssh keys to push to remote repo +use_ssh_keys=false + +# complete path to the ssh key to be used, if not set then the server will use the private keys from ~/.ssh/ +#private_key= + username=username password=password diff --git a/services/alarm-logger/README.md b/services/alarm-logger/README.md index 6b62dcca19..cd73a9618e 100644 --- a/services/alarm-logger/README.md +++ b/services/alarm-logger/README.md @@ -103,3 +103,15 @@ One or more indices can be deleted with the following command: ``` curl -X DELETE 'localhost:9200/accelerator_alarms_state_2019-02-*' ``` + +## Release + +**Prepare the release** +`mvn release:prepare` +In this step will ensure there are no uncommitted changes, ensure the versions number are correct, tag the scm, etc. +A full list of checks is documented [here](https://maven.apache.org/maven-release/maven-release-plugin/examples/prepare-release.html). + +**Perform the release** +`mvn -Darguments="-Dskip-executable-jar" -Pdocs,releases release:perform` +Checkout the release tag, build, sign and push the build binaries to sonatype. The `docs` profile is needed in order +to create required javadocs jars. \ No newline at end of file diff --git a/services/alarm-logger/pom.xml b/services/alarm-logger/pom.xml index 46d16fa311..7f014d6b3f 100644 --- a/services/alarm-logger/pom.xml +++ b/services/alarm-logger/pom.xml @@ -3,7 +3,7 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 1.11 @@ -95,12 +95,12 @@ org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -111,7 +111,7 @@ - + org.slf4j slf4j-jdk14 1.7.28 @@ -142,20 +142,53 @@ + + + + + executable-jar + + + !skip-executable-jar + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.9 + + + + repackage + + + + + + + + + + + + src/main/resources + true + + **/application.properties + + + + src/main/resources + false + + **/application.properties + + + - - org.springframework.boot - spring-boot-maven-plugin - 2.1.6.RELEASE - - - - repackage - - - - maven-assembly-plugin 3.2.0 diff --git a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/AlarmCmdLogger.java b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/AlarmCmdLogger.java index 964c32c988..9b7807d574 100644 --- a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/AlarmCmdLogger.java +++ b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/AlarmCmdLogger.java @@ -89,7 +89,7 @@ public long extract(ConsumerRecord record, long previousTimestam })); final String indexDateSpanUnits = props.getProperty("date_span_units"); - final boolean useDatedIndexNames = Boolean.getBoolean(props.getProperty("use_dated_index_names")); + final boolean useDatedIndexNames = Boolean.parseBoolean(props.getProperty("use_dated_index_names")); try { indexNameHelper = new IndexNameHelper(topic + INDEX_FORMAT, useDatedIndexNames, indexDateSpanUnits); diff --git a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/ElasticClientHelper.java b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/ElasticClientHelper.java index ab465193ac..af8b1b147f 100644 --- a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/ElasticClientHelper.java +++ b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/ElasticClientHelper.java @@ -5,11 +5,8 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.Refresh; -import co.elastic.clients.elasticsearch._types.Result; import co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.BulkResponse; -import co.elastic.clients.elasticsearch.core.IndexRequest; -import co.elastic.clients.elasticsearch.core.IndexResponse; import co.elastic.clients.elasticsearch.indices.ExistsIndexTemplateRequest; import co.elastic.clients.elasticsearch.indices.PutIndexTemplateRequest; import co.elastic.clients.elasticsearch.indices.PutIndexTemplateResponse; @@ -47,7 +44,6 @@ * A Utility service to allow for batched indexing of alarm state, config, and command messages to an elastic backend * * @author Kunal Shroff {@literal } - * */ public class ElasticClientHelper { Properties props = PropertiesHelper.getProperties(); @@ -65,9 +61,11 @@ public class ElasticClientHelper { private static final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(4); ScheduledFuture job; // State messages to be indexed - BlockingQueue> stateMessagedQueue = new LinkedBlockingDeque<>(); + BlockingQueue> stateMessagedQueue = new LinkedBlockingDeque<>(); // State messages to be indexed - BlockingQueue> configMessagedQueue = new LinkedBlockingDeque<>(); + BlockingQueue> configMessagedQueue = new LinkedBlockingDeque<>(); + + BlockingQueue> commandMessagedQueue = new LinkedBlockingDeque<>(); private final ObjectMapper mapper = new ObjectMapper(); @@ -80,8 +78,7 @@ private ElasticClientHelper() { client.shutdown(); transport.close(); restClient.close(); - } - catch (IOException ex){ + } catch (IOException ex) { logger.log(Level.WARNING, "Failed to close the elastic client.", ex); } } @@ -89,7 +86,7 @@ private ElasticClientHelper() { // Create the low-level client restClient = RestClient.builder( - new HttpHost(props.getProperty("es_host"),Integer.parseInt(props.getProperty("es_port")))).build(); + new HttpHost(props.getProperty("es_host"), Integer.parseInt(props.getProperty("es_port")))).build(); mapper.registerModule(new JavaTimeModule()); transport = new RestClientTransport( @@ -105,7 +102,7 @@ private ElasticClientHelper() { esInitialized.set(!Boolean.parseBoolean(props.getProperty("es_create_templates"))); // Start the executor for periodically logging into es - job = scheduledExecutorService.scheduleAtFixedRate(new flush2Elastic(stateMessagedQueue, configMessagedQueue), + job = scheduledExecutorService.scheduleAtFixedRate(new flush2Elastic(stateMessagedQueue, configMessagedQueue, commandMessagedQueue), 0, 250, TimeUnit.MILLISECONDS); } catch (Exception e) { try { @@ -133,12 +130,13 @@ public ElasticsearchClient getClient() { /** * Index an alarm state message - * @param indexName - * @param alarmStateMessage + * + * @param indexName Name of Elasticsearch index, e.g. myConfig_alarms_state_yyyy-MM-dd + * @param alarmStateMessage Object holding alarm state message */ public void indexAlarmStateDocuments(String indexName, AlarmStateMessage alarmStateMessage) { try { - stateMessagedQueue.put(new SimpleImmutableEntry<>(indexName,alarmStateMessage)); + stateMessagedQueue.put(new SimpleImmutableEntry<>(indexName, alarmStateMessage)); } catch (InterruptedException e) { logger.log(Level.SEVERE, "failed to log message " + alarmStateMessage + " to index " + indexName, e); } @@ -146,32 +144,27 @@ public void indexAlarmStateDocuments(String indexName, AlarmStateMessage alarmSt /** * Index an alarm command message - * @param indexName - * @param alarmCommandMessage - * @return + * + * @param indexName Name of Elasticsearch index, e.g. myConfig_alarms_cmd_yyyy-MM-dd + * @param alarmCommandMessage Object holding alarm command message */ - public boolean indexAlarmCmdDocument(String indexName, AlarmCommandMessage alarmCommandMessage) { - IndexRequest indexRequest = new IndexRequest.Builder() - .index(indexName.toLowerCase()) - .document(alarmCommandMessage) - .build(); + public void indexAlarmCmdDocument(String indexName, AlarmCommandMessage alarmCommandMessage) { try { - IndexResponse indexResponse = client.index(indexRequest); - return indexResponse.result().equals(Result.Created); - } catch (IOException e) { - logger.log(Level.SEVERE, "failed to log message " + alarmCommandMessage + " to index " + indexName, e); - return false; + commandMessagedQueue.put(new SimpleImmutableEntry<>(indexName, alarmCommandMessage)); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "failed to log command message " + alarmCommandMessage + " to index " + indexName, e); } } /** * Index an alarm config message - * @param indexName - * @param alarmConfigMessage + * + * @param indexName Name of Elasticsearch index, e.g. myConfig_alarms_config_yyyy-MM-dd + * @param alarmConfigMessage Object holding alarm config message */ public void indexAlarmConfigDocuments(String indexName, AlarmConfigMessage alarmConfigMessage) { try { - configMessagedQueue.put(new SimpleImmutableEntry<>(indexName,alarmConfigMessage)); + configMessagedQueue.put(new SimpleImmutableEntry<>(indexName, alarmConfigMessage)); } catch (InterruptedException e) { logger.log(Level.SEVERE, "failed to log message " + alarmConfigMessage + " to index " + indexName, e); } @@ -182,13 +175,16 @@ public void indexAlarmConfigDocuments(String indexName, AlarmConfigMessage alarm */ private static class flush2Elastic implements Runnable { - private final BlockingQueue> stateMessagedQueue; - private final BlockingQueue> configMessagedQueue; + private final BlockingQueue> stateMessagedQueue; + private final BlockingQueue> configMessagedQueue; + private final BlockingQueue> commandMessagedQueue; - public flush2Elastic(BlockingQueue> stateMessagedQueue, - BlockingQueue> configMessagedQueue) { + public flush2Elastic(BlockingQueue> stateMessagedQueue, + BlockingQueue> configMessagedQueue, + BlockingQueue> commandMessagedQueue) { this.stateMessagedQueue = stateMessagedQueue; this.configMessagedQueue = configMessagedQueue; + this.commandMessagedQueue = commandMessagedQueue; } @Override @@ -200,51 +196,61 @@ public void run() { logger.log(Level.SEVERE, "failed to create the alarm log indices ", e); } } - if(stateMessagedQueue.size() + configMessagedQueue.size() > 0){ + if (stateMessagedQueue.size() + configMessagedQueue.size() > 0) { logger.log(Level.INFO, "batch execution of : " + stateMessagedQueue.size() + " state messages and " + configMessagedQueue.size() + " config messages"); BulkRequest.Builder bulkRequest = new BulkRequest.Builder().refresh(Refresh.True); - Collection> statePairs = new ArrayList<>(); + Collection> statePairs = new ArrayList<>(); stateMessagedQueue.drainTo(statePairs); - Collection> configPairs = new ArrayList<>(); + Collection> configPairs = new ArrayList<>(); configMessagedQueue.drainTo(configPairs); - statePairs.forEach( pair -> bulkRequest.operations(op -> op + Collection> commandPairs = new ArrayList<>(); + commandMessagedQueue.drainTo(commandPairs); + statePairs.forEach(pair -> bulkRequest.operations(op -> op .index(idx -> idx .index(pair.getKey().toLowerCase()) .document(pair.getValue().sourceMap())))); - configPairs.forEach( pair -> bulkRequest.operations(op->op - .index(idx->idx + configPairs.forEach(pair -> bulkRequest.operations(op -> op + .index(idx -> idx + .index(pair.getKey().toLowerCase()) + .document(pair.getValue().sourceMap())))); + commandPairs.forEach(pair -> bulkRequest.operations(op -> op + .index(idx -> idx .index(pair.getKey().toLowerCase()) .document(pair.getValue().sourceMap())))); try { BulkResponse bulkResponse = client.bulk(bulkRequest.build()); - bulkResponse.items().forEach(item -> { - if (item.error()!=null) { - logger.log(Level.SEVERE, "Failed while indexing to " + item.index() + " type " - + item.operationType() + item.error().reason() + "]"); - } + bulkResponse.items().forEach(item -> { + if (item.error() != null) { + logger.log(Level.SEVERE, "Failed while indexing to " + item.index() + " type " + + item.operationType() + item.error().reason() + "]"); } - ); + } + ); } catch (IOException e) { logger.log(Level.SEVERE, "failed to log messages to index ", e); } } } - private static Properties props = new Properties(); - { + + private static final Properties props = new Properties(); + + static { props.putAll(PropertiesHelper.getProperties()); } - private String ALARM_STATE_TEMPLATE = props.getProperty("elasticsearch.alarm.state.template","alarms_state_template"); - private String ALARM_STATE_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.state.template.pattern","*_alarms_state*"); - private String ALARM_CMD_TEMPLATE = props.getProperty("elasticsearch.alarm.cmd.template","alarms_cmd_template"); - private String ALARM_CMD_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.cmd.template.pattern","*_alarms_cmd*"); + private final String ALARM_STATE_TEMPLATE = props.getProperty("elasticsearch.alarm.state.template", "alarms_state_template"); + private final String ALARM_STATE_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.state.template.pattern", "*_alarms_state*"); + + private final String ALARM_CMD_TEMPLATE = props.getProperty("elasticsearch.alarm.cmd.template", "alarms_cmd_template"); + private final String ALARM_CMD_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.cmd.template.pattern", "*_alarms_cmd*"); - private String ALARM_CONFIG_TEMPLATE = props.getProperty("elasticsearch.alarm.config.template","alarms_config_template"); - private String ALARM_CONFIG_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.config.template.pattern","*_alarms_config*"); + private final String ALARM_CONFIG_TEMPLATE = props.getProperty("elasticsearch.alarm.config.template", "alarms_config_template"); + private final String ALARM_CONFIG_TEMPLATE_PATTERN = props.getProperty("elasticsearch.alarm.config.template.pattern", "*_alarms_config*"); /** * Check if the required templated for the phoebus alarm logs exists, if not create them. - * @throws IOException + * + * @throws IOException if Elasticsearch interaction fails */ public void initializeIndices() throws IOException { // Create the alarm state messages index template diff --git a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/AlarmLogSearchUtil.java b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/AlarmLogSearchUtil.java index 83cde405d7..841a21d6a1 100644 --- a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/AlarmLogSearchUtil.java +++ b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/AlarmLogSearchUtil.java @@ -5,6 +5,7 @@ import co.elastic.clients.elasticsearch._types.SortOptions; import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery; @@ -26,6 +27,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAmount; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -77,7 +79,7 @@ public static List search(ElasticsearchClient client, logger.info("searching for alarm log entires : " + searchParameters.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).collect(Collectors.joining())); - Instant fromInstant = Instant.now().minus(7, ChronoUnit.DAYS); + Instant fromInstant = Instant.EPOCH; Instant toInstant = Instant.now(); // The maximum search result size @@ -89,8 +91,7 @@ public static List search(ElasticsearchClient client, BoolQuery.Builder boolQuery = new BoolQuery.Builder(); List indexList = new ArrayList<>(); - - String root = ""; + List alarmConfigs = new ArrayList<>(); for (Map.Entry parameter : searchParameters.entrySet()) { switch (parameter.getKey().strip().toLowerCase()) { @@ -135,24 +136,29 @@ public static List search(ElasticsearchClient client, break; case ROOT: if (!parameter.getValue().equalsIgnoreCase("*")) { - root = parameter.getValue().strip(); - String _root = root; - boolQuery.must( - Query.of(b -> b.bool(s -> s.should( - Query.of(q -> q - .wildcard(WildcardQuery.of(w -> w - .field("config").value("state:/" + _root + "*") - ) - ) - ), - Query.of(q -> q - .wildcard(WildcardQuery.of(w -> w - .field("config").value("config:/" + _root + "*") - ) - ) - ) - ))) - ); + DisMaxQuery.Builder alarmConfigQuery = new DisMaxQuery.Builder(); + List alarmConfigQueries = new ArrayList<>(); + // Construct a list of alarm config names + alarmConfigs = + Arrays.stream(parameter.getValue().split(",")).map(s -> s.trim()).collect(Collectors.toList()); + for (String alarmConfig : alarmConfigs) { + alarmConfigQueries.add(Query.of(b -> b.bool(s -> s.should( + Query.of(q -> q + .wildcard(WildcardQuery.of(w -> w + .field("config").value("state:/" + alarmConfig + "*") + ) + ) + ), + Query.of(q -> q + .wildcard(WildcardQuery.of(w -> w + .field("config").value("config:/" + alarmConfig + "*") + ) + ) + ) + )))); + } + Query configsQuery = alarmConfigQuery.queries(alarmConfigQueries).build()._toQuery(); + boolQuery.must(configsQuery); configSet = true; } break; @@ -239,9 +245,11 @@ public static List search(ElasticsearchClient client, ); try { - // "root" is empty string unless user specifies one, in which case we can narrow down to + // "root" is empty string unless user specifies a list of alarm configs, in which case we can narrow down to // only matching alarm config indices. - indexList = findIndexNames(root.toLowerCase() + "*", fromInstant, toInstant, indexDateSpanUnits); + for(String alarmConfig : alarmConfigs){ + indexList.addAll(findIndexNames(alarmConfig, fromInstant, toInstant, indexDateSpanUnits)); + } } catch (Exception e) { logger.log(Level.SEVERE, "Failed to search for alarm logs:" + e.getMessage(), e); @@ -292,8 +300,9 @@ public static List search(ElasticsearchClient client, */ public static List searchConfig(ElasticsearchClient client, Map allRequestParams) { String configString = allRequestParams.get("config"); - // Determine which alarm config to specify as Elasticsearch index - String alarmConfig = configString.split("/")[1]; + // Determine which alarm config to specify as Elasticsearch index, convert to lower case as + // indices are created using lower case. + String alarmConfig = configString.split("/")[1].toLowerCase(); String searchPattern = "*".concat(configString).concat("*"); int size = 1; @@ -331,22 +340,22 @@ public static List searchConfig(ElasticsearchClient client, Map /** * return a list of index names between the from and to instant - * - * @param fromInstant From time - * @param toInstant To time + * @param baseIndexName A lower case index base name, which should be same as an alarm config name. + * @param fromInstant From time + * @param toInstant To time * @param indexDateSpanUnits Date span unit (Y, M, D...) * @return List of index names * @throws Exception If index names cannot be determined */ public static List findIndexNames(String baseIndexName, Instant fromInstant, Instant toInstant, String indexDateSpanUnits) throws Exception { + List indexList = new ArrayList<>(); - IndexNameHelper fromIndexNameHelper = new IndexNameHelper(baseIndexName, true, indexDateSpanUnits); - IndexNameHelper toIndexNameHelper = new IndexNameHelper(baseIndexName, true, indexDateSpanUnits); + IndexNameHelper fromIndexNameHelper = new IndexNameHelper(baseIndexName.toLowerCase() + "*", true, indexDateSpanUnits); + IndexNameHelper toIndexNameHelper = new IndexNameHelper(baseIndexName.toLowerCase() + "*", true, indexDateSpanUnits); String fromIndex = fromIndexNameHelper.getIndexName(fromInstant); String toIndex = toIndexNameHelper.getIndexName(toInstant); - List indexList = new ArrayList<>(); if (fromInstant.isBefore(toInstant)) { if (fromIndex.equalsIgnoreCase(toIndex)) { indexList.add(fromIndex); @@ -374,6 +383,7 @@ public static List findIndexNames(String baseIndexName, Instant fromInst } } } + return indexList; } } diff --git a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/SearchController.java b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/SearchController.java index e1fca46b22..614483b024 100644 --- a/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/SearchController.java +++ b/services/alarm-logger/src/main/java/org/phoebus/alarm/logging/rest/SearchController.java @@ -7,12 +7,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.phoebus.alarm.logging.AlarmLoggingService; import org.phoebus.alarm.logging.ElasticClientHelper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import java.io.IOException; import java.util.HashMap; @@ -35,6 +38,9 @@ public class SearchController { private static final ObjectMapper objectMapper = new ObjectMapper(); + @Value("${version:1.0.0}") + private String version; + /** * @return Information about the alarm logging service */ @@ -43,7 +49,7 @@ public String info() { Map alarmLoggingServiceInfo = new LinkedHashMap(); alarmLoggingServiceInfo.put("name", "Alarm logging Service"); - //alarmLoggingServiceInfo.put("version", version); + alarmLoggingServiceInfo.put("version", version); Map elasticInfo = new LinkedHashMap(); try { @@ -84,6 +90,12 @@ public List searchPv(@PathVariable String pv) { @RequestMapping(value = "/search/alarm/config", method = RequestMethod.GET) public List searchConfig(@RequestParam Map allRequestParams) { + if(allRequestParams == null || + allRequestParams.isEmpty() || + !allRequestParams.containsKey("config") || + allRequestParams.get("config").isEmpty()){ + throw new ResponseStatusException(HttpStatus.BAD_REQUEST); + } List result = AlarmLogSearchUtil.searchConfig(ElasticClientHelper.getInstance().getClient(), allRequestParams); return result; } diff --git a/services/alarm-logger/src/main/resources/application.properties b/services/alarm-logger/src/main/resources/application.properties index 611bb5d11b..d35c1565dd 100644 --- a/services/alarm-logger/src/main/resources/application.properties +++ b/services/alarm-logger/src/main/resources/application.properties @@ -1,24 +1,29 @@ +version=@project.version@ + # The server port for the rest service server.port=8080 # Disable the spring banner spring.main.banner-mode=off +# Disable the auto configured springboot elastic client +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration + # Suppress the logging from spring boot during debugging this should be set to DEBUG logging.level.root=WARN # Alarm topics to be logged, they can be defined as a comma separated list alarm_topics=Accelerator -# location of elastic node/s +# Location of elastic node/s es_host=localhost es_port=9200 -# max default size for es queries +# Max default size for es queries es_max_size=1000 -# set to 'true' if sniffing to be enabled to discover other cluster nodes +# Set to 'true' if sniffing to be enabled to discover other cluster nodes es_sniff=false -# when set to true, the service will automatically create the index templates needed +# When set to true, the service will automatically create the index templates needed es_create_templates=true # Kafka server location diff --git a/services/alarm-server/pom.xml b/services/alarm-server/pom.xml index f510976a27..0d47cbd28d 100644 --- a/services/alarm-server/pom.xml +++ b/services/alarm-server/pom.xml @@ -2,7 +2,7 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 4.0.0 service-alarm-server @@ -24,33 +24,33 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-formula - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-email - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-alarm-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -83,7 +83,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.3.0 @@ -103,7 +103,7 @@ --> maven-assembly-plugin - 3.1.1 + 3.5.0 posix @@ -120,42 +120,6 @@ - - maven-antrun-plugin - - - verify - - true - - - - - - - - - - - - - - - - - - - - - - - - run - - - - org.sonatype.plugins nexus-staging-maven-plugin diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmConfigTool.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmConfigTool.java index 85fae11d5a..a6ae5fb71a 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmConfigTool.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmConfigTool.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -28,14 +28,17 @@ @SuppressWarnings("nls") public class AlarmConfigTool { + /** Timeout for receiving the first config update, a quasi connection timeout. Default is 10 seconds. */ + public static long CONNECTION_SECS = 10; + /** Time the model must be stable for. Unit is seconds. Default is 4 seconds. */ - private static final long STABILIZATION_SECS = 4; + public static long STABILIZATION_SECS = 4; - // Export an alarm system model to an xml file. + // Export an alarm system model to an xml file. public void exportModel(String filename, String server, String config, String kafka_properties_file) throws Exception - { + { final XmlModelWriter xmlWriter; - + // Write to stdout or to file. if (filename.equals("stdout")) xmlWriter = new XmlModelWriter(System.out); @@ -54,10 +57,11 @@ public void exportModel(String filename, String server, String config, String ka final AlarmClient client = new AlarmClient(server, config, kafka_properties_file); client.start(); - System.out.printf("Writing file after model is stable for %d seconds:\n", STABILIZATION_SECS); + System.out.printf("Writing file after %d second connection timeout, then waiting until model is stable for %d seconds:\n", + CONNECTION_SECS, STABILIZATION_SECS); System.out.println("Monitoring changes..."); - final AlarmConfigMonitor updateMonitor = new AlarmConfigMonitor(STABILIZATION_SECS, client); + final AlarmConfigMonitor updateMonitor = new AlarmConfigMonitor(CONNECTION_SECS, STABILIZATION_SECS, client); updateMonitor.waitForPauseInUpdates(30); System.out.printf("Received no more updates for %d seconds, I think I have a stable configuration\n", STABILIZATION_SECS); @@ -75,18 +79,18 @@ public void exportModel(String filename, String server, String config, String ka updateMonitor.dispose(); client.shutdown(); - } + } - // Import an alarm system model from an xml file. - public void importModel(final String filename, final String server, final String config, String kafka_properties_file) throws InterruptedException, Exception - { - System.out.println("Reading new configuration from " + filename); - final long start = System.currentTimeMillis(); - final File file = new File(filename); - final FileInputStream fileInputStream = new FileInputStream(file); + // Import an alarm system model from an xml file. + public void importModel(final String filename, final String server, final String config, String kafka_properties_file) throws InterruptedException, Exception + { + System.out.println("Reading new configuration from " + filename); + final long start = System.currentTimeMillis(); + final File file = new File(filename); + final FileInputStream fileInputStream = new FileInputStream(file); - final XmlModelReader xmlModelReader = new XmlModelReader(); - xmlModelReader.load(fileInputStream); + final XmlModelReader xmlModelReader = new XmlModelReader(); + xmlModelReader.load(fileInputStream); final AlarmClientNode new_root = xmlModelReader.getRoot(); // Check that the configs match. @@ -97,13 +101,14 @@ public void importModel(final String filename, final String server, final String } final long got_xml = System.currentTimeMillis(); - // Connect to the server. - final AlarmClient client = new AlarmClient(server, config, kafka_properties_file); + // Connect to the server. + final AlarmClient client = new AlarmClient(server, config, kafka_properties_file); client.start(); try { - System.out.println("Fetching existing alarm configuration for \"" + config + "\", then waiting for it to remain stable for " + STABILIZATION_SECS + " seconds..."); - final AlarmConfigMonitor updateMonitor = new AlarmConfigMonitor(STABILIZATION_SECS, client); + System.out.println("Fetching existing alarm configuration for \"" + config + "\" with " + CONNECTION_SECS + + " connection timeout, then waiting for it to remain stable for " + STABILIZATION_SECS + " seconds..."); + final AlarmConfigMonitor updateMonitor = new AlarmConfigMonitor(CONNECTION_SECS, STABILIZATION_SECS, client); updateMonitor.waitForPauseInUpdates(30); updateMonitor.dispose(); final long got_old_config = System.currentTimeMillis(); @@ -116,7 +121,7 @@ public void importModel(final String filename, final String server, final String // Delete the old model. Leave the root node. final List> root_children = root.getChildren(); for (final AlarmTreeItem child : root_children) - client.removeComponent(child); + client.removeComponent(child); final long deleted_old = System.currentTimeMillis(); System.out.println("Loading new " + new_root.getName() + " ..."); @@ -124,7 +129,7 @@ public void importModel(final String filename, final String server, final String // For every child of the new root, add them and their descendants to the old root. final List> new_root_children = new_root.getChildren(); for (final AlarmTreeItem child : new_root_children) - addNodes(client, root, child); + addNodes(client, root, child); final long loaded_new = System.currentTimeMillis(); System.out.println("Time to read XML : " + SecondsParser.formatSeconds((got_xml - start) / 1000.0)); @@ -138,16 +143,16 @@ public void importModel(final String filename, final String server, final String { client.shutdown(); } - } - - private void addNodes(final AlarmClient client, final AlarmTreeItem parent, final AlarmTreeItem tree_item) throws Exception - { - // Send the configuration for the newly created node. - client.sendItemConfigurationUpdate(tree_item.getPathName(), tree_item); - - // Recurse over children. - final List> children = tree_item.getChildren(); - for (final AlarmTreeItem child : children) - addNodes(client, tree_item, child); - } + } + + private void addNodes(final AlarmClient client, final AlarmTreeItem parent, final AlarmTreeItem tree_item) throws Exception + { + // Send the configuration for the newly created node. + client.sendItemConfigurationUpdate(tree_item.getPathName(), tree_item); + + // Recurse over children. + final List> children = tree_item.getChildren(); + for (final AlarmTreeItem child : children) + addNodes(client, tree_item, child); + } } diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmLogic.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmLogic.java index ae422ea8a0..4f54528b64 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmLogic.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmLogic.java @@ -37,8 +37,8 @@ * Abstract base of AlarmPV to allow tests independent from actual * control system connection. * - * @see AlarmPV - * @see AlarmLogicUnitTest + * @see AlarmServerPV + * see AlarmLogicUnitTest * @author Kay Kasemir */ @SuppressWarnings("nls") @@ -218,7 +218,7 @@ public boolean isEnabled() return enabled.get(); } - /** @param latch Annunciate alarms from the PV? + /** @param annunciate Annunciate alarms from the PV? * @return true if this is a change */ public boolean setAnnunciating(final boolean annunciate) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java index 3f90ea39df..225eb86564 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2022 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -87,7 +87,9 @@ private AlarmServerMain(final String server, final String config, final boolean { logger.info("Fetching past alarm states..."); final AlarmStateInitializer init = new AlarmStateInitializer(server, config, kafka_props_file); - if (! init.awaitCompleteStates()) + if (init.awaitCompleteStates()) + logger.log(Level.INFO, "Alarm state stabilized"); + else logger.log(Level.WARNING, "Keep receiving state updates, may have incomplete initial set of alarm states"); final ConcurrentHashMap initial_states = init.shutdown(); @@ -542,6 +544,8 @@ private static void help() System.out.println("-export config.xml - Export alarm configuration to file"); System.out.println("-import config.xml - Import alarm configruation from file"); System.out.println("-logging logging.properties - Load log settings"); + System.out.println("-connect_secs 10 - Time alarm server and config import/export waits for connection"); + System.out.println("-stable_secs 4 - Time alarm server and config import/export waits for stable configuration"); System.out.println("-kafka_properties client.properties - Load kafka client settings from file"); System.out.println(); } @@ -573,6 +577,8 @@ public static void main(final String[] original_args) throws Exception String export_arg = "-export"; String import_arg = "-import"; String logging_arg = "-logging"; + String connect_secs_arg = "-connect_secs"; + String stable_secs_arg = "-stable_secs"; String kafka_props_arg = "-kafka_properties"; Set options = Set.of( @@ -582,6 +588,8 @@ public static void main(final String[] original_args) throws Exception export_arg, import_arg, logging_arg, + connect_secs_arg, + stable_secs_arg, kafka_props_arg); Set flags = Set.of( @@ -602,45 +610,46 @@ public static void main(final String[] original_args) throws Exception while (iter.hasNext()) { final String cmd = iter.next(); - if (options.contains(cmd)) { + if (options.contains(cmd)) + { if (! iter.hasNext()) throw new Exception("Missing argument for " + cmd); final String arg = iter.next(); parsed_args.put(cmd, arg); } - else if (flags.contains(cmd)) { + else if (flags.contains(cmd)) parsed_args.put(cmd, ""); - } - else { + else throw new Exception("Unknown option " + cmd); - } } - if (parsed_args.containsKey(help_arg) || parsed_args.containsKey(help_alt_arg)){ + if (parsed_args.containsKey(help_arg) || parsed_args.containsKey(help_alt_arg)) + { help(); return; } - if (parsed_args.containsKey(logging_arg)) { + if (parsed_args.containsKey(logging_arg)) LogManager.getLogManager().readConfiguration(new FileInputStream(parsed_args.get(logging_arg))); - } - if (parsed_args.containsKey(settings_arg)){ + if (parsed_args.containsKey(settings_arg)) + { final String filename = parsed_args.get(settings_arg); logger.info("Loading settings from " + filename); PropertyPreferenceLoader.load(new FileInputStream(filename)); - Preferences userPrefs = Preferences.userRoot().node("org/phoebus/applications/alarm"); + final Preferences userPrefs = Preferences.userRoot().node("org/phoebus/applications/alarm"); - for (Map.Entry entry: args_to_prefs.entrySet()) { + for (Map.Entry entry: args_to_prefs.entrySet()) + { final String prefKey = entry.getValue(); final String arg = entry.getKey(); - if (parsed_args.containsKey(arg)){ + if (parsed_args.containsKey(arg)) + { logger.log(Level.WARNING,"Potentially conflicting setting: -settings/"+prefKey+": " + userPrefs.get(prefKey, "") + " and " + arg + ":" + parsed_args.get(arg)); logger.log(Level.WARNING,"Using argument " + arg + " instead of -settings"); logger.log(Level.WARNING,prefKey + ": " + parsed_args.get(arg)); } - else if (Set.of(userPrefs.keys()).contains(prefKey)){ + else if (Set.of(userPrefs.keys()).contains(prefKey)) parsed_args.put(arg, userPrefs.get(prefKey, "")); - } } } @@ -648,27 +657,37 @@ else if (Set.of(userPrefs.keys()).contains(prefKey)){ server = parsed_args.getOrDefault(server_arg, server); kafka_properties = parsed_args.getOrDefault(kafka_props_arg, kafka_properties); use_shell = !parsed_args.containsKey(noshell_arg); + + if (parsed_args.containsKey(connect_secs_arg)) + AlarmStateInitializer.CONNECTION_SECS = AlarmConfigTool.CONNECTION_SECS + = Long.parseLong(parsed_args.get(connect_secs_arg)); + + if (parsed_args.containsKey(stable_secs_arg)) + AlarmStateInitializer.STABILIZATION_SECS = AlarmConfigTool.STABILIZATION_SECS + = Long.parseLong(parsed_args.get(stable_secs_arg)); - if (parsed_args.containsKey(create_topics_arg)){ + if (parsed_args.containsKey(create_topics_arg)) + { logger.info("Discovering and creating any missing topics at " + server); CreateTopics.discoverAndCreateTopics(server, true, List.of(config, config + AlarmSystemConstants.COMMAND_TOPIC_SUFFIX, config + AlarmSystemConstants.TALK_TOPIC_SUFFIX), kafka_properties); } - if (parsed_args.containsKey(export_arg)){ + if (parsed_args.containsKey(export_arg)) + { final String filename = parsed_args.get(export_arg); logger.info("Exporting model to " + filename); new AlarmConfigTool().exportModel(filename, server, config, kafka_properties); } - if (parsed_args.containsKey(import_arg)){ + if (parsed_args.containsKey(import_arg)) + { final String filename = parsed_args.get(import_arg); logger.info("Import model from " + filename); new AlarmConfigTool().importModel(filename, server, config, kafka_properties); } - if (parsed_args.containsKey(export_arg) || parsed_args.containsKey(import_arg)){ + if (parsed_args.containsKey(export_arg) || parsed_args.containsKey(import_arg)) return; - } } catch (final Exception ex) { diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerNode.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerNode.java index d032312423..7021d96ff6 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerNode.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerNode.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2021 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -125,6 +125,8 @@ public boolean setActions(final List actions) if (! Objects.equals(this.severity_pv_name, severity_pv_name)) { + logger.log(Level.INFO, "Changing severity PV for " + getPathName() + " from '" + this.severity_pv_name + "' to '" + severity_pv_name + "'"); + SeverityPVHandler.clear(this.severity_pv_name); this.severity_pv_name = severity_pv_name; // Initial update, since severity may not change for a while if (severity_pv_name != null) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerPV.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerPV.java index 25d315a9c7..46f6972cf0 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerPV.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerPV.java @@ -198,7 +198,7 @@ public boolean setEnabled(final boolean enable) return true; } - /** @param enable Enable the PV? + /** @param enabled_state Enable the PV? * @return true if this is a change * Set as listener to enable */ @@ -214,7 +214,7 @@ public boolean setEnabled(final EnabledState enabled_state) return true; } - /** @param enable Enable the PV? + /** @param enabled_date Enable the PV? * @return true if this is a change */ @Override diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateHistory.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateHistory.java index fa9d87a8d6..aa042e5d36 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateHistory.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateHistory.java @@ -45,7 +45,6 @@ public int getCount() /** Add alarm state to history * @param state New state - * @param timestamp Time stamp for that state */ public void add(final AlarmState state) { diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateInitializer.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateInitializer.java index 2b4b774733..09febcafab 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateInitializer.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmStateInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -43,7 +43,13 @@ @SuppressWarnings("nls") public class AlarmStateInitializer { - private final ResettableTimeout timer = new ResettableTimeout(4); + /** Timeout for receiving the first update, a quasi connection timeout. Default is 10 seconds. */ + public static long CONNECTION_SECS = 10; + + /** Time the model must be stable for. Unit is seconds. Default is 4 seconds. */ + public static long STABILIZATION_SECS = 4; + + private final ResettableTimeout timer = new ResettableTimeout(CONNECTION_SECS); private final AtomicBoolean running = new AtomicBoolean(true); private final Consumer consumer; private final Thread thread; @@ -51,7 +57,7 @@ public class AlarmStateInitializer /** @param server Kafka Server host:port * @param config_name Name of alarm tree root - * @param kafka_props Additional properties to pass to the kafka client + * @param kafka_props_file Additional properties to pass to the kafka client */ public AlarmStateInitializer(final String server, final String config_name, final String kafka_props_file) { @@ -105,7 +111,7 @@ private void checkUpdates() if (node_config == null) { // No config -> Delete node inititial_severity.remove(path); - timer.reset(); + timer.reset(STABILIZATION_SECS); } else { @@ -119,7 +125,7 @@ private void checkUpdates() inititial_severity.remove(path); else inititial_severity.put(path, state); - timer.reset(); + timer.reset(STABILIZATION_SECS); } } } diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java index 6021d88ab9..6c6fb2ad1e 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import org.apache.kafka.clients.consumer.Consumer; @@ -76,22 +77,23 @@ class ServerModel /** Did the last connectivity check fail? */ private boolean connection_lost = false; + /** + * Timeout in seconds waiting for response from Kafka when sending producer messages. + */ + private static final int KAFKA_CLIENT_TIMEOUT = 10; /** @param kafka_servers Servers * @param config_name Name of alarm tree root * @param initial_states * @param listener * @param kafka_properties_file Additional properties to pass to the kafka client - * @throws Exception on error */ public ServerModel(final String kafka_servers, final String config_name, final ConcurrentHashMap initial_states, final ServerModelListener listener, - final String kafka_properties_file) throws Exception + final String kafka_properties_file) { this.initial_states = initial_states; - // initial_states.entrySet().forEach(state -> - // System.out.println("Initial state for " + state.getKey() + " : " + state.getValue())); config_state_topic = Objects.requireNonNull(config_name); command_topic = config_name + AlarmSystem.COMMAND_TOPIC_SUFFIX; @@ -187,7 +189,7 @@ private void checkConnectivity(final long now) // but silently drops them, so clients will get out of sync, // and since Kafka is down, it won't track the most recent alarm state // for future clients... - if (connected == false && connection_lost == false) + if (!connected && !connection_lost) logger.log(Level.WARNING, "Lost Kafka connectitity"); else if (connected && connection_lost) { @@ -334,9 +336,8 @@ public AlarmTreeItem findNode(final String path) throws Exception * * @param name PV name * @return Node, null if model does not contain the PV - * @throws Exception on error */ - public AlarmServerPV findPV(final String name) throws Exception + public AlarmServerPV findPV(final String name) { return findPV(name, root); } @@ -471,7 +472,7 @@ public void sendStateUpdate(final String path, final BasicState new_state) { final String json = new_state == null ? null : new String(JsonModelWriter.toJsonBytes(new_state, AlarmLogic.getMaintenanceMode(), AlarmLogic.getDisableNotify())); final ProducerRecord record = new ProducerRecord<>(config_state_topic, AlarmSystem.STATE_PREFIX + path, json); - producer.send(record); + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); last_state_update = System.currentTimeMillis(); } catch (Throwable ex) @@ -482,7 +483,7 @@ public void sendStateUpdate(final String path, final BasicState new_state) /** Send alarm update to 'config' topic * @param path Path of item that has a new state - * @param new_state That new state + * @param config That new state */ public void sendConfigUpdate(final String path, final AlarmTreeItem config) { @@ -490,7 +491,7 @@ public void sendConfigUpdate(final String path, final AlarmTreeItem { final String json = config == null ? null : new String(JsonModelWriter.toJsonBytes(config)); final ProducerRecord record = new ProducerRecord<>(config_state_topic, AlarmSystem.CONFIG_PREFIX + path, json); - producer.send(record); + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); } catch (Throwable ex) { @@ -511,7 +512,7 @@ public void sendAnnunciatorMessage(final String path, final SeverityLevel severi final String json = JsonModelWriter.talkToString(severity, message); final ProducerRecord record = new ProducerRecord<>(talk_topic, AlarmSystem.TALK_PREFIX + path, json); - producer.send(record); + producer.send(record).get(KAFKA_CLIENT_TIMEOUT, TimeUnit.SECONDS); } catch (Throwable ex) { diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/SeverityPVHandler.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/SeverityPVHandler.java index 9b0ce804e9..13cdfc7e23 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/SeverityPVHandler.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/SeverityPVHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,8 +10,8 @@ import static org.phoebus.applications.alarm.AlarmSystem.logger; import java.util.Iterator; -import java.util.Map.Entry; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; @@ -32,7 +32,31 @@ @SuppressWarnings("nls") public class SeverityPVHandler { - /** Pending updates */ + // When alarm server calls update(severity_pv_name, severity), + // this value is not written to the PV right away for two reasons: + // + // 1) This could result in many updates. Channel Access/PV Access might + // suppress intermediate values and displays also tend to throttle updates, + // but we want to coalesce updates at the source to reduce overall traffic. + // 2) For alarms with few changes, imagine that the IOC hosting the PV is down so PV cannot be written. + // Once the IOC reappears on the network, severity PV would have wrong value + // until the alarm state changes and the PV is then written. + // + // To avoid both issues, 'updates' tracks the most recent value for each PV, + // and a separate thread 'updates' to PVs and - on success - removes from 'updates'. + // The map coalesces multiple updates to a PV between writes, + // and writes to missing PVs are repeated until they succeed. + + /** PVs that are currently handled */ + private static final Set handled_pvs = ConcurrentHashMap.newKeySet(); + + /** Pending updates + * + * Updates are added, removed to be handled, re-added on errors to try again. + * + * Entry in the map means PV has a severity to be written. + * PV not in map means either PV isn't in use, or there is no need to write a new value. + */ private static final ConcurrentHashMap updates = new ConcurrentHashMap<>(); /** Map of PVs by name */ @@ -105,26 +129,27 @@ private static void run() /** Perform all requested updates, clearing the 'updates' map */ private static void performUpdates() { - final Iterator> entries = updates.entrySet().iterator(); - while (entries.hasNext()) + for (String pv_name : updates.keySet()) { - final Entry entry = entries.next(); - final String pv_name = entry.getKey(); - final SeverityLevel severity = entry.getValue(); - + // Atomically remove severity for this PV from accumulated updates + final SeverityLevel severity = updates.remove(pv_name); logger.log(Level.FINE, "Should update PV '" + pv_name + "' to " + severity.name()); try { final PV pv = getConnectedPV(pv_name); + // null? Cannot create PV, forget about it. Else write... if (pv != null) pv.write(severity.ordinal()); - - // Remove request on success. Otherwise will try again - entries.remove(); } catch (Exception ex) { - logger.log(Level.WARNING, "Cannot set severity PV '" + pv_name + "' to " + severity.ordinal(), ex); + // Cannot connect, put back into `updates` for another attempt. + // put-if-absent because update() could by now have registered a _new_ severity that we must preserve + if (handled_pvs.contains(pv_name)) + { + logger.log(Level.WARNING, "Cannot set severity PV '" + pv_name + "' to " + severity.ordinal(), ex); + updates.putIfAbsent(pv_name, severity); + } } } } @@ -146,7 +171,7 @@ private static PV getConnectedPV(final String pv_name) throws Exception })); // Assert connection - int timeout = AlarmSystem.connection_timeout; + int timeout = AlarmSystem.severity_pv_timeout; while (PV.isDisconnected(pv.read())) { if (abort.poll(1, TimeUnit.SECONDS) == Boolean.TRUE) @@ -167,9 +192,23 @@ public static void update(final String severity_pv_name, final SeverityLevel sev // Write to map, handle in SeverityPVUpdater thread // If SeverityPVUpdater is slow, and several updates arrive for the same PV, // this will place the most recent severity for that PV in the map. + handled_pvs.add(severity_pv_name); updates.put(severity_pv_name, severity); } + /** Clear all entries for a PV + * @param severity_pv_name PV that should no longer be updated + */ + public static void clear(final String severity_pv_name) + { + if (severity_pv_name != null) + { + // Mark to be cleared, and remove from 'updates' in case one is pending.. + handled_pvs.remove(severity_pv_name); + updates.remove(severity_pv_name); + } + } + /** Release all PVs */ public static void stop() { diff --git a/services/archive-engine/pom.xml b/services/archive-engine/pom.xml index 71a1a09fee..c5f5bd0eeb 100644 --- a/services/archive-engine/pom.xml +++ b/services/archive-engine/pom.xml @@ -3,7 +3,7 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT service-archive-engine @@ -82,17 +82,17 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -120,7 +120,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.3.0 @@ -131,43 +131,6 @@ - - - - maven-antrun-plugin - - - verify - - true - - - - - - - - - - - - - - - - - - - - - - - run - - - - org.sonatype.plugins nexus-staging-maven-plugin diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/RDBConfig.java b/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/RDBConfig.java index 719d7b7e26..8ab95f78a1 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/RDBConfig.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/RDBConfig.java @@ -96,7 +96,7 @@ public List list() throws Exception /** @param config_name Name of engine to create (or use existing) * @param description - * @param replace_engine If exists, remove existing groups & channels? + * @param replace_engine If exists, remove existing groups and channels ? * @return ID of new or existing engine * @throws Exception on error, which includes finding existing engine without 'replace' option */ @@ -196,7 +196,7 @@ public int createGroup(final int engine_id, final String group_name) throws Exce } /** @param group_id Group where to add channel - * @param duplicates How to handle duplicate channels + * @param duplicate_mode How to handle duplicate channels * @param original_name Name of channel * @param monitor Monitor? * @param period Scan or estimated monitor period in seconds diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/XMLConfig.java b/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/XMLConfig.java index 33c5557308..a72a0bafe1 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/XMLConfig.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/engine/config/XMLConfig.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2021 Oak Ridge National Laboratory. + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -73,7 +73,7 @@ private void write(final EngineModel model, final XMLStreamWriter writer, final writer.writeEndElement(); for (int c=0; ctrue if labels are equal */ final public static boolean equals(final List labels, final Object obj) diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/SeverityCache.java b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/SeverityCache.java index f4c55734f4..ce440d6468 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/SeverityCache.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/SeverityCache.java @@ -39,7 +39,7 @@ public void dispose() } /** Find or create a severity by name. - * @param alarmSeverity Severity name + * @param severity alarm Severity name * @return Severity * @throws Exception on error */ diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java index dbdf67b7ab..7c0b865ca9 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java @@ -22,7 +22,7 @@ @SuppressWarnings("nls") public class TimestampHelper { - /** @param timestamp {@link Timestamp}, may be null + /** @param timestamp {@link Instant}, may be null * @return Time stamp formatted as string */ public static String format(final Instant timestamp) diff --git a/services/pom.xml b/services/pom.xml index 9f67ffbae3..f7baef104d 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -5,7 +5,7 @@ org.phoebus parent - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT alarm-server diff --git a/services/save-and-restore/doc/index.rst b/services/save-and-restore/doc/index.rst index 6344675c24..8d9641fa88 100644 --- a/services/save-and-restore/doc/index.rst +++ b/services/save-and-restore/doc/index.rst @@ -723,11 +723,47 @@ Body: } ] +Authentication and Authorization +================================ + +All non-GET endpoints are subject to authentication, i.e. clients must send a basic authentication header. The +service can be configured to delegate authentication to Active Directory or remote or local LDAP. For demo and test +purposes hard coded credentials are found in the ``WebSecurityConfig`` class. See the file ``application.properties`` +for information on how to select authentication method. + +Two roles are defined, "sar-user" and "sar-admin". The actual name of these roles can be customizable in ``application.properties``, +and must match role/group names in LDAP or Active Directory. + +Authorization uses a role-based approach like so: + +* Unauthenticated users may read data, i.e. access GET endpoints. +* Save-and-restore role "sar-user": + * Create and update configurations + * Create and update snapshots + * Create and update composite snapshots + * Create and update filters + * Create and update tags, except GOLDEN tag + * Update and delete objects if user name matches object's user id and: + * Object is a snapshot node and not referenced in a composite snapshot node + * Object is a composite snapshot node + * Object is configuration or folder node with no child nodes + * Object is a filter + * Object is a tag +* Save-and-restore role "sar-admin": no restrictions + + +Enabled authentication, disabled authorization +---------------------------------------------- + +The application property ``authorization.permitall`` (default ``true``) can be used to bypass all authorization. In +this case authentication is still required for protected endpoints, but user need not be associated with +a save-and-restore role/group. + Migration ---------- +========= -Commit ``48e17a380b660d59b79cec4d2bd908c0d78eeeae`` of the service code base is about changing the persistence -component from a RDB engine to Elasticsearch. Sites using save-and-restore with an RDB engine may migrate +From commit ``48e17a380b660d59b79cec4d2bd908c0d78eeeae`` of the service code base the persistence +layer is moved from RDB engine to Elasticsearch. Sites using save-and-restore with an RDB engine may migrate data using the below procedure. Terminology: "source host" is the host running the legacy service instance using a RDB engine, diff --git a/services/save-and-restore/pom.xml b/services/save-and-restore/pom.xml index af3cbd47a8..299b648376 100644 --- a/services/save-and-restore/pom.xml +++ b/services/save-and-restore/pom.xml @@ -5,12 +5,12 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus service-save-and-restore - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT 2.7.3 @@ -55,13 +55,13 @@ org.phoebus save-and-restore-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -74,6 +74,28 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.ldap + spring-ldap-core + + + org.springframework.security + spring-security-ldap + + + com.unboundid + unboundid-ldapsdk + + + org.springframework.security + spring-security-test + + jakarta.json jakarta.json-api @@ -83,7 +105,6 @@ org.springframework.boot spring-boot-starter-logging - ${spring.boot.version} org.apache.logging.log4j @@ -138,18 +159,6 @@ - - org.springframework.boot - spring-boot-maven-plugin - 2.7.0 - - - - repackage - - - - org.jacoco jacoco-maven-plugin @@ -206,5 +215,31 @@ + + + + executable-jar + + + !skip-executable-jar + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.9 + + + + repackage + + + + + + + diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/migration/MigrateRdbToElastic.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/migration/MigrateRdbToElastic.java index 8856ac3760..c033b64882 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/migration/MigrateRdbToElastic.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/migration/MigrateRdbToElastic.java @@ -29,7 +29,6 @@ import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @@ -185,11 +184,11 @@ private void createSnapshot(RestTemplate restTemplate, Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(legacySnapshotNode); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(snapshotItems)); + snapshotData.setSnapshotItems(Arrays.asList(snapshotItems)); snapshotData.setUniqueId(legacySnapshotNode.getUniqueId()); snapshot.setSnapshotData(snapshotData); snapshotCount++; - elasticsearchDAO.saveSnapshot(newConfigurationNode.getUniqueId(), snapshot); + elasticsearchDAO.createSnapshot(newConfigurationNode.getUniqueId(), snapshot); } } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java index fedc1ab0b2..0fa5aa4304 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/NodeDAO.java @@ -18,9 +18,6 @@ package org.phoebus.service.saveandrestore.persistence.dao; -import co.elastic.clients.elasticsearch.core.SearchRequest; -import co.elastic.clients.elasticsearch.core.SearchResponse; -import co.elastic.clients.elasticsearch.core.search.Hit; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; import org.phoebus.applications.saveandrestore.model.Configuration; @@ -33,14 +30,9 @@ import org.phoebus.applications.saveandrestore.model.TagData; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; -import org.springframework.http.HttpStatus; import org.springframework.util.MultiValueMap; -import org.springframework.web.server.ResponseStatusException; -import java.io.IOException; import java.util.List; -import java.util.logging.Level; -import java.util.stream.Collectors; /** * @author georgweiss Created 11 Mar 2019 @@ -71,14 +63,20 @@ public interface NodeDAO { List getNodes(List uniqueNodeIds); /** - * Deletes a {@link Node}, folder or configuration. If the node is a folder, the - * entire sub-tree of the folder is deleted, including the snapshots associated - * with configurations in the sub-tree. + * This is deprecated, use {@link #deleteNodes} instead. * * @param nodeId The unique id of the node to delete. */ + @Deprecated void deleteNode(String nodeId); + /** + * Checks that each of the node ids passed to this method exist, and that none of them + * is the root node. If check passes all nodes are deleted. + * @param nodeIds List of (existing) node ids. + */ + void deleteNodes(List nodeIds); + /** * Creates a new node in the tree. * @@ -104,8 +102,8 @@ public interface NodeDAO { /** * Copies {@link Node}s (folder or config) to some parent node. * - * @param nodeIds List of unique node ids subject to move - * @param targetId Unique id of target node + * @param nodeIds Non-null and non-empty list of unique node ids subject to move + * @param targetId Non-null and non-empty unique id of target node * @param userName The (account) name of the user performing the operation. * @return The target {@link Node} object that is the new parent of the moved source {@link Node} */ @@ -136,7 +134,16 @@ public interface NodeDAO { * @param snapshot The {@link Snapshot} data. * @return The persisted {@link Snapshot} data. */ - Snapshot saveSnapshot(String parentNodeId, Snapshot snapshot); + Snapshot createSnapshot(String parentNodeId, Snapshot snapshot); + + /** + * Updates a {@link Snapshot} with respect to name, description/comment. No other properties of the + * node can be modified, but last updated date will be set accordingly. + * + * @param snapshot The {@link Snapshot} subject to update. + * @return The {@link Snapshot} object as read from the persistence implementation. + */ + Snapshot updateSnapshot(Snapshot snapshot); /** * Updates a {@link Node} with respect to name, description/comment and tags. No other properties of the @@ -215,14 +222,6 @@ public interface NodeDAO { */ SnapshotData getSnapshotData(String uniqueId); - /** - * Determines of a move or copy operation is allowed. - * @param nodesToMove List of {@link Node}s subject to move/copy. - * @param targetNode The target {@link Node} of the move/copy operation - * @return true if the list of {@link Node}s can be moved/copied, - * otherwise false. - */ - boolean isMoveOrCopyAllowed(List nodesToMove, Node targetNode); /** * Finds the {@link Node} corresponding to the parent of last element in the split path. For instance, given a @@ -316,7 +315,7 @@ public interface NodeDAO { /** * Deletes a {@link Filter} based on its name. - * @param name + * @param name Unique name of the {@link Filter} */ void deleteFilter(String name); diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ConfigurationDataRepository.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ConfigurationDataRepository.java index ba6c0ea413..f8281f5b36 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ConfigurationDataRepository.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ConfigurationDataRepository.java @@ -22,31 +22,28 @@ import co.elastic.clients.elasticsearch._types.Refresh; import co.elastic.clients.elasticsearch._types.Result; import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery; -import co.elastic.clients.elasticsearch.core.CountRequest; -import co.elastic.clients.elasticsearch.core.CountResponse; -import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest; -import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; -import co.elastic.clients.elasticsearch.core.DeleteRequest; -import co.elastic.clients.elasticsearch.core.DeleteResponse; -import co.elastic.clients.elasticsearch.core.ExistsRequest; -import co.elastic.clients.elasticsearch.core.GetRequest; -import co.elastic.clients.elasticsearch.core.GetResponse; -import co.elastic.clients.elasticsearch.core.IndexRequest; -import co.elastic.clients.elasticsearch.core.IndexResponse; +import co.elastic.clients.elasticsearch.core.*; +import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.transport.endpoints.BooleanResponse; import org.phoebus.applications.saveandrestore.model.ConfigurationData; +import org.phoebus.service.saveandrestore.search.SearchUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.repository.CrudRepository; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Repository; +import org.springframework.util.MultiValueMap; import org.springframework.web.server.ResponseStatusException; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; @Repository public class ConfigurationDataRepository implements CrudRepository { @@ -58,6 +55,9 @@ public class ConfigurationDataRepository implements CrudRepository findById(String id) { if (!resp.found()) { return Optional.empty(); } - return Optional.of(resp.source()); + return resp.source() != null ? Optional.of(resp.source()) : Optional.empty(); } catch (Exception e) { logger.log(Level.SEVERE, "Failed to retrieve configuration with id: " + id, e); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Failed to retrieve configuration with id: " + id); @@ -123,7 +123,7 @@ public boolean existsById(String s) { } @Override - public Iterable findAll() { + public Iterable findAll() { return null; } @@ -134,14 +134,13 @@ public Iterable findAllById(Iterable strings) { @Override public long count() { - try{ + try { CountRequest countRequest = CountRequest.of(c -> c.index(ES_CONFIGURATION_INDEX)); CountResponse countResponse = client.count(countRequest); return countResponse.count(); - } - catch(Exception e){ - logger.log(Level.SEVERE, "Failed to count ConfigurationData objects" , e); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to count ConfigurationData objects", e); throw new RuntimeException(e); } } @@ -152,10 +151,9 @@ public void deleteById(String s) { DeleteRequest deleteRequest = DeleteRequest.of(d -> d.index(ES_CONFIGURATION_INDEX).id(s).refresh(Refresh.True)); DeleteResponse deleteResponse = client.delete(deleteRequest); - if(deleteResponse.result().equals(Result.Deleted)){ + if (deleteResponse.result().equals(Result.Deleted)) { logger.log(Level.WARNING, "Configuration with id " + s + " deleted."); - } - else{ + } else { logger.log(Level.WARNING, "Configuration with id " + s + " NOT deleted."); } } catch (IOException e) { @@ -191,4 +189,26 @@ public void deleteAll() { throw new RuntimeException(e); } } + + /** + * Performs a search on a list of PV names. An OR strategy is used, i.e. {@link ConfigurationData} document need + * only contain one of the listed PV names. + * @param searchParameters Search parameters provided by client. + * @return Potentially empty {@link List} of {@link ConfigurationData} objects contain any of the listed PV names. + */ + public List searchOnPvName(MultiValueMap searchParameters) { + Optional>> optional = + searchParameters.entrySet().stream().filter(e -> e.getKey().strip().equalsIgnoreCase("pvs")).findFirst(); + if (optional.isEmpty()) { + return Collections.emptyList(); + } + SearchRequest searchRequest = searchUtil.buildSearchRequestForPvs(optional.get().getValue()); + try { + SearchResponse searchResponse = client.search(searchRequest, ConfigurationData.class); + return searchResponse.hits().hits().stream().map(Hit::source).collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java index a888abbf62..5ff7180588 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAO.java @@ -18,6 +18,7 @@ package org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch; +import org.apache.commons.collections4.CollectionUtils; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; import org.phoebus.applications.saveandrestore.model.ConfigPv; @@ -41,9 +42,10 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import java.lang.annotation.Inherited; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -52,6 +54,8 @@ import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.phoebus.applications.saveandrestore.model.Node.ROOT_FOLDER_UNIQUE_ID; @@ -82,6 +86,8 @@ public class ElasticsearchDAO implements NodeDAO { @Autowired private SearchUtil searchUtil; + private final Pattern NODE_NAME_PATTERN = Pattern.compile(".*(\\scopy(\\s\\d*)?$)"); + private static final Logger logger = Logger.getLogger(ElasticsearchDAO.class.getName()); @@ -119,15 +125,31 @@ public List getNodes(List uniqueNodeIds) { return nodes; } + /** + * {@inheritDoc} + */ @Override + @Deprecated public void deleteNode(String nodeId) { - Node nodeToDelete = getNode(nodeId); - if (nodeToDelete == null) { - throw new NodeNotFoundException("Cannot delete non-existing node"); - } else if (nodeToDelete.getUniqueId().equals(ROOT_FOLDER_UNIQUE_ID)) { - throw new IllegalArgumentException("Root node cannot be deleted"); + deleteNodes(List.of(nodeId)); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteNodes(List nodeIds){ + List nodes = new ArrayList<>(); + for(String nodeId : nodeIds){ + Node nodeToDelete = getNode(nodeId); + if (nodeToDelete == null) { + throw new NodeNotFoundException("Cannot delete non-existing node"); + } else if (nodeToDelete.getUniqueId().equals(ROOT_FOLDER_UNIQUE_ID)) { + throw new IllegalArgumentException("Root node cannot be deleted"); + } + nodes.add(nodeToDelete); } - deleteNode(nodeToDelete); + nodes.forEach(this::deleteNode); } @Override @@ -221,101 +243,238 @@ public Node getParentNode(String uniqueNodeId) { @Override public Node moveNodes(List nodeIds, String targetId, String userName) { - Optional targetNode = elasticsearchTreeRepository.findById(targetId); - if (targetNode.isEmpty()) { - throw new NodeNotFoundException(String.format("Target node with unique id=%s not found", targetId)); + // Move root node is not allowed + if (nodeIds.contains(ROOT_FOLDER_UNIQUE_ID)) { + throw new IllegalArgumentException("Move root node not supported"); + } + + // Check that the target node is not any of the source nodes, i.e. a node cannot be copied to itself. + if (nodeIds.stream().anyMatch(i -> i.equals(targetId))) { + throw new IllegalArgumentException("At least one source node is same as target node"); } - if (!targetNode.get().getNode().getNodeType().equals(NodeType.FOLDER)) { - throw new IllegalArgumentException("Move not allowed: target node is not a folder"); + // Get target node. If it does not exist, a NodeNotFoundException is thrown. + Optional targetNodeOptional; + + try { + targetNodeOptional = elasticsearchTreeRepository.findById(targetId); + } catch (NodeNotFoundException e) { + throw new IllegalArgumentException("Target node does not exist"); } + ESTreeNode targetNode = targetNodeOptional.get(); + List sourceNodes = new ArrayList<>(); nodeIds.forEach(id -> { Optional esTreeNode = elasticsearchTreeRepository.findById(id); esTreeNode.ifPresent(treeNode -> sourceNodes.add(treeNode.getNode())); }); - if (sourceNodes.size() != nodeIds.size()) { - throw new IllegalArgumentException("At least one unique node id not found."); + // Get node type of first element... + NodeType nodeTypeOfFirstSourceNode = sourceNodes.get(0).getNodeType(); + + // All nodes must be of same type + if (sourceNodes.stream().anyMatch(n -> !n.getNodeType().equals(nodeTypeOfFirstSourceNode))) { + throw new IllegalArgumentException("Move nodes supported only if all source nodes are of same type"); } - if (!isMoveOrCopyAllowed(sourceNodes, targetNode.get().getNode())) { - throw new IllegalArgumentException("Prerequisites for moving source node(s) not met."); + // All nodes must have same parent node + String parentNodeOfFirstSourceNode = + elasticsearchTreeRepository.getParentNode(sourceNodes.get(0).getUniqueId()).getNode().getUniqueId(); + if (sourceNodes.stream().anyMatch(n -> + !parentNodeOfFirstSourceNode.equals(elasticsearchTreeRepository.getParentNode(n.getUniqueId()).getNode().getUniqueId()))) { + throw new IllegalArgumentException("All source nodes must have same parent node"); + } + + // Configuration, composite snapshot and folder can be moved only to folder + if ((nodeTypeOfFirstSourceNode.equals(NodeType.FOLDER) || + nodeTypeOfFirstSourceNode.equals(NodeType.CONFIGURATION) || + nodeTypeOfFirstSourceNode.equals(NodeType.COMPOSITE_SNAPSHOT)) + && !targetNode.getNode().getNodeType().equals(NodeType.FOLDER)) { + throw new IllegalArgumentException(nodeTypeOfFirstSourceNode + " cannot be moved to " + targetNode.getNode().getNodeType() + " node"); + } + + // Snapshot may only be moved to a configuration node. + if (nodeTypeOfFirstSourceNode.equals(NodeType.SNAPSHOT) + && !targetNode.getNode().getNodeType().equals(NodeType.CONFIGURATION)) { + throw new IllegalArgumentException(nodeTypeOfFirstSourceNode + " cannot be moved to " + targetNode.getNode().getNodeType() + " node"); + } + + // Snapshot nodes' PV list must match target configuration's PV list. This is checked for all source snapshots: + // if one mismatch is found, the move operation is aborted -> no snapshots moved. + if (nodeTypeOfFirstSourceNode.equals(NodeType.SNAPSHOT)) { + for (Node node : sourceNodes) { + if (!mayMoveOrCopySnapshot(node, targetNode.getNode())) { + throw new IllegalArgumentException("At least one snapshot's PV list does not match target configuration's PV list"); + } + } } - // Remove source nodes from the list of child nodes in parent node. ESTreeNode parentNode = elasticsearchTreeRepository.getParentNode(sourceNodes.get(0).getUniqueId()); if (parentNode == null) { throw new RuntimeException("Parent node of source node " + sourceNodes.get(0).getUniqueId() + " not found. Should not happen."); } + + if (targetNode.getChildNodes() != null){ + List targetsChildNodes = new ArrayList<>(); + for(String parentChildNode : targetNode.getChildNodes()){ + Optional targetChildNodeOptional = elasticsearchTreeRepository.findById(parentChildNode); + if(targetChildNodeOptional.isEmpty()){ // Should not happen, but ignore if it does. + continue; + } + targetsChildNodes.add(targetChildNodeOptional.get().getNode()); + } + for(Node sourceNode : sourceNodes){ + for(Node targetChildNode : targetsChildNodes) { + if (targetChildNode.getName().equals(sourceNode.getName()) && targetChildNode.getNodeType().equals(sourceNode.getNodeType())) { + throw new IllegalArgumentException("Cannot move, at least one source node has same name and type as a target child node"); + } + } + } + } + + // Remove source nodes from the list of child nodes in parent node. parentNode.getChildNodes().removeAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); elasticsearchTreeRepository.save(parentNode); // Update the target node to include the source nodes in its list of child nodes - if (targetNode.get().getChildNodes() == null) { - targetNode.get().setChildNodes(new ArrayList<>()); + if (targetNode.getChildNodes() == null) { + targetNode.setChildNodes(new ArrayList<>()); } - targetNode.get().getChildNodes().addAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); - ESTreeNode updatedTargetNode = elasticsearchTreeRepository.save(targetNode.get()); + targetNode.getChildNodes().addAll(sourceNodes.stream().map(Node::getUniqueId).collect(Collectors.toList())); + ESTreeNode updatedTargetNode = elasticsearchTreeRepository.save(targetNode); return updatedTargetNode.getNode(); } @Override public Node copyNodes(List nodeIds, String targetId, String userName) { - Optional targetNode = elasticsearchTreeRepository.findById(targetId); - if (targetNode.isEmpty()) { - throw new NodeNotFoundException(String.format("Target node with unique id=%s not found", targetId)); + // Copy to root node not allowed, neither is copying of root folder itself + if (targetId.equals(ROOT_FOLDER_UNIQUE_ID) || nodeIds.contains(ROOT_FOLDER_UNIQUE_ID)) { + throw new IllegalArgumentException("Copy to root node or copy root node not supported"); + } + + // Check that the target node is not any of the source nodes, i.e. a node cannot be copied to itself. + if (nodeIds.stream().anyMatch(i -> i.equals(targetId))) { + throw new IllegalArgumentException("At least one source node is same as target node"); } - if (!targetNode.get().getNode().getNodeType().equals(NodeType.FOLDER)) { - throw new IllegalArgumentException("Move not allowed: target node is not a folder"); + // Get target node. If it does not exist, a NodeNotFoundException is thrown. + Optional targetNodeOptional; + + try { + targetNodeOptional = elasticsearchTreeRepository.findById(targetId); + } catch (NodeNotFoundException e) { + throw new IllegalArgumentException("Target node does not exist"); } + Node targetNode = targetNodeOptional.get().getNode(); + List sourceNodes = new ArrayList<>(); - nodeIds.forEach(id -> { - Optional esTreeNode = elasticsearchTreeRepository.findById(id); - esTreeNode.ifPresent(treeNode -> sourceNodes.add(treeNode.getNode())); - }); - if (sourceNodes.size() != nodeIds.size()) { - throw new IllegalArgumentException("At least one unique node id not found."); + try { + for (String nodeId : nodeIds) { + Optional esTreeNode = elasticsearchTreeRepository.findById(nodeId); + sourceNodes.add(esTreeNode.get().getNode()); + } + } catch (NodeNotFoundException e) { + throw new IllegalArgumentException("At least one source node does not exist"); + } + + // Get node type of first element... + NodeType nodeTypeOfFirstSourceNode = sourceNodes.get(0).getNodeType(); + // ... if type is folder, abort + if (nodeTypeOfFirstSourceNode.equals(NodeType.FOLDER)) { + throw new IllegalArgumentException("Copy of folder(s) not supported"); + } + // All nodes must be of same type + if (sourceNodes.stream().anyMatch(n -> !n.getNodeType().equals(nodeTypeOfFirstSourceNode))) { + throw new IllegalArgumentException("Copy nodes supported only if all source nodes are of same type"); + } + // All nodes must have same parent node + String parentNodeOfFirstSourceNode = + elasticsearchTreeRepository.getParentNode(sourceNodes.get(0).getUniqueId()).getNode().getUniqueId(); + if (sourceNodes.stream().anyMatch(n -> + !parentNodeOfFirstSourceNode.equals(elasticsearchTreeRepository.getParentNode(n.getUniqueId()).getNode().getUniqueId()))) { + throw new IllegalArgumentException("All source nodes must have same parent node"); } - if (!isMoveOrCopyAllowed(sourceNodes, targetNode.get().getNode())) { - throw new IllegalArgumentException("Prerequisites for copying source node(s) not met."); + // Configuration and composite snapshot nodes may be copied only to folder + if ((nodeTypeOfFirstSourceNode.equals(NodeType.CONFIGURATION) || nodeTypeOfFirstSourceNode.equals(NodeType.COMPOSITE_SNAPSHOT)) + && !targetNode.getNodeType().equals(NodeType.FOLDER)) { + throw new IllegalArgumentException(nodeTypeOfFirstSourceNode + " cannot be copied to " + targetNode.getNodeType() + " node"); } - sourceNodes.forEach(sourceNode -> copyNode(sourceNode, targetNode.get().getNode(), userName)); + // Snapshot may only be copied to a configuration node. + if (nodeTypeOfFirstSourceNode.equals(NodeType.SNAPSHOT) + && !targetNode.getNodeType().equals(NodeType.CONFIGURATION)) { + throw new IllegalArgumentException(nodeTypeOfFirstSourceNode + " cannot be copied to " + targetNode.getNodeType() + " node"); + } - return targetNode.get().getNode(); + // Snapshot nodes' PV list must match target configuration's PV list. This is checked for all source snapshots: + // if one mismatch is found, the copy operation is aborted -> no snapshots copied. + if (nodeTypeOfFirstSourceNode.equals(NodeType.SNAPSHOT)) { + for (Node node : sourceNodes) { + if (!mayMoveOrCopySnapshot(node, targetNode)) { + throw new IllegalArgumentException("Snapshot not compatible with configuration"); + } + } + } + + sourceNodes.forEach(sourceNode -> copyNode(sourceNode, targetNode, userName)); + + return targetNode; } - private void copyNode(Node sourceNode, Node targetNode, String userName) { - // In order to copy, we first create a shallow clone of the source node + /** + * Creates a copy (clone) of the source {@link Node} and associated data like so: + *
        + *
      1. Determine the new {@link Node}'s name.
      2. + *
      3. Create the new {@link Node}.
      4. + *
      5. Clone the data, which of course varies depending on the {@link NodeType} of the source {@link Node}.
      6. + *
      + * + * @param sourceNode The source {@link Node} to be copied (cloned). + * @param targetParentNode The parent {@link Node} of the copy. + * @param userName Username of the individual performing the action. + */ + private void copyNode(Node sourceNode, Node targetParentNode, String userName) { + List targetsChildNodes = getChildNodes(targetParentNode.getUniqueId()); + String newNodeName = determineNewNodeName(sourceNode, targetsChildNodes); + // First create a clone of the source Node object Node sourceNodeClone = Node.builder() - .name(sourceNode.getName()) + .name(newNodeName) .nodeType(sourceNode.getNodeType()) .userName(userName) .tags(sourceNode.getTags()) .description(sourceNode.getDescription()) .build(); - final Node newSourceNode = createNode(targetNode.getUniqueId(), sourceNodeClone); + final Node newSourceNode = createNode(targetParentNode.getUniqueId(), sourceNodeClone); + // Next copy data and associate it with the cloned Node object if (sourceNode.getNodeType().equals(NodeType.CONFIGURATION)) { ConfigurationData sourceConfiguration = getConfigurationData(sourceNode.getUniqueId()); copyConfigurationData(newSourceNode, sourceConfiguration); } else if (sourceNode.getNodeType().equals(NodeType.SNAPSHOT)) { SnapshotData snapshotData = getSnapshotData(sourceNode.getUniqueId()); copySnapshotData(newSourceNode, snapshotData); + } else if (sourceNode.getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)) { + CompositeSnapshotData compositeSnapshotData = + getCompositeSnapshotData(sourceNode.getUniqueId()); + copyCompositeSnapshotData(newSourceNode, compositeSnapshotData); } + } - if (sourceNode.getNodeType().equals(NodeType.FOLDER) || sourceNode.getNodeType().equals(NodeType.CONFIGURATION)) { - List childNodes = getChildNodes(sourceNode.getUniqueId()); - childNodes.forEach(childNode -> copyNode(childNode, newSourceNode, userName)); - } + protected boolean mayMoveOrCopySnapshot(Node sourceNode, Node targetParentNode) { + SnapshotData snapshotData = getSnapshotData(sourceNode.getUniqueId()); + ConfigurationData configurationData = getConfigurationData(targetParentNode.getUniqueId()); + + List pvsInSnapshot = + snapshotData.getSnapshotItems().stream().map(si -> si.getConfigPv().getPvName()).collect(Collectors.toList()); + List pvsInConfiguration = + configurationData.getPvList().stream().map(ConfigPv::getPvName).collect(Collectors.toList()); + return CollectionUtils.containsAll(pvsInSnapshot, pvsInConfiguration) && CollectionUtils.containsAll(pvsInConfiguration, pvsInSnapshot); } /** @@ -325,16 +484,24 @@ private void copyNode(Node sourceNode, Node targetNode, String userName) { * associated. This must already exist in the Elasticsearch index. * @param sourceConfiguration The source {@link ConfigurationData} */ - private ConfigurationData copyConfigurationData(Node targetConfigurationNode, ConfigurationData sourceConfiguration) { + private void copyConfigurationData(Node targetConfigurationNode, ConfigurationData sourceConfiguration) { ConfigurationData clonedConfigurationData = ConfigurationData.clone(sourceConfiguration); clonedConfigurationData.setUniqueId(targetConfigurationNode.getUniqueId()); - return configurationDataRepository.save(clonedConfigurationData); + configurationDataRepository.save(clonedConfigurationData); } - private SnapshotData copySnapshotData(Node targetSnapshotNode, SnapshotData snapshotData) { - SnapshotData clonedSnapshotData = SnapshotData.clone(snapshotData); + private void copySnapshotData(Node targetSnapshotNode, SnapshotData snapshotData) { + SnapshotData clonedSnapshotData = new SnapshotData(); + clonedSnapshotData.setSnapshotItems(snapshotData.getSnapshotItems()); clonedSnapshotData.setUniqueId(targetSnapshotNode.getUniqueId()); - return snapshotDataRepository.save(clonedSnapshotData); + snapshotDataRepository.save(clonedSnapshotData); + } + + private void copyCompositeSnapshotData(Node targetCompositeSnapshotNode, CompositeSnapshotData compositeSnapshotData) { + CompositeSnapshotData clonedCompositeSnapshotData = new CompositeSnapshotData(); + clonedCompositeSnapshotData.setReferencedSnapshotNodes(compositeSnapshotData.getReferencedSnapshotNodes()); + clonedCompositeSnapshotData.setUniqueId(targetCompositeSnapshotNode.getUniqueId()); + compositeSnapshotDataRepository.save(clonedCompositeSnapshotData); } @Override @@ -530,9 +697,10 @@ public Configuration updateConfiguration(Configuration configuration) { Node existingConfigurationNode = getNode(configuration.getConfigurationNode().getUniqueId()); - // Set name and description, even if unchanged. + // Set name, description and user even if unchanged. existingConfigurationNode.setName(configuration.getConfigurationNode().getName()); existingConfigurationNode.setDescription(configuration.getConfigurationNode().getDescription()); + existingConfigurationNode.setUserName(configuration.getConfigurationNode().getUserName()); // Update last modified date existingConfigurationNode.setLastModified(new Date()); existingConfigurationNode = updateNode(existingConfigurationNode, false); @@ -545,87 +713,6 @@ public Configuration updateConfiguration(Configuration configuration) { .build(); } - /** - * Move of list of {@link Node}s is allowed only if: - *
        - *
      • All elements are of same {@link NodeType}.
      • - *
      • All elements have same parent.
      • - *
      • All elements are folder nodes if the target is the root node.
      • - *
      • None of the elements is a snapshot node, or the root node.
      • - *
      • The target node - which must be a folder - does not contain any direct child nodes - * with same name and node type as any of the source nodes.
      • - *
      • Target node is not a descendant at any depth of any of the source nodes.
      • - *
      - * - * @param sourceNodes List of source {@link Node}s - * @param targetNode The wanted target {@link Node} - * @return true if move criteria are met, otherwise false - */ - @Override - public boolean isMoveOrCopyAllowed(List sourceNodes, Node targetNode) { - // Does target node even exist? - Optional esTargetTreeNodeOptional = elasticsearchTreeRepository.findById(targetNode.getUniqueId()); - if (esTargetTreeNodeOptional.isEmpty()) { - throw new NodeNotFoundException("Target node " + targetNode.getUniqueId() + " does not exist."); - } - // Check that the target node is not any of the source nodes, i.e. a node cannot be copied/moved to itself. - for (Node sourceNode : sourceNodes) { - if (sourceNode.getUniqueId().equals(esTargetTreeNodeOptional.get().getNode().getUniqueId())) { - return false; - } - } - // Root node cannot be copied/moved. Individual snapshot nodes cannot be copies/moved. - Optional rootOrSnapshotNode = sourceNodes.stream() - .filter(node -> node.getUniqueId().equals(ROOT_FOLDER_UNIQUE_ID) || - node.getNodeType().equals(NodeType.SNAPSHOT)).findFirst(); - if (rootOrSnapshotNode.isPresent()) { - logger.info("Move/copy not allowed: source node(s) list contains snapshot or root node."); - return false; - } - // Check if selection contains configuration or snapshot node. - Optional saveSetNode = sourceNodes.stream() - .filter(node -> node.getNodeType().equals(NodeType.CONFIGURATION)).findFirst(); - // Configuration nodes may not be moved/copied to root node. - if (saveSetNode.isPresent() && targetNode.getUniqueId().equals(ROOT_FOLDER_UNIQUE_ID)) { - logger.info("Move/copy of configuration node(s) to root node not allowed."); - return false; - } - if (sourceNodes.size() > 1) { - // Check that all elements are of same type and have the same parent. - NodeType firstElementType = sourceNodes.get(0).getNodeType(); - Node parentNodeOfFirst = getParentNode(sourceNodes.get(0).getUniqueId()); - for (int i = 1; i < sourceNodes.size(); i++) { - if (!sourceNodes.get(i).getNodeType().equals(firstElementType)) { - logger.info("Move not allowed: all source nodes must be of same type."); - return false; - } - Node parent = getParentNode(sourceNodes.get(i).getUniqueId()); - if (!parent.getUniqueId().equals(parentNodeOfFirst.getUniqueId())) { - logger.info("Move not allowed: all source nodes must have same parent node."); - return false; - } - } - } - // Check if there is any name/type clash - List parentsChildNodes = getChildNodes(targetNode.getUniqueId()); - for (Node node : sourceNodes) { - if (!isNodeNameValid(node, parentsChildNodes)) { - logger.info("Move/copy not allowed: target node already contains child node with same name and type: " + node.getName()); - return false; - } - } - - boolean containedInSubtree = false; - for (Node sourceNode : sourceNodes) { - containedInSubtree = isContainedInSubtree(sourceNode.getUniqueId(), targetNode.getUniqueId()); - if (containedInSubtree) { - break; - } - } - - return !containedInSubtree; - } - @Override public ConfigurationData getConfigurationData(String uniqueId) { Optional configurationData = configurationDataRepository.findById(uniqueId); @@ -636,7 +723,7 @@ public ConfigurationData getConfigurationData(String uniqueId) { } @Override - public Snapshot saveSnapshot(String parentNodeId, Snapshot snapshot) { + public Snapshot createSnapshot(String parentNodeId, Snapshot snapshot) { SnapshotData sanitizedSnapshotData = removeDuplicateSnapshotItems(snapshot.getSnapshotData()); snapshot.setSnapshotData(sanitizedSnapshotData); @@ -660,6 +747,27 @@ public Snapshot saveSnapshot(String parentNodeId, Snapshot snapshot) { return newSnapshot; } + @Override + public Snapshot updateSnapshot(Snapshot snapshot) { + + SnapshotData sanitizedSnapshotData = removeDuplicateSnapshotItems(snapshot.getSnapshotData()); + snapshot.setSnapshotData(sanitizedSnapshotData); + + snapshot.getSnapshotNode().setNodeType(NodeType.SNAPSHOT); // Force node type + SnapshotData newSnapshotData; + try { + newSnapshotData = snapshotDataRepository.save(snapshot.getSnapshotData()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + Snapshot newSnapshot = new Snapshot(); + newSnapshot.setSnapshotData(newSnapshotData); + newSnapshot.setSnapshotNode(snapshot.getSnapshotNode()); + + return newSnapshot; + } + @Override public SnapshotData getSnapshotData(String uniqueId) { Optional snapshotData = snapshotDataRepository.findById(uniqueId); @@ -722,27 +830,27 @@ public boolean isContainedInSubtree(String startNode, String nodeToLookFor) { /** * Removes duplicate PV names if found in the {@link ConfigurationData}. While user and client should * take measures to not add duplicates, this is to safeguard that only sanitized data is persisted. + * * @param configurationData The {@link ConfigurationData} subject to sanitation. * @return The sanitized {@link ConfigurationData} object. */ - protected ConfigurationData removeDuplicatePVNames(ConfigurationData configurationData){ - if(configurationData == null){ + protected ConfigurationData removeDuplicatePVNames(ConfigurationData configurationData) { + if (configurationData == null) { return null; } - if(configurationData.getPvList() == null){ + if (configurationData.getPvList() == null) { return configurationData; } Map sanitizedMap = new HashMap<>(); - for (ConfigPv configPv : configurationData.getPvList()){ - if(sanitizedMap.containsKey(configPv.getPvName())){ + for (ConfigPv configPv : configurationData.getPvList()) { + if (sanitizedMap.containsKey(configPv.getPvName())) { continue; } sanitizedMap.put(configPv.getPvName(), configPv); } ConfigurationData sanitizedConfigurationData = new ConfigurationData(); sanitizedConfigurationData.setUniqueId(configurationData.getUniqueId()); - List sanitizedList = new ArrayList<>(); - sanitizedList.addAll(sanitizedMap.values()); + List sanitizedList = new ArrayList<>(sanitizedMap.values()); sanitizedConfigurationData.setPvList(sanitizedList); return sanitizedConfigurationData; } @@ -750,6 +858,7 @@ protected ConfigurationData removeDuplicatePVNames(ConfigurationData configurati /** * Removes duplicate PV names if found in the {@link SnapshotData}. While user and client should * take measures to not add duplicates, this is to safeguard that only sanitized data is persisted. + * * @param snapshotData The {@link SnapshotData} subject to sanitation. * @return The sanitized {@link SnapshotData} object. */ @@ -757,7 +866,7 @@ protected SnapshotData removeDuplicateSnapshotItems(SnapshotData snapshotData) { if (snapshotData == null) { return null; } - if(snapshotData.getSnapshotItems() == null){ + if (snapshotData.getSnapshotItems() == null) { return snapshotData; } Map sanitizedMap = new HashMap<>(); @@ -769,7 +878,7 @@ protected SnapshotData removeDuplicateSnapshotItems(SnapshotData snapshotData) { } SnapshotData sanitizedSnapshotData = new SnapshotData(); List sanitizedList = new ArrayList<>(sanitizedMap.values()); - sanitizedSnapshotData.setSnasphotItems(sanitizedList); + sanitizedSnapshotData.setSnapshotItems(sanitizedList); return sanitizedSnapshotData; } @@ -970,7 +1079,26 @@ private boolean checkCompositeSnapshotReferencedNodeType(String nodeId) { @Override public SearchResult search(MultiValueMap searchParameters) { - return elasticsearchTreeRepository.search(searchParameters); + return searchInternal(searchParameters); + } + + private SearchResult searchInternal(MultiValueMap searchParameters){ + // Did client specify search on pv name(s)? + if(searchParameters.keySet().stream().anyMatch(k -> k.strip().toLowerCase().contains("pvs"))){ + List configurationDataList = configurationDataRepository.searchOnPvName(searchParameters); + if(configurationDataList.isEmpty()){ + // No matching configurations found, return empty SearchResult + return new SearchResult(0, Collections.emptyList()); + } + List uniqueIds = configurationDataList.stream().map(ConfigurationData::getUniqueId).collect(Collectors.toList()); + MultiValueMap augmented = new LinkedMultiValueMap<>(); + augmented.putAll(searchParameters); + augmented.put("uniqueid", uniqueIds); + return elasticsearchTreeRepository.search(augmented); + } + else{ + return elasticsearchTreeRepository.search(searchParameters); + } } /** @@ -979,11 +1107,12 @@ public SearchResult search(MultiValueMap searchParameters) { *
    • Only valid keys are saved.
    • *
    • Values (search terms) with multiple elements are formatted correctly.
    • * + * * @param filter The {@link Filter} to save - * @return + * @return The saved {@link Filter} */ @Override - public Filter saveFilter(Filter filter){ + public Filter saveFilter(Filter filter) { // Parse the search query to make sure only supported keys are accepted Map queryParams = SearchQueryUtil.parseHumanReadableQueryString(filter.getQueryString()); // Format query again before saving it @@ -993,30 +1122,25 @@ public Filter saveFilter(Filter filter){ @Override - public List getAllFilters(){ + public List getAllFilters() { Iterable filtersIterable = filterRepository.findAll(); List filters = new ArrayList<>(); - filtersIterable.forEach(element -> filters.add(element)); + filtersIterable.forEach(filters::add); return filters; } @Override - public void deleteFilter(String name){ + public void deleteFilter(String name) { filterRepository.deleteById(name); } @Override - public void deleteAllFilters(){ + public void deleteAllFilters() { filterRepository.deleteAll(); } @Override - /** - * Adds a {@link Tag} to specified list of target {@link Node}s - * @param tagData See {@link TagData} - * @return The list of updated {@link Node}s - */ - public List addTag(TagData tagData){ + public List addTag(TagData tagData) { List updatedNodes = new ArrayList<>(); tagData.getUniqueNodeIds().forEach(nodeId -> { try { @@ -1030,7 +1154,7 @@ public List addTag(TagData tagData){ .created(node.getCreated()) .build(); List tags = node.getTags(); - if(tags == null){ + if (tags == null) { tags = new ArrayList<>(); } tags.add(tagData.getTag()); @@ -1047,16 +1171,17 @@ public List addTag(TagData tagData){ /** * Removes a {@link Tag} from specified list of target {@link Node}s. If a {@link Node} does not * contain the {@link Tag}, this method does not update that {@link Node}. + * * @param tagData See {@link TagData} * @return The list of updated {@link Node}s. This may contain fewer elements than the list of * unique node ids as {@link Node}s not containing the {@link Tag} are omitted from update. */ - public List deleteTag(TagData tagData){ + public List deleteTag(TagData tagData) { List updatedNodes = new ArrayList<>(); tagData.getUniqueNodeIds().forEach(nodeId -> { try { Node node = getNode(nodeId); - if(node != null){ + if (node != null) { Node updatedNode = Node.builder() .nodeType(node.getNodeType()) .userName(node.getUserName()) @@ -1067,7 +1192,7 @@ public List deleteTag(TagData tagData){ .build(); List tags = node.getTags(); Optional optional = tags.stream().filter(tag -> tag.getName().equals(tagData.getTag().getName())).findFirst(); - if(optional.isPresent()){ + if (optional.isPresent()) { tags.remove(optional.get()); updatedNode.setTags(tags); updatedNode = updateNode(updatedNode, false); @@ -1080,4 +1205,68 @@ public List deleteTag(TagData tagData){ }); return updatedNodes; } + + /** + * Determines a name for a copied/moved node. Some (ugly) logic is applied to implement a strategy where + * a string like "copy" or "copy 2" is appended to the node name in case the target node already contains a + * child node with same name and type. + * + * @param sourceNode The node subject to copy/move + * @param targetParentChildNodes List of child nodes in target + * @return A node name that does not clash with any existing node of same type in the target node. + */ + protected String determineNewNodeName(Node sourceNode, List targetParentChildNodes) { + // Filter to make sure only nodes of same type are considered. + targetParentChildNodes = targetParentChildNodes.stream().filter(n -> n.getNodeType().equals(sourceNode.getNodeType())).collect(Collectors.toList()); + List targetParentChildNodeNames = targetParentChildNodes.stream().map(Node::getName).collect(Collectors.toList()); + if(!targetParentChildNodeNames.contains(sourceNode.getName())){ + return sourceNode.getName(); + } + String newNodeBaseName = sourceNode.getName(); + Matcher matcher = NODE_NAME_PATTERN.matcher(newNodeBaseName); + if (matcher.matches()) { // If source node already contains "copy X", then calculate the "base name". + newNodeBaseName = newNodeBaseName.substring(0, (newNodeBaseName.length() - matcher.group(1).length())); + } + List nodeNameCopies = new ArrayList<>(); + Pattern pattern = Pattern.compile(newNodeBaseName + "(\\scopy(\\s\\d*)?$)"); + for (Node targetChildNode : targetParentChildNodes) { + String targetChildNodeName = targetChildNode.getName(); + if(pattern.matcher(targetChildNodeName).matches()){ + nodeNameCopies.add(targetChildNodeName); + } + } + // NOTE: nodeNameCopies may also contain an element with equal name as source node. + if (nodeNameCopies.isEmpty()) { + return newNodeBaseName + " copy"; + } else { + Collections.sort(nodeNameCopies, new NodeNameComparator()); + try { + String lastCopyName = nodeNameCopies.get(nodeNameCopies.size() - 1); + if (lastCopyName.equals(newNodeBaseName + " copy")) { + return newNodeBaseName + " copy 2"; + } else { + int highestIndex = Integer.parseInt(nodeNameCopies.get(nodeNameCopies.size() - 1).substring((newNodeBaseName + " copy ").length())); + return newNodeBaseName + " copy " + (highestIndex + 1); + } + } catch (NumberFormatException e) { // Should not happen... + logger.log(Level.WARNING, "Unable to determine copy name index from " + nodeNameCopies.get(nodeNameCopies.size() - 1)); + return sourceNode.getUniqueId(); + } + } + } + + public static class NodeNameComparator implements Comparator{ + + @Override + public int compare(String s1, String s2){ + if(s1.endsWith("copy") || s2.endsWith("copy")){ + return s1.compareTo(s2); + } + int copyIndex1 = s1.indexOf("copy"); + int copyIndex2 = s1.indexOf("copy"); + int index1 = Integer.parseInt(s1.substring(copyIndex1 + 5)); + int index2 = Integer.parseInt(s2.substring(copyIndex2 + 5)); + return index1 - index2; + } + } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchTreeRepository.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchTreeRepository.java index 2143aeb6d5..ef4fdec0ad 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchTreeRepository.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchTreeRepository.java @@ -204,7 +204,7 @@ public Iterable findAll() { */ @Override public Iterable findAllById(Iterable uniqueIds) { - if (!uniqueIds.iterator().hasNext()) { + if (uniqueIds == null || !uniqueIds.iterator().hasNext()) { return Collections.emptyList(); } List ids = new ArrayList<>(); @@ -351,22 +351,4 @@ public SearchResult search(MultiValueMap searchParameters) { throw new RuntimeException(e); } } - - public List getByNodeName(String nodeName) { - BoolQuery.Builder boolQueryBuilder = new Builder(); - NestedQuery innerNestedQuery; - MatchQuery matchQuery = MatchQuery.of(m -> m.field("node.name").query(nodeName)); - innerNestedQuery = NestedQuery.of(n1 -> n1.path("node").query(matchQuery._toQuery())); - boolQueryBuilder.must(innerNestedQuery._toQuery()); - SearchRequest searchRequest = SearchRequest.of(s -> s.index(ES_TREE_INDEX) - .query(boolQueryBuilder.build()._toQuery()) - .timeout("60s") - .size(1000)); - try { - SearchResponse esTreeNodeSearchResponse = client.search(searchRequest, ESTreeNode.class); - return esTreeNodeSearchResponse.hits().hits().stream().map(Hit::source).collect(Collectors.toList()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/search/SearchUtil.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/search/SearchUtil.java index be149040d1..63bc00169a 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/search/SearchUtil.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/search/SearchUtil.java @@ -1,15 +1,10 @@ package org.phoebus.service.saveandrestore.search; -import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.FieldSort; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.*; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery.Builder; -import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode; -import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.FuzzyQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.NestedQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery; import co.elastic.clients.elasticsearch.core.SearchRequest; import org.phoebus.applications.saveandrestore.model.Tag; import org.springframework.beans.factory.annotation.Value; @@ -21,13 +16,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.*; import java.util.Map.Entry; -import java.util.Optional; +import java.util.stream.Collectors; /** * A utility class for creating a search query for log entries based on time, @@ -44,6 +35,8 @@ public class SearchUtil { @SuppressWarnings("unused") @Value("${elasticsearch.tree_node.index:saveandrestore_tree}") public String ES_TREE_INDEX; + @Value("${elasticsearch.configuration_node.index:saveandrestore_configuration}") + public String ES_CONFIGURATION_INDEX; @SuppressWarnings("unused") @Value("${elasticsearch.result.size.search.default:100}") private int defaultSearchSize; @@ -60,7 +53,9 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara boolean fuzzySearch = false; List descriptionTerms = new ArrayList<>(); List nodeNameTerms = new ArrayList<>(); + List nodeNamePhraseTerms = new ArrayList<>(); List nodeTypeTerms = new ArrayList<>(); + List uniqueIdTerms = new ArrayList<>(); boolean temporalSearch = false; ZonedDateTime start = ZonedDateTime.ofInstant(Instant.EPOCH, ZoneId.systemDefault()); ZonedDateTime end = ZonedDateTime.now(); @@ -69,11 +64,24 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara for (Entry> parameter : searchParameters.entrySet()) { switch (parameter.getKey().strip().toLowerCase()) { + case "uniqueid": + for (String value : parameter.getValue()) { + for (String pattern : value.split("[|,;]")) { + uniqueIdTerms.add(pattern.trim()); + } + } + break; // Search for node name. List of names cannot be split on space char as it is allowed in a node name. case "name": for (String value : parameter.getValue()) { for (String pattern : value.split("[|,;]")) { - nodeNameTerms.add(pattern.trim().toLowerCase()); + String term = pattern.trim().toLowerCase(); + if(term.startsWith("\"") && term.endsWith("\"")){ + nodeNamePhraseTerms.add(term.substring(1, term.length() - 1)); + } + else{ + nodeNameTerms.add(term); + } } } break; @@ -208,7 +216,7 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara DisMaxQuery.Builder descQuery = new DisMaxQuery.Builder(); List descQueries = new ArrayList<>(); if (fuzzySearch) { - descriptionTerms.stream().forEach(searchTerm -> { + descriptionTerms.forEach(searchTerm -> { Query fuzzyQuery = FuzzyQuery.of(f -> f.field("node.description").value(searchTerm))._toQuery(); NestedQuery nestedQuery = NestedQuery.of(n1 -> n1.path("node") @@ -216,7 +224,7 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara descQueries.add(nestedQuery._toQuery()); }); } else { - descriptionTerms.stream().forEach(searchTerm -> { + descriptionTerms.forEach(searchTerm -> { Query wildcardQuery = WildcardQuery.of(w -> w.field("node.description").value(searchTerm))._toQuery(); NestedQuery nestedQuery = @@ -229,6 +237,11 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara boolQueryBuilder.must(descQuery.build()._toQuery()); } + // Add uniqueId query + if(!uniqueIdTerms.isEmpty()){ + boolQueryBuilder.must(IdsQuery.of(id -> id.values(uniqueIdTerms))._toQuery()); + } + // Add the name query if (!nodeNameTerms.isEmpty()) { DisMaxQuery.Builder nodeNameQuery = new DisMaxQuery.Builder(); @@ -252,6 +265,19 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara boolQueryBuilder.must(nodeNameQuery.build()._toQuery()); } + if(!nodeNamePhraseTerms.isEmpty()){ + DisMaxQuery.Builder nodeNamePhraseQueryBuilder = new DisMaxQuery.Builder(); + List nestedQueries = new ArrayList<>(); + nodeNamePhraseTerms.forEach(phraseSearchTerm -> { + NestedQuery innerNestedQuery; + MatchPhraseQuery matchPhraseQuery = MatchPhraseQuery.of(m -> m.field("node.name").query(phraseSearchTerm)); + innerNestedQuery = NestedQuery.of(n -> n.path("node").query(matchPhraseQuery._toQuery())); + nestedQueries.add(innerNestedQuery); + }); + nodeNamePhraseQueryBuilder.queries(nestedQueries.stream().map(QueryVariant::_toQuery).collect(Collectors.toList())); + boolQueryBuilder.must(nodeNamePhraseQueryBuilder.build()._toQuery()); + } + // Add node type query. Fuzzy search not needed as node types are well-defined and limited in number. if (!nodeTypeTerms.isEmpty()) { DisMaxQuery.Builder nodeTypeQuery = new DisMaxQuery.Builder(); @@ -271,8 +297,43 @@ public SearchRequest buildSearchRequest(MultiValueMap searchPara return SearchRequest.of(s -> s.index(ES_TREE_INDEX) .query(boolQueryBuilder.build()._toQuery()) + .sort(SortOptions.of(o -> o + .field(FieldSort.of(f -> f + .field("node.name.raw") + .nested(n -> n.path("node")) + .order(SortOrder.Asc) + ) + ) + ) + ) .timeout("60s") .size(Math.min(_searchResultSize, maxSearchSize)) .from(_from)); } + + /** + * Builds a query on the configuration index to find {@link org.phoebus.applications.saveandrestore.model.ConfigurationData} + * documents containing any of the PV names passed to this method. Both setpoint and readback PV names are considered. + * @param pvNames List of PV names. Query will user or-strategy. + * @return A {@link SearchRequest} object, no limit on result size except maximum Elastic limit. + */ + public SearchRequest buildSearchRequestForPvs(List pvNames) { + int searchResultSize = defaultSearchSize; + Builder boolQueryBuilder = new Builder(); + DisMaxQuery.Builder pvQuery = new DisMaxQuery.Builder(); + List pvQueries = new ArrayList<>(); + for (String value : pvNames) { + for (String pattern : value.split("[|,;]")) { + pvQueries.add(MatchQuery.of(m -> m.field("pvList").query(pattern.trim()))._toQuery()); + } + } + Query pvsQuery = pvQuery.queries(pvQueries).build()._toQuery(); + boolQueryBuilder.must(pvsQuery); + + return SearchRequest.of(s -> s.index(ES_CONFIGURATION_INDEX) + .query(boolQueryBuilder.build()._toQuery()) + .timeout("60s") + .size(Math.min(searchResultSize, maxSearchSize)) + .from(0)); + } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/AuthEnabledCondition.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/AuthEnabledCondition.java new file mode 100644 index 0000000000..8063e2f9d9 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/AuthEnabledCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.config; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link Condition} subclass used to determine if authentication/authorization is enabled through an + * application property setting. + */ +public class AuthEnabledCondition implements Condition { + /** + * @param context the condition context + * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class} + * or {@link org.springframework.core.type.MethodMetadata method} being checked + * @return true if application property auth.impl is anything other than none. + */ + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return !"none".equalsIgnoreCase(context.getEnvironment().getProperty("auth.impl").trim()); + } +} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebSecurityConfig.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebSecurityConfig.java new file mode 100644 index 0000000000..090101b991 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/config/WebSecurityConfig.java @@ -0,0 +1,286 @@ +package org.phoebus.service.saveandrestore.web.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.*; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.http.HttpMethod; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.ldap.LdapAuthenticationProviderConfigurer; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory; +import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.PersonContextMapper; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@SuppressWarnings("unused") +public class WebSecurityConfig { + + /** + * Authentication implementation. + */ + @Value("${auth.impl:demo}") + protected String authenitcationImplementation; + + /** + * External Active Directory configuration properties + */ + @Value("${ad.url:ldap://localhost:389/}") + String ad_url; + @Value("${ad.domain}") + String ad_domain; + /** + * External LDAP configuration properties + */ + @Value("${ldap.urls:ldaps://localhost:389/}") + String ldap_url; + @Value("${ldap.base.dn}") + String ldap_base_dn; + @Value("${ldap.user.dn.pattern}") + String ldap_user_dn_pattern; + @Value("${ldap.groups.search.base}") + String ldap_groups_search_base; + @Value("${ldap.groups.search.pattern}") + String ldap_groups_search_pattern; + @Value("${ldap.manager.dn}") + String ldap_manager_dn; + @Value("${ldap.manager.password}") + String ldap_manager_password; + @Value("${ldap.user.search.base:invalid}") + String ldap_user_search_base; + @Value("${ldap.user.search.filter:invalid}") + String ldap_user_search_filter; + + @Value("${role.user:sar-user}") + public String roleUser; + + @Value("${role.admin:sar-admin}") + public String roleAdmin; + + @Value("${demo.user:user}") + public String demoUser; + + @Value("${demo.user.password:userPass}") + public String demoUserPassword; + + @Value("${demo.admin:admin}") + public String demoAdmin; + + @Value("${demo.admin.password:adminPass}") + public String demoAdminPassword; + + @Value("${demo.readOnly:johndoe}") + public String demoReadOnly; + + @Value("${demo.readOnly.password:1234}") + public String demoReadOnlyPassword; + + @Bean + public String roleUser() { + return roleUser.toUpperCase(); + } + + @Bean + public String roleAdmin() { + return roleAdmin.toUpperCase(); + } + + @Bean + public String demoUser(){ + return demoUser; + } + + @Bean + public String demoUserPassword(){ + return demoUserPassword; + } + + @Bean + public String demoAdmin(){ + return demoAdmin; + } + + @Bean + public String demoAdminPassword(){ + return demoAdminPassword; + } + + @Bean + public String demoReadOnly(){ + return demoReadOnly; + } + + @Bean + public String demoReadOnlyPassword(){ + return demoReadOnlyPassword; + } + + @Bean + public String authenticationImplementation(){ + return authenitcationImplementation; + } + @Bean + public WebSecurityCustomizer ignoringCustomizer() { + return web -> { + // The below lists exceptions for authentication. + web.ignoring().antMatchers(HttpMethod.GET, "/**"); + web.ignoring().antMatchers(HttpMethod.POST, "/**/login*"); + }; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.authorizeRequests().anyRequest().authenticated(); + http.httpBasic(); + return http.build(); + } + + @Bean + @Conditional(LdapAuthCondition.class) + public DefaultSpringSecurityContextSource contextSourceFactoryBeanLdap() { + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(ldap_url); + if (ldap_manager_dn != null && !ldap_manager_dn.isEmpty() && ldap_manager_password != null && !ldap_manager_password.isEmpty()) { + contextSource.setUserDn(ldap_manager_dn); + contextSource.setPassword(ldap_manager_password); + } + contextSource.setBase(ldap_base_dn); + return contextSource; + } + + @Bean + @Conditional(LdapAuthCondition.class) + public AuthenticationManager ldapAuthenticationManager( + BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = + new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns(ldap_user_dn_pattern); + factory.setUserDetailsContextMapper(new PersonContextMapper()); + + factory.setLdapAuthoritiesPopulator(authorities(contextSource)); + return factory.createAuthenticationManager(); + } + + @Bean + @Conditional(LdapAuthCondition.class) + public LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) { + DefaultLdapAuthoritiesPopulator myAuthPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, ldap_groups_search_base); + myAuthPopulator.setGroupSearchFilter(ldap_groups_search_pattern); + myAuthPopulator.setSearchSubtree(true); + myAuthPopulator.setIgnorePartialResultException(true); + LdapAuthenticationProviderConfigurer configurer = new LdapAuthenticationProviderConfigurer(); + if (ldap_user_dn_pattern != null && !ldap_user_dn_pattern.isEmpty()) { + configurer.userDnPatterns(ldap_user_dn_pattern); + } + if (ldap_user_search_filter != null && !ldap_user_search_filter.isEmpty()) { + configurer.userSearchFilter(ldap_user_search_filter); + } + if (ldap_user_search_base != null && !ldap_user_search_base.isEmpty()) { + configurer.userSearchBase(ldap_user_search_base); + } + configurer.contextSource(contextSource); + return myAuthPopulator; + } + + @Bean + @ConditionalOnProperty(name = "auth.impl", havingValue = "ad") + public AuthenticationManager authenticationProvider() throws Exception { + ActiveDirectoryLdapAuthenticationProvider adProvider = + new ActiveDirectoryLdapAuthenticationProvider(ad_domain, ad_url); + adProvider.setConvertSubErrorCodesToExceptions(true); + adProvider.setUseAuthenticationRequestCredentials(true); + adProvider.setUserDetailsContextMapper(new PersonContextMapper()); + SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper(); + simpleAuthorityMapper.setConvertToUpperCase(true); + adProvider.setAuthoritiesMapper(simpleAuthorityMapper); + return new AuthenticationManagerBuilder(new ObjectPostProcessor<>() { + @Override + public O postProcess(O object) { + return object; + } + }).authenticationProvider(adProvider).build(); + } + + @Bean + @ConditionalOnProperty(name = "auth.impl", havingValue = "demo") + public AuthenticationManager demoAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception { + return new AuthenticationManagerBuilder(new ObjectPostProcessor<>() { + @Override + public O postProcess(O object) { + return object; + } + }).inMemoryAuthentication() + .passwordEncoder(encoder()) + .withUser(demoAdmin).password(encoder().encode(demoAdminPassword)).roles(roleAdmin()).and() + .withUser(demoUser).password(encoder().encode(demoUserPassword)).roles(roleUser()).and() + .withUser(demoReadOnly).password(encoder().encode(demoReadOnlyPassword)).roles().and().and().build(); + } + + @Bean + @Scope("singleton") + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(); + } + + @SuppressWarnings("unused") + @Bean + @Scope("singleton") + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return objectMapper; + } + + /** + * Configures role hierarchy, i.e. user - superuser - admin. Do not remove this {@link Bean}! + *

      NOTE!

      + * Some Spring Security documentation will state that "and" can be used instead of new-line char to + * separate rule items. But that does NOT work, at least not with the Spring Security version used in this project. + * + * @return A {@link RoleHierarchy} object. + */ + @Bean + public RoleHierarchy roleHierarchy() { + RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); + hierarchy.setHierarchy("ROLE_" + roleAdmin.toUpperCase() + " > ROLE_" + roleUser.toUpperCase()); + return hierarchy; + } + + /** + * {@link Condition} subclass used to select ldap and ldap_embedded + * authentication/authorization provider. + */ + private static class LdapAuthCondition implements Condition { + /** + * @param context the condition context + * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class} + * or {@link org.springframework.core.type.MethodMetadata method} being checked + * @return true if application property auth.impl is ldap + * or ldap_embedded, otherwise false. + */ + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + String testValue = context.getEnvironment().getProperty("auth.impl"); + return "ldap".equals(testValue) || "ldap_embedded".equals(testValue); + } + } +} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthenticationController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthenticationController.java new file mode 100644 index 0000000000..7f501eb4f5 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthenticationController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import org.phoebus.applications.saveandrestore.model.UserData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +@RestController +public class AuthenticationController extends BaseController { + + @Autowired + private AuthenticationManager authenticationManager; + + /** + * Authenticates user. + * + * @param userName The user principal name + * @param password User's password + * @return A {@link ResponseEntity} carrying a {@link UserData} object if the login was successful, + * otherwise the body will be null. + */ + @PostMapping(value = "login") + public ResponseEntity login(@RequestParam(value = "username") String userName, + @RequestParam(value = "password") String password) { + Authentication authentication = + new UsernamePasswordAuthenticationToken(userName, password); + try { + authentication = authenticationManager.authenticate(authentication); + } catch (AuthenticationException e) { + Logger.getLogger(AuthenticationController.class.getName()).log(Level.WARNING, "Unable to authenticate", e); + return new ResponseEntity<>( + null, + HttpStatus.UNAUTHORIZED); + } + List roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + return new ResponseEntity<>( + new UserData(userName, roles), + HttpStatus.OK); + } +} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java new file mode 100644 index 0000000000..c5244627d3 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/AuthorizationHelper.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import org.phoebus.applications.saveandrestore.model.*; +import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +/** + * {@link Service} class implementing domain specific authorization rules in order to + * grant or deny access to certain REST endpoints. + */ +@Service("authorizationHelper") +@SuppressWarnings("unused") +public class AuthorizationHelper { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private String roleAdmin; + + @Autowired + private String roleUser; + + @Value("${authorization.permitall:true}") + public boolean permitAll; + + private static final String ROLE_PREFIX = "ROLE_"; + + /** + * Checks if all the nodes provided to this method can be deleted by the user. User with admin privileges is always + * permitted to delete, while a user not having required role may never delete. + * + * @param nodeIds The list of {@link Node} ids subject to the check. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return true only if all if the nodes can be deleted by the user. + */ + public boolean mayDelete(List nodeIds, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + for (String nodeId : nodeIds) { + if (!mayDelete(nodeId, ((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername())) { + return false; + } + } + return true; + } + + /** + * An authenticated user may delete id user identity is same as the target {@link Node}'s user unique id and: + *
        + *
      • Target {@link Node} is a snapshot.
      • + *
      • Target {@link Node} is not a snapshot, but has no child nodes.
      • + *
      + * + * @param nodeId The target {@link Node}'s unique node id. + * @param userName {@link MethodSecurityExpressionOperations} Username of authenticated user. + * @return false if user may not delete the {@link Node}. + */ + private boolean mayDelete(String nodeId, String userName) { + Node node = nodeDAO.getNode(nodeId); + if (!node.getUserName().equals(userName)) { + return false; + } + if (node.getNodeType().equals(NodeType.CONFIGURATION) || node.getNodeType().equals(NodeType.FOLDER)) { + return nodeDAO.getChildNodes(node.getUniqueId()).isEmpty(); + } + return true; + } + + /** + * An authenticated user may update a node if user has admin privileges, or + * if user identity is same as the target {@link Node}'s user id. + * + * @param node {@link Node} identifying the target of the user's update operation. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not update the {@link Node}. + */ + public boolean mayUpdate(Node node, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + return nodeDAO.getNode(node.getUniqueId()).getUserName() + .equals(((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername()); + } + + /** + * An authenticated user may update a composite snapshot if user has admin privileges, or + * if user identity is same as the target {@link Node}'s user id. + * + * @param compositeSnapshot {@link CompositeSnapshot} identifying the target of the user's update operation. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not update the {@link CompositeSnapshot}. + */ + public boolean mayUpdate(CompositeSnapshot compositeSnapshot, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + return nodeDAO.getNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()).getUserName() + .equals(((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername()); + } + + /** + * An authenticated user may update a configuration if user has admin privileges, or + * if user identity is same as the target {@link Node}'s user id. + * + * @param configuration {@link Configuration} identifying the target of the user's update operation. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not update the {@link Configuration}. + */ + public boolean mayUpdate(Configuration configuration, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + return nodeDAO.getNode(configuration.getConfigurationNode().getUniqueId()).getUserName() + .equals(((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername()); + } + + /** + * An authenticated user may update a snapshot if user has admin privileges, or + * if user identity is same as the target {@link Node}'s user id. + * + * @param snapshot {@link Snapshot} identifying the target of the user's update operation. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not update the {@link Snapshot}. + */ + public boolean mayUpdate(Snapshot snapshot, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + // If snapshot's node has null id, then this is a + return nodeDAO.getNode(snapshot.getSnapshotNode().getUniqueId()).getUserName() + .equals(((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername()); + } + + /** + * An authenticated user may add or delete {@link Tag}s if user identity is same as the target's + * snapshot {@link Node}. However, to add or delete golden tag user must have admin privileges. + * + * @param tagData {@link TagData} containing {@link Node} ids and {@link Tag} name. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return true if {@link Tag} can be added or deleted. + */ + public boolean mayAddOrDeleteTag(TagData tagData, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (tagData.getTag() == null || + tagData.getTag().getName() == null || + tagData.getTag().getName().isEmpty() || + tagData.getUniqueNodeIds() == null) { + throw new IllegalArgumentException("Cannot add tag, data invalid"); + } + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + Tag tag = tagData.getTag(); + if (tag.getName().equals(Tag.GOLDEN)) { + return methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin); + } + String username = ((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername(); + for (String nodeId : tagData.getUniqueNodeIds()) { + Node node = nodeDAO.getNode(nodeId); + if (!node.getUserName().equals(username)) { + return false; + } + } + return true; + } + + /** + *

      + * An authenticated user may save a filter, and update/delete if user identity is same as the target's + * name field. + *

      + * + * @param filterName Unique name identifying the target of the user's update operation. + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not update the {@link Filter}. + */ + public boolean maySaveOrDeleteFilter(String filterName, MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + if (permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin)) { + return true; + } + if (!methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser)) { + return false; + } + Optional filter1 = + nodeDAO.getAllFilters().stream().filter(f -> + f.getName().equals(filterName)).findFirst(); + // If the filter does not (yet) exist, save is OK + if (filter1.isEmpty()) { + return true; + } + String username = ((UserDetails) methodSecurityExpressionOperations.getAuthentication().getPrincipal()).getUsername(); + return filter1.map(value -> value.getUser().equals(username)).orElse(true); + } + + /** + * Checks if user is allowed to create an object (node, snapshot...). This is the case if authorization + * is disabled or if user has (basic) user role. + * + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not create the object, otherwise true. + */ + public boolean mayCreate(MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + return permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleUser); + } + + /** + * Checks if user is allowed to move or copy objects. This is the case if authorization + * is disabled or if user has admin role. + * + * @param methodSecurityExpressionOperations {@link MethodSecurityExpressionOperations} Spring managed object + * queried for authorization. + * @return false if user may not create the object, otherwise true. + */ + public boolean mayMoveOrCopy(MethodSecurityExpressionOperations methodSecurityExpressionOperations) { + return permitAll || methodSecurityExpressionOperations.hasAuthority(ROLE_PREFIX + roleAdmin); + } +} diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/BaseController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/BaseController.java index ff87d74339..8a025c82bb 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/BaseController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/BaseController.java @@ -1,33 +1,32 @@ -/** +/** * Copyright (C) 2018 European Spallation Source ERIC. - * + *

      * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. - * + *

      * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + *

      * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.phoebus.service.saveandrestore.web.controllers; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; - +import org.phoebus.service.saveandrestore.NodeNotFoundException; +import org.phoebus.service.saveandrestore.SnapshotNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; -import org.phoebus.service.saveandrestore.NodeNotFoundException; -import org.phoebus.service.saveandrestore.SnapshotNotFoundException; - +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; import java.util.logging.Level; import java.util.logging.Logger; @@ -40,52 +39,58 @@ @RestController @SuppressWarnings("unused") public abstract class BaseController { - - public static final String JSON = "application/json"; - - private final Logger logger = Logger.getLogger(BaseController.class.getName()); + public static final String JSON = "application/json"; + + private final Logger logger = Logger.getLogger(BaseController.class.getName()); + + @Autowired + public String roleAdmin; // This MUST be public!!! + + @Autowired + public String roleUser; // This MUST be public!!! + + + /** + * Intercepts {@link SnapshotNotFoundException} and triggers a {@link HttpStatus#NOT_FOUND}. + * @param req The servlet request + * @param exception The exception to intercept + * @return A {@link ResponseEntity} carrying the underlying exception message. + */ + @ExceptionHandler(SnapshotNotFoundException.class) + public ResponseEntity handleSnapshotNotFoundException(HttpServletRequest req, + SnapshotNotFoundException exception) { + log(exception); + return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND); + } + + /** + * Intercepts {@link IllegalArgumentException} and triggers a {@link HttpStatus#BAD_REQUEST}. + * @param req The servlet request + * @param exception The exception to intercept + * @return A {@link ResponseEntity} carrying the underlying exception message. + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(HttpServletRequest req, + IllegalArgumentException exception) { + log(exception); + return new ResponseEntity<>(exception.getMessage(), HttpStatus.BAD_REQUEST); + } + + /** + * Intercepts {@link NodeNotFoundException} and triggers a {@link HttpStatus#NOT_FOUND}. + * @param req The {@link HttpServlet} request + * @param exception The exception to intercept + * @return A {@link ResponseEntity} carrying the underlying exception message. + */ + @ExceptionHandler(NodeNotFoundException.class) + public ResponseEntity handleNodeNotFoundException(HttpServletRequest req, + NodeNotFoundException exception) { + log(exception); + return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND); + } - /** - * Intercepts {@link SnapshotNotFoundException} and triggers a {@link HttpStatus#NOT_FOUND}. - * @param req The servlet request - * @param exception The exception to intercept - * @return A {@link ResponseEntity} carrying the underlying exception message. - */ - @ExceptionHandler(SnapshotNotFoundException.class) - public ResponseEntity handleSnapshotNotFoundException(HttpServletRequest req, - SnapshotNotFoundException exception) { - log(exception); - return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND); - } - - /** - * Intercepts {@link IllegalArgumentException} and triggers a {@link HttpStatus#BAD_REQUEST}. - * @param req The servlet request - * @param exception The exception to intercept - * @return A {@link ResponseEntity} carrying the underlying exception message. - */ - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(HttpServletRequest req, - IllegalArgumentException exception) { - log(exception); - return new ResponseEntity<>(exception.getMessage(), HttpStatus.BAD_REQUEST); - } - - /** - * Intercepts {@link NodeNotFoundException} and triggers a {@link HttpStatus#NOT_FOUND}. - * @param req The {@link HttpServlet} request - * @param exception The exception to intercept - * @return A {@link ResponseEntity} carrying the underlying exception message. - */ - @ExceptionHandler(NodeNotFoundException.class) - public ResponseEntity handleNodeNotFoundException(HttpServletRequest req, - NodeNotFoundException exception) { - log(exception); - return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND); - } - - private void log(Throwable throwable) { - logger.log(Level.INFO, "Intercepted " + throwable.getClass().getName(), throwable); - } + private void log(Throwable throwable) { + logger.log(Level.INFO, "Intercepted " + throwable.getClass().getName(), throwable); + } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java index 66358d7f6a..96690cf79b 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotController.java @@ -19,12 +19,10 @@ package org.phoebus.service.saveandrestore.web.controllers; -import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; -import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.applications.saveandrestore.model.*; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -33,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.security.Principal; import java.util.List; @SuppressWarnings("unused") @@ -43,17 +42,28 @@ public class CompositeSnapshotController extends BaseController { private NodeDAO nodeDAO; @PutMapping(value = "/composite-snapshot", produces = JSON) + @PreAuthorize("@authorizationHelper.mayCreate(#root)") public CompositeSnapshot createCompositeSnapshot(@RequestParam(value = "parentNodeId") String parentNodeId, - @RequestBody CompositeSnapshot compositeSnapshot) { + @RequestBody CompositeSnapshot compositeSnapshot, + Principal principal) { + if(!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)){ + throw new IllegalArgumentException("Composite snapshot node of wrong type"); + } + compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); return nodeDAO.createCompositeSnapshot(parentNodeId, compositeSnapshot); } @PostMapping(value = "/composite-snapshot", produces = JSON) - public CompositeSnapshot updateCompositeSnapshot(@RequestBody CompositeSnapshot compositeSnapshot) { + @PreAuthorize("@authorizationHelper.mayUpdate(#compositeSnapshot, #root)") + public CompositeSnapshot updateCompositeSnapshot(@RequestBody CompositeSnapshot compositeSnapshot, + Principal principal) { + if(!compositeSnapshot.getCompositeSnapshotNode().getNodeType().equals(NodeType.COMPOSITE_SNAPSHOT)){ + throw new IllegalArgumentException("Composite snapshot node of wrong type"); + } + compositeSnapshot.getCompositeSnapshotNode().setUserName(principal.getName()); return nodeDAO.updateCompositeSnapshot(compositeSnapshot); } - @GetMapping(value = "/composite-snapshot/{uniqueId}", produces = JSON) public CompositeSnapshotData getCompositeSnapshotData(@PathVariable String uniqueId) { return nodeDAO.getCompositeSnapshotData(uniqueId); diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java index f4d4d96e16..81813e3a27 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationController.java @@ -19,8 +19,10 @@ import org.phoebus.applications.saveandrestore.model.Configuration; import org.phoebus.applications.saveandrestore.model.ConfigurationData; +import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -30,6 +32,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.security.Principal; + @RestController @RequestMapping("/config") public class ConfigurationController extends BaseController { @@ -40,8 +44,11 @@ public class ConfigurationController extends BaseController { @SuppressWarnings("unused") @PutMapping(produces = JSON) + @PreAuthorize("@authorizationHelper.mayCreate(#root)") public Configuration createConfiguration(@RequestParam(value = "parentNodeId") String parentNodeId, - @RequestBody Configuration configuration) { + @RequestBody Configuration configuration, + Principal principal) { + configuration.getConfigurationNode().setUserName(principal.getName()); return nodeDAO.createConfiguration(parentNodeId, configuration); } @@ -53,7 +60,10 @@ public ConfigurationData getConfigurationData(@PathVariable String uniqueId) { @SuppressWarnings("unused") @PostMapping(produces = JSON) - public Configuration updateConfiguration(@RequestBody Configuration configuration) { + @PreAuthorize("@authorizationHelper.mayUpdate(#configuration, #root)") + public Configuration updateConfiguration(@RequestBody Configuration configuration, + Principal principal) { + configuration.getConfigurationNode().setUserName(principal.getName()); return nodeDAO.updateConfiguration(configuration); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java index 19a26af94e..11ca952fc5 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java @@ -22,15 +22,12 @@ import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; -import java.util.logging.Logger; +import java.util.Optional; @RestController public class FilterController extends BaseController { @@ -39,27 +36,32 @@ public class FilterController extends BaseController { @Autowired private NodeDAO nodeDAO; - private Logger logger = Logger.getLogger(FilterController.class.getName()); - /** * Saves a new or updated {@link Filter}. * - * @param filter The {@link Filter} to save. + * @param filter The {@link Filter} to save. + * @param principal The {@link java.security.Principal} of the authenticated user * @return The saved {@link Filter}. */ @SuppressWarnings("unused") @PutMapping(value = "/filter", produces = JSON) - public Filter saveFilter(@RequestBody final Filter filter) { + @PreAuthorize("@authorizationHelper.maySaveOrDeleteFilter(#filter.getName(), #root)") + public Filter saveFilter(@RequestBody final Filter filter, + Principal principal) { + filter.setUser(principal.getName()); return nodeDAO.saveFilter(filter); } + @SuppressWarnings("unused") @GetMapping(value = "/filters", produces = JSON) public List getAllFilters() { return nodeDAO.getAllFilters(); } + @SuppressWarnings("unused") @DeleteMapping(value = "/filter/{name}") - public void deleteFilter(@PathVariable final String name) { + @PreAuthorize("@authorizationHelper.maySaveOrDeleteFilter(#name, #root)") + public void deleteFilter(@PathVariable final String name, Principal principal) { nodeDAO.deleteFilter(name); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/InfoController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/InfoController.java new file mode 100644 index 0000000000..0e6f5e6d49 --- /dev/null +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/InfoController.java @@ -0,0 +1,80 @@ +package org.phoebus.service.saveandrestore.web.controllers; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchVersionInfo; +import co.elastic.clients.elasticsearch.core.InfoResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.phoebus.applications.saveandrestore.model.Node.ROOT_FOLDER_UNIQUE_ID; + +/** + * Controller implementing endpoints to retrieve service info + */ +@RestController +public class InfoController extends BaseController { + + private final Logger logger = Logger.getLogger(InfoController.class.getName()); + + @Value("${app.name:4.7.0}") + private String name; + + @Value("${app.version:4.7.0}") + private String version; + + @Autowired + @Qualifier("client") + ElasticsearchClient client; + + private static final ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + + /** + * + * @return Information about the Save and Restore service + */ + @GetMapping + public String info() { + + Map saveRestoreServiceInfo = new LinkedHashMap<>(); + saveRestoreServiceInfo.put("name", name); + saveRestoreServiceInfo.put("version", version); + + Map elasticInfo = new LinkedHashMap<>(); + try { + InfoResponse response = client.info(); + elasticInfo.put("status", "Connected"); + elasticInfo.put("clusterName", response.clusterName()); + elasticInfo.put("clusterUuid", response.clusterUuid()); + ElasticsearchVersionInfo version = response.version(); + elasticInfo.put("version", version.toString()); + //elasticInfo.put("elasticHost", host); + //elasticInfo.put("elasticPort", String.valueOf(port)); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to create Save and Restore service info resource.", e); + elasticInfo.put("status", "Failed to connect to elastic " + e.getLocalizedMessage()); + } + saveRestoreServiceInfo.put("elastic", elasticInfo); + + saveRestoreServiceInfo.put("rootNodeID", ROOT_FOLDER_UNIQUE_ID); + + try { + return objectMapper.writeValueAsString(saveRestoreServiceInfo); + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Failed to create Save and Restore service info resource.", e); + return "Failed to gather Save and Restore service info"; + } + } +} \ No newline at end of file diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java index 31e90a7fd4..95ef3471c1 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/NodeController.java @@ -23,16 +23,14 @@ import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; import java.util.logging.Logger; @@ -47,7 +45,7 @@ public class NodeController extends BaseController { @Autowired private NodeDAO nodeDAO; - private Logger logger = Logger.getLogger(NodeController.class.getName()); + private final Logger logger = Logger.getLogger(NodeController.class.getName()); /** * Create a new folder in the tree structure. @@ -60,22 +58,24 @@ public class NodeController extends BaseController { * * * @param parentsUniqueId The unique id of the parent node for the new node. - * @param node A {@link Node} object. The {@link Node#getName()} and {@link Node#getUserName()} ()} fields must be + * @param node A {@link Node} object. The {@link Node#getName()} field must be * non-null and non-empty. + * @param principal Authenticated user's {@link java.security.Principal} * @return The new folder in the tree. */ @SuppressWarnings("unused") @PutMapping(value = "/node", produces = JSON) - public Node createNode(@RequestParam(name = "parentNodeId") String parentsUniqueId, @RequestBody final Node node) { - if (node.getUserName() == null || node.getUserName().isEmpty()) { - throw new IllegalArgumentException("User name must be non-null and of non-zero length"); - } + @PreAuthorize("@authorizationHelper.mayCreate(#root)") + public Node createNode(@RequestParam(name = "parentNodeId") String parentsUniqueId, + @RequestBody final Node node, + Principal principal) { if (node.getName() == null || node.getName().isEmpty()) { throw new IllegalArgumentException("Node name must be non-null and of non-zero length"); } - if(!areTagsValid(node)){ + if (!areTagsValid(node)) { throw new IllegalArgumentException("Node may not contain golden tag"); } + node.setUserName(principal.getName()); return nodeDAO.createNode(parentsUniqueId, node); } @@ -132,50 +132,93 @@ public List getChildNodes(@PathVariable final String uniqueNodeId) { *

      * * @param uniqueNodeId The non-zero id of the node to delete + * @param authentication {@link Authentication} of authenticated user. */ + /* @SuppressWarnings("unused") @DeleteMapping(value = "/node/{uniqueNodeId}", produces = JSON) - public void deleteNode(@PathVariable final String uniqueNodeId) { + @PreAuthorize("hasRole(this.roleAdmin) or @authorizationHelper.mayDelete(#uniqueNodeId, #authentication)") + @Deprecated + public void deleteNode(@PathVariable final String uniqueNodeId, Authentication authentication) { logger.info("Deleting node with unique id " + uniqueNodeId); nodeDAO.deleteNode(uniqueNodeId); } + */ + + /** + * Deletes all {@link Node}s contained in the provided list. + *
      + * Checks are made to make sure user may delete + * the {@link Node}s, see {@link AuthorizationHelper}. If the checks fail on any of the {@link Node} ids, + * checks are aborted and client will receive an HTTP 403 response. + *
      + * Note that the {@link PreAuthorize} annotations calls a helper method in {@link AuthorizationHelper}, using + * the list of {@link Node} ids and a Spring injected object - root - used to check + * authorities of the user. + *
      + * Note also that an unauthenticated user (e.g. no basic authentication header in client's request) will + * receive a HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * @param nodeIds + */ + @SuppressWarnings("unused") + @DeleteMapping(value = "/node", produces = JSON) + @PreAuthorize("@authorizationHelper.mayDelete(#nodeIds, #root)") + public void deleteNodes(@RequestBody List nodeIds) { + nodeDAO.deleteNodes(nodeIds); + } + /** * Updates a {@link Node}. The purpose is to support modification of name or comment/description, or both. Modification of * node type is not supported. * + *
      + * Checks are made to make sure user may update + * the {@link Node}, see {@link AuthorizationHelper}. If the checks fail client will receive an HTTP 403 response. + *
      + * Note that the {@link PreAuthorize} annotations calls a helper method in {@link AuthorizationHelper}, using + * the of {@link Node} id and a Spring injected object - root - used to check + * authorities of the user. + *
      + * Note also that an unauthenticated user (e.g. no basic authentication header in client's request) will + * receive a HTTP 401 response, i.e. the {@link PreAuthorize} check is not invoked. + * *

      * A {@link HttpStatus#BAD_REQUEST} is returned if a node of the same name and type already exists in the parent folder, * or if the node in question is the root node (0). *

      * * @param customTimeForMigration Self-explanatory - * @param nodeToUpdate {@link Node} object containing updated data. Only name, description and properties may be changed. The user name - * should be set by the client in an automated fashion and will be updated by the persistence layer. + * @param nodeToUpdate {@link Node} object containing updated data. Only name, description and properties may be changed. + * @param principal Authenticated user's {@link java.security.Principal} * @return A {@link Node} object representing the updated node. */ @SuppressWarnings("unused") @PostMapping(value = "/node", produces = JSON) + @PreAuthorize("@authorizationHelper.mayUpdate(#nodeToUpdate, #root)") public Node updateNode(@RequestParam(value = "customTimeForMigration", required = false, defaultValue = "false") String customTimeForMigration, - @RequestBody Node nodeToUpdate) { - if(!areTagsValid(nodeToUpdate)){ + @RequestBody Node nodeToUpdate, + Principal principal) { + if (!areTagsValid(nodeToUpdate)) { throw new IllegalArgumentException("Node may not contain golden tag"); } + nodeToUpdate.setUserName(principal.getName()); return nodeDAO.updateNode(nodeToUpdate, Boolean.valueOf(customTimeForMigration)); } /** * Checks if a {@link Node} has a tag named "golden". If so, it must be of type {@link NodeType#SNAPSHOT}. + * * @param node A {@link Node} with potentially null or empty list of tags. * @return true if the {@link Node} in question has a valid list of tags, otherwise false. */ - private boolean areTagsValid(Node node){ - if(node.getTags() == null || node.getTags().isEmpty()){ + private boolean areTagsValid(Node node) { + if (node.getTags() == null || node.getTags().isEmpty()) { return true; } - if(!node.getNodeType().equals(NodeType.SNAPSHOT) && - node.getTags().stream().filter(t -> t.getName().equalsIgnoreCase(Tag.GOLDEN)).findFirst().isPresent()){ + if (!node.getNodeType().equals(NodeType.SNAPSHOT) && + node.getTags().stream().anyMatch(t -> t.getName().equalsIgnoreCase(Tag.GOLDEN))) { return false; } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java index 629dd48061..5654233e3c 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotController.java @@ -17,22 +17,16 @@ */ package org.phoebus.service.saveandrestore.web.controllers; -import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; -import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.SnapshotData; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; @SuppressWarnings("unused") @@ -53,8 +47,25 @@ public List getAllSnapshots() { } @PutMapping(value = "/snapshot", produces = JSON) - public Snapshot saveSnapshot(@RequestParam(value = "parentNodeId") String parentNodeId, - @RequestBody Snapshot snapshot) { - return nodeDAO.saveSnapshot(parentNodeId, snapshot); + @PreAuthorize("@authorizationHelper.mayCreate(#root)") + public Snapshot createSnapshot(@RequestParam(value = "parentNodeId") String parentNodeId, + @RequestBody Snapshot snapshot, + Principal principal) { + if(!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)){ + throw new IllegalArgumentException("Snapshot node of wrong type"); + } + snapshot.getSnapshotNode().setUserName(principal.getName()); + return nodeDAO.createSnapshot(parentNodeId, snapshot); + } + + @PostMapping(value = "/snapshot", produces = JSON) + @PreAuthorize("@authorizationHelper.mayUpdate(#snapshot, #root)") + public Snapshot updateSnapshot(@RequestBody Snapshot snapshot, + Principal principal) { + if(!snapshot.getSnapshotNode().getNodeType().equals(NodeType.SNAPSHOT)){ + throw new IllegalArgumentException("Snapshot node of wrong type"); + } + snapshot.getSnapshotNode().setUserName(principal.getName()); + return nodeDAO.updateSnapshot(snapshot); } } diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java index 72c4f94359..4bfc3e1cc8 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/StructureController.java @@ -21,14 +21,11 @@ import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; +import java.security.Principal; import java.util.Date; import java.util.List; import java.util.logging.Logger; @@ -44,54 +41,51 @@ public class StructureController extends BaseController { @Autowired private NodeDAO nodeDAO; - private Logger logger = Logger.getLogger(StructureController.class.getName()); - - /** * Moves a list of source nodes to a new target (parent) node. * - * @param to The unique id of the new parent, which must be a folder. If empty or if - * target node does not exist, {@link HttpStatus#BAD_REQUEST} is returned. - * @param userName Identity of the user performing the action on the client. - * If empty, {@link HttpStatus#BAD_REQUEST} is returned. - * @param nodes List of source nodes to move. If empty, or if any of the listed source nodes does not exist, - * {@link HttpStatus#BAD_REQUEST} is returned. + * @param to The unique id of the new parent, which must be a folder. If empty or if + * target node does not exist, {@link HttpStatus#BAD_REQUEST} is returned. + * @param nodes List of source nodes to move. If empty, or if any of the listed source nodes does not exist, + * {@link HttpStatus#BAD_REQUEST} is returned. + * @param principal The {@link Principal} of the authenticated user. * @return The (updated) target node. */ @SuppressWarnings("unused") @PostMapping(value = "/move", produces = JSON) + @PreAuthorize("@authorizationHelper.mayMoveOrCopy(#root)") public Node moveNodes(@RequestParam(value = "to") String to, - @RequestParam(value = "username") String userName, - @RequestBody List nodes) { - if (to.isEmpty() || userName.isEmpty() || nodes.isEmpty()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username, target node and list of source nodes must all be non-empty."); + @RequestBody List nodes, + Principal principal) { + if (to.isEmpty() || nodes.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target node and list of source nodes must all be non-empty."); } Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " move"); - return nodeDAO.moveNodes(nodes, to, userName); + return nodeDAO.moveNodes(nodes, to, principal.getName()); } /** * Copies a list of source nodes to a target (parent) node. Since the source nodes may contain sub-trees at * any depth, the copy operation needs to do a deep copy, which may take some time to complete. * - * @param to The unique id of the target parent node, which must be a folder. If empty or if - * target node does not exist, {@link HttpStatus#BAD_REQUEST} is returned. - * @param userName Identity of the user performing the action on the client. - * If empty, {@link HttpStatus#BAD_REQUEST} is returned. - * @param nodes List of source nodes to copy. If empty, or if any of the listed source nodes does not exist, - * {@link HttpStatus#BAD_REQUEST} is returned. + * @param to The unique id of the target parent node, which must be a folder. If empty or if + * target node does not exist, {@link HttpStatus#BAD_REQUEST} is returned. + * @param nodes List of source nodes to copy. If empty, or if any of the listed source nodes does not exist, + * {@link HttpStatus#BAD_REQUEST} is returned. + * @param principal The {@link Principal} of the authenticated user. * @return The (updated) target node. */ @SuppressWarnings("unused") @PostMapping(value = "/copy", produces = JSON) + @PreAuthorize("@authorizationHelper.mayMoveOrCopy(#root)") public Node copyNodes(@RequestParam(value = "to") String to, - @RequestParam(value = "username") String userName, - @RequestBody List nodes) { - if (to.isEmpty() || userName.isEmpty() || nodes.isEmpty()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username, target node and list of source nodes must all be non-empty."); + @RequestBody List nodes, + Principal principal) { + if (to.isEmpty() || nodes.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target node and list of source nodes must all be non-empty."); } Logger.getLogger(StructureController.class.getName()).info(Thread.currentThread().getName() + " " + (new Date()) + " move"); - return nodeDAO.copyNodes(nodes, to, userName); + return nodeDAO.copyNodes(nodes, to, principal.getName()); } /** diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java index 67a925c030..2c04dac8df 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/TagController.java @@ -1,23 +1,23 @@ /** * Copyright (C) 2020 Facility for Rare Isotope Beams - * + *

      * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + *

      * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + *

      * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * + *

      * Contact Information: Facility for Rare Isotope Beam, - * Michigan State University, - * East Lansing, MI 48824-1321 - * http://frib.msu.edu + * Michigan State University, + * East Lansing, MI 48824-1321 + * http://frib.msu.edu */ package org.phoebus.service.saveandrestore.web.controllers; @@ -26,17 +26,14 @@ import org.phoebus.applications.saveandrestore.model.TagData; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; /** - * {@link TagController} class for supporting RESTful APIs for tag + * {@link TagController} class for supporting REST-ful APIs for tag * * @author Genie Jhang */ @@ -56,34 +53,29 @@ public List getTags() { /** * Adds a {@link Tag} to specified list of target {@link Node}s. The {@link Tag} contained * in tagData must be non-null, and its name must be non-null and non-empty. - * @param tagData See {@link TagData} + * + * @param tagData See {@link TagData} + * @param principal {@link Principal} of authenticated user. * @return The list of updated {@link Node}s */ @PostMapping("/tags") - public List addTag(@RequestBody TagData tagData){ - if(tagData.getTag() == null || - tagData.getTag().getName() == null || - tagData.getTag().getName().isEmpty() || - tagData.getUniqueNodeIds() == null){ - throw new IllegalArgumentException("Cannot add tag, data invalid"); - } + @PreAuthorize("@authorizationHelper.mayAddOrDeleteTag(#tagData, #root)") + public List addTag(@RequestBody TagData tagData, + Principal principal) { + tagData.getTag().setUserName(principal.getName()); return nodeDAO.addTag(tagData); } /** * Removes a {@link Tag} from specified list of target {@link Node}s. The {@link Tag} contained - * * in tagData must be non-null, and its name must be non-null and non-empty. + * * in tagData must be non-null, and its name must be non-null and non-empty. + * * @param tagData See {@link TagData} * @return The list of updated {@link Node}s */ @DeleteMapping("/tags") - public List deleteTag(@RequestBody TagData tagData){ - if(tagData.getTag() == null || - tagData.getTag().getName() == null || - tagData.getTag().getName().isEmpty() || - tagData.getUniqueNodeIds() == null){ - throw new IllegalArgumentException("Cannot add tag, data invalid"); - } + @PreAuthorize("@authorizationHelper.mayAddOrDeleteTag(#tagData, #root)") + public List deleteTag(@RequestBody TagData tagData) { return nodeDAO.deleteTag(tagData); } } diff --git a/services/save-and-restore/src/main/resources/application.properties b/services/save-and-restore/src/main/resources/application.properties index cdf8294b76..525552efcb 100644 --- a/services/save-and-restore/src/main/resources/application.properties +++ b/services/save-and-restore/src/main/resources/application.properties @@ -1,6 +1,6 @@ -logging.level.org.springframework=INFO +logging.level.org.springframework=DEBUG app.version=@project.version@ -app.name=@project.name@ +app.name=@project.artifactId@ # The server port for the rest service server.port=8080 @@ -12,6 +12,8 @@ elasticsearch.http.port=9200 # Do not change this! spring.jackson.serialization.write-dates-as-timestamps=false +server.servlet.contextPath=/save-restore + # The names of the index to use for save&restore elasticsearch.tree_node.index=saveandrestore_tree elasticsearch.configuration_node.index=saveandrestore_configuration @@ -27,3 +29,66 @@ logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=INFO # serve a resource like for instance SearchHelp.html spring.mvc.static-path-pattern=/** +############## AD - External ############## +ad.enabled = false +ad.url = ldap://127.0.0.1 +ad.domain = test.com + +############## LDAP - External ############## +# If uncommenting in this section, make sure +# to comment out in LDAP - Embedded section +############################################# +#ldap.urls=ldaps://ldap-cslab.cslab.esss.lu.se +#ldap.base.dn = dc=esss,dc=lu,dc=se +#ldap.user.search.base= +# User search pattern, e.g. uid={0},ou=People. No default value as LDAP environment may not +# support user search by pattern. +#ldap.user.dn.pattern=uid={0},ou=Users +# User search filter, e.g. (&(objectClass=person)(SAMAccountName={0})). No default value as LDAP environment +# may not support user search by filter. +#ldap.user.search.filter= +#ldap.groups.search.base = ou=Groups +#ldap.groups.search.pattern = (memberUid= {1}) +# dn of manager account, may be required for group search +ldap.manager.dn= +# password of account +ldap.manager.password= + +############## LDAP - Embedded ############## +# If uncommenting in this section, make sure +# to comment out in LDAP - External section +############################################# +ldap.urls=ldap://localhost:8389/dc=sar,dc=local +ldap.base.dn = dc=sar,dc=local +ldap.user.dn.pattern = uid={0},ou=Group +ldap.groups.search.base = ou=Group +ldap.groups.search.pattern = (memberUid= {1}) +spring.ldap.embedded.ldif=classpath:sar.ldif +spring.ldap.embedded.base-dn=dc=sar,dc=local +spring.ldap.embedded.port=8389 +spring.ldap.embedded.validation.enabled=false + +############## Demo credentials ############## +demo.user=user +demo.user.password=userPass +demo.admin=admin +demo.admin.password=adminPass +demo.readOnly=johndoe +demo.readOnly.password=1234 + +############## Authentication Implementation ############## +# Supported options: +# ad - Microsoft AD +# ldap - Probably Open LDAP +# ldap_embedded - Embedded LDAP. Config in sar.ldif +# demo - Hard coded, see WebSecurityConfig class +auth.impl = demo + +###### Bypass authorization (but not authentication!) ####### +authorization.permitall = true + +############## Authorization Roles ################ + +role.user=sar-user +role.admin=sar-admin + diff --git a/services/save-and-restore/src/main/resources/configuration_mapping.json b/services/save-and-restore/src/main/resources/configuration_mapping.json index d528ff276a..37c984d68b 100644 --- a/services/save-and-restore/src/main/resources/configuration_mapping.json +++ b/services/save-and-restore/src/main/resources/configuration_mapping.json @@ -5,18 +5,7 @@ "type" : "keyword" }, "pvList": { - "type" : "nested", - "properties": { - "pvName": { - "type": "keyword" - }, - "readbackPvName": { - "type": "keyword" - }, - "readOnly": { - "type": "boolean" - } - } + "type" : "flattened" } } } diff --git a/services/save-and-restore/src/main/resources/sar.ldif b/services/save-and-restore/src/main/resources/sar.ldif new file mode 100644 index 0000000000..91133bfb6a --- /dev/null +++ b/services/save-and-restore/src/main/resources/sar.ldif @@ -0,0 +1,65 @@ +dn: dc=sar,dc=local +objectClass: dcObject +objectClass: organization +dc: sar +o: sar + +dn: cn=sar,dc=sar,dc=local +objectClass: simpleSecurityObject +objectClass: organizationalRole +userPassword:: e1NIQX1jUkR0cE5DZUJpcWw1S09Rc0tWeXJBMHNBaUE9 +cn: sar + +dn: ou=Group,dc=sar,dc=local +objectClass: organizationalunit +ou: Group +description: groups branch + +dn: cn=sar-user,ou=Group,dc=sar,dc=local +cn: sar-user +objectClass: posixGroup +description: save-n-restore user +gidNumber: 27001 +uidNumber: 27001 +memberUid: user + +dn: cn=sar-admin,ou=Group,dc=sar,dc=local +cn: sar-admin +objectClass: posixGroup +description: save-n-restore admin +gidNumber: 27003 +uidNumber: 27003 +memberUid: admin + +dn: uid=user,ou=Group,dc=sar,dc=local +uid: user +objectClass: account +objectClass: posixAccount +description: User with sar-user role +cn: user +userPassword: userPass +uidNumber: 23004 +gidNumber: 23004 +homeDirectory: /dev/null + +dn: uid=johndoe,ou=Group,dc=sar,dc=local +uid: johndoe +objectClass: account +objectClass: posixAccount +description: User without sar roles +cn: johndoe +userPassword: 1234 +uidNumber: 23007 +gidNumber: 23007 +homeDirectory: /dev/null + +dn: uid=admin,ou=Group,dc=sar,dc=local +uid: admin +objectClass: account +objectClass: posixAccount +description: User with admin role +cn: admin +userPassword: adminPass +uidNumber: 23005 +gidNumber: 23005 +homeDirectory: /dev/null diff --git a/services/save-and-restore/src/main/resources/scripts/update-node-name-mapping.sh b/services/save-and-restore/src/main/resources/scripts/update-node-name-mapping.sh new file mode 100644 index 0000000000..3a17e7bf86 --- /dev/null +++ b/services/save-and-restore/src/main/resources/scripts/update-node-name-mapping.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +if [ $# -eq 0 ]; then + echo "Invalid usage. Please specify path to the file tree_node_mapping.json as first argument." + exit 1 +fi + +echo "Creating temporary index" +curl -XPUT "http://localhost:9200/saveandrestore_tree_tmp" -H 'Content-Type: application/json' --data @$1 +echo + +echo "Copying data to temporary index" +curl -XPOST "http://localhost:9200/_reindex" -H 'Content-Type: application/json' -d '{"source":{"index": "saveandrestore_tree"}, "dest":{"index":"saveandrestore_tree_tmp"}}' +echo + +echo "Flushing data to temporary index" +curl -XPOST "http://localhost:9200/saveandrestore_tree_tmp/_flush" +echo + +echo "Delete original index" +curl -XDELETE "http://localhost:9200/saveandrestore_tree" +echo + +echo "Re-creating original index" +curl -XPUT "http://localhost:9200/saveandrestore_tree" -H 'Content-Type: application/json' --data @$1 +echo + +echo "Copying data to original index" +curl -XPOST "http://localhost:9200/_reindex" -H 'Content-Type: application/json' -d '{"source":{"index": "saveandrestore_tree_tmp"}, "dest":{"index":"saveandrestore_tree"}}' +echo + +echo "Flushing data to original index" +curl -XPOST "http://localhost:9200/saveandrestore_tree/_flush" +echo + +echo "Delete temporary index" +curl -XDELETE "http://localhost:9200/saveandrestore_tree_tmp" +echo \ No newline at end of file diff --git a/services/save-and-restore/src/main/resources/snapshot_mapping.json b/services/save-and-restore/src/main/resources/snapshot_mapping.json index 6f76ed81a6..5788fba8ad 100644 --- a/services/save-and-restore/src/main/resources/snapshot_mapping.json +++ b/services/save-and-restore/src/main/resources/snapshot_mapping.json @@ -5,23 +5,7 @@ "type" : "keyword" }, "snapshotItems" : { - "type" : "nested", - "properties": { - "configPv": { - "properties": { - "pvName" : { - "type": "keyword" - }, - "readOnly": { - "type" : "boolean" - } - } - }, - "value": { - "enabled": false, - "type": "object" - } - } + "type" : "flattened" } } } diff --git a/services/save-and-restore/src/main/resources/static/SearchHelp_en.html b/services/save-and-restore/src/main/resources/static/SearchHelp_en.html index 049dda5727..7ad50e0074 100644 --- a/services/save-and-restore/src/main/resources/static/SearchHelp_en.html +++ b/services/save-and-restore/src/main/resources/static/SearchHelp_en.html @@ -66,6 +66,10 @@

      start and end

      name

      A search for nodes by name will match on the name as specified by the user when the node was created or updated. The value for this key is case insensitive. +

      + The space character along with other "special" characters is used by the underlying search engine to tokenize + text when it is indexed. Consequently, a search for a name that includes a space character must be quoted. +

      user

      This is the username associated with a node when it was saved. The value for this key is case insensitive. @@ -99,6 +103,15 @@

      tags

      tags=Beam* will work, while tags=Beam will not.

      +

      PV name(s)

      + This is used to perform a search for configuration nodes containing the specified PV name(s). The input + field is backed by the PV name completion mechanism used in other parts of the application, i.e. a list of + PV names - supplied by the Channel Finder service - is shown as user types in the input field. + +

      + Search on PV names will consider both setpoint PVs as well readback PVs in configurations. +

      +

      Combining keys

      If multiple keys are used in a search query, the service will consider all (valid) keys and return nodes matching all criteria. In other words, search criteria are and:ed. However, as mentioned above, diff --git a/services/save-and-restore/src/main/resources/tree_node_mapping.json b/services/save-and-restore/src/main/resources/tree_node_mapping.json index 8b6bcda4a8..84821e36a0 100644 --- a/services/save-and-restore/src/main/resources/tree_node_mapping.json +++ b/services/save-and-restore/src/main/resources/tree_node_mapping.json @@ -8,7 +8,12 @@ "type": "keyword" }, "name": { - "type": "text" + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + } }, "userName": { "type": "text" diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java index 1a7cc136d7..c16a335dcd 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/DAOTestIT.java @@ -22,62 +22,33 @@ import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest; import co.elastic.clients.elasticsearch.indices.ExistsRequest; import co.elastic.clients.transport.endpoints.BooleanResponse; -import org.epics.vtype.Alarm; -import org.epics.vtype.AlarmSeverity; -import org.epics.vtype.AlarmStatus; -import org.epics.vtype.Display; -import org.epics.vtype.Time; -import org.epics.vtype.VDouble; -import org.epics.vtype.VInt; - +import org.epics.vtype.*; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; - -import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; -import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; -import org.phoebus.applications.saveandrestore.model.ConfigPv; -import org.phoebus.applications.saveandrestore.model.Configuration; -import org.phoebus.applications.saveandrestore.model.ConfigurationData; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.NodeType; -import org.phoebus.applications.saveandrestore.model.Snapshot; -import org.phoebus.applications.saveandrestore.model.SnapshotData; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.applications.saveandrestore.model.Tag; -import org.phoebus.applications.saveandrestore.model.TagData; +import org.phoebus.applications.saveandrestore.model.*; import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.applications.saveandrestore.model.search.SearchResult; import org.phoebus.service.saveandrestore.NodeNotFoundException; import org.phoebus.service.saveandrestore.persistence.config.ElasticConfig; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ConfigurationDataRepository; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Profile; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import java.io.IOException; import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; /** * Integration test to be executed against a running Elasticsearch 8.x instance. @@ -272,10 +243,10 @@ public void testGetConfigForSnapshot() { .description("comment") .userName("user").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - Node newSnapshot = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); + Node newSnapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); config = nodeDAO.getParentNode(newSnapshot.getUniqueId()); @@ -291,7 +262,7 @@ public void testGetParentNodeForNonexistingNode() { } @Test - public void testDeleteSnapshotReferencedInCompositeSnapshot(){ + public void testDeleteSnapshotReferencedInCompositeSnapshot() { Node rootNode = nodeDAO.getRootNode(); Node topLevelFolderNode = @@ -305,12 +276,12 @@ public void testDeleteSnapshotReferencedInCompositeSnapshot(){ Node snapshotNode = Node.builder().nodeType(NodeType.SNAPSHOT).name("snapshot").build(); Snapshot snapshot = new Snapshot(); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv1").build()) + snapshotData.setSnapshotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv1").build()) .build())); snapshot.setSnapshotData(snapshotData); snapshot.setSnapshotNode(snapshotNode); - snapshot = nodeDAO.saveSnapshot(configNode.getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(configNode.getUniqueId(), snapshot); Node compositeSnapshotNode = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); @@ -331,7 +302,7 @@ public void testDeleteSnapshotReferencedInCompositeSnapshot(){ } @Test - public void testUpdateCompositeSnapshot(){ + public void testUpdateCompositeSnapshot() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); @@ -353,10 +324,10 @@ public void testUpdateCompositeSnapshot(){ Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(snapshotNode); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv1").build()).build())); + snapshotData.setSnapshotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv1").build()).build())); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); compositeSnapshot.setCompositeSnapshotNode(compositeSnapshotNode); @@ -388,10 +359,10 @@ public void testUpdateCompositeSnapshot(){ Snapshot snapshot2 = new Snapshot(); snapshot2.setSnapshotNode(snapshotNode2); SnapshotData snapshotData2 = new SnapshotData(); - snapshotData2.setSnasphotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv2").build()).build())); + snapshotData2.setSnapshotItems(List.of(SnapshotItem.builder().configPv(ConfigPv.builder().pvName("pv2").build()).build())); snapshot2.setSnapshotData(snapshotData2); - snapshot2 = nodeDAO.saveSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2); + snapshot2 = nodeDAO.createSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2); compositeSnapshotData = compositeSnapshot.getCompositeSnapshotData(); compositeSnapshotData.setReferencedSnapshotNodes(Arrays.asList(snapshot.getSnapshotNode().getUniqueId(), @@ -413,7 +384,7 @@ public void testUpdateCompositeSnapshot(){ } @Test - public void testGetAllCompositeSnapshotData(){ + public void testGetAllCompositeSnapshotData() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); @@ -427,7 +398,7 @@ public void testGetAllCompositeSnapshotData(){ List compositeSnapshotNodeIds = new ArrayList<>(); - for(int i = 0; i < 20; i++){ + for (int i = 0; i < 20; i++) { Node compositeSnapshotNode = Node.builder().name("My composite snapshot " + i).nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); @@ -439,15 +410,15 @@ public void testGetAllCompositeSnapshotData(){ Node snapshotNode = Node.builder().nodeType(NodeType.SNAPSHOT).name(i + "_").build(); SnapshotData snapshotData = new SnapshotData(); snapshotData.setUniqueId(snapshotNode.getUniqueId()); - snapshotData.setSnasphotItems(List.of( + snapshotData.setSnapshotItems(List.of( SnapshotItem.builder() .configPv(ConfigPv.builder().pvName("pvName" + i).build()) - .value(VInt.of(Integer.valueOf(i), Alarm.none(), Time.now(), Display.none())) + .value(VInt.of(i, Alarm.none(), Time.now(), Display.none())) .build())); Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(snapshotNode); snapshot.setSnapshotData(snapshotData); - nodeDAO.saveSnapshot(configNode.getUniqueId(), snapshot); + nodeDAO.createSnapshot(configNode.getUniqueId(), snapshot); compositeSnapshotData.setReferencedSnapshotNodes(List.of(snapshotNode.getUniqueId())); compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); @@ -497,15 +468,15 @@ public void testTakeSnapshot() { .description("comment") .build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(config.getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(config.getUniqueId(), snapshot); List snapshotItems = snapshot.getSnapshotData().getSnapshotItems(); assertEquals(1, snapshotItems.size()); - assertEquals(7.7, ((VDouble) snapshotItems.get(0).getValue()).getValue().doubleValue(), 0.01); - assertEquals(8.8, ((VDouble) snapshotItems.get(0).getReadbackValue()).getValue().doubleValue(), 0.01); + assertEquals(7.7, ((VDouble) snapshotItems.get(0).getValue()).getValue(), 0.01); + assertEquals(8.8, ((VDouble) snapshotItems.get(0).getReadbackValue()).getValue(), 0.01); List snapshots = nodeDAO.getSnapshots(config.getUniqueId()); assertEquals(1, snapshots.size()); @@ -518,6 +489,64 @@ public void testTakeSnapshot() { clearAllData(); } + @Test + public void testUpdateSnapshot() { + Node rootNode = nodeDAO.getRootNode(); + Node folderNode = + Node.builder().name("folder").build(); + + folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); + + Node config = Node.builder().name("My config 3").nodeType(NodeType.CONFIGURATION).build(); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(config); + ConfigurationData configurationData = new ConfigurationData(); + configurationData.setPvList(List.of(ConfigPv.builder() + .pvName("whatever").readbackPvName("readback_whatever").build())); + configuration.setConfigurationData(configurationData); + + configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); + + SnapshotItem item1 = SnapshotItem.builder().configPv(configuration.getConfigurationData().getPvList().get(0)) + .value(VDouble.of(7.7, alarm, time, display)).readbackValue(VDouble.of(8.8, alarm, time, display)) + .build(); + + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder() + .name("snapshot name") + .userName("user") + .description("comment") + .build()); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(item1)); + snapshot.setSnapshotData(snapshotData); + + snapshot = nodeDAO.createSnapshot(config.getUniqueId(), snapshot); + + List snapshotItems = snapshot.getSnapshotData().getSnapshotItems(); + assertEquals(1, snapshotItems.size()); + assertEquals(7.7, ((VDouble) snapshotItems.get(0).getValue()).getValue(), 0.01); + assertEquals(8.8, ((VDouble) snapshotItems.get(0).getReadbackValue()).getValue(), 0.01); + + List snapshots = nodeDAO.getSnapshots(config.getUniqueId()); + assertEquals(1, snapshots.size()); + + Node snapshotNode = snapshot.getSnapshotNode(); + snapshotNode.setName("other snapshot name"); + snapshotNode.setDescription("other comment"); + + snapshot.setSnapshotNode(snapshotNode); + + snapshot = nodeDAO.updateSnapshot(snapshot); + + snapshotNode = snapshot.getSnapshotNode(); + assertEquals("other snapshot name", snapshotNode.getName()); + assertEquals("other comment", snapshotNode.getDescription()); + + clearAllData(); + } + @Test public void testGetSnapshotsNoSnapshots() { assertThrows(NodeNotFoundException.class, @@ -553,12 +582,12 @@ public void testGetSnapshotItemsWithNullPvValues() { .description("comment") .build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(config.getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(config.getUniqueId(), snapshot); - assertEquals(7.7, ((VDouble) snapshot.getSnapshotData().getSnapshotItems().get(0).getValue()).getValue().doubleValue(), 0.01); + assertEquals(7.7, ((VDouble) snapshot.getSnapshotData().getSnapshotItems().get(0).getValue()).getValue(), 0.01); assertNull(snapshot.getSnapshotData().getSnapshotItems().get(0).getReadbackValue()); Snapshot snapshot1 = new Snapshot(); @@ -570,7 +599,7 @@ public void testGetSnapshotItemsWithNullPvValues() { SnapshotData snapshotData1 = new SnapshotData(); snapshot1.setSnapshotData(snapshotData1); - snapshot1 = nodeDAO.saveSnapshot(config.getUniqueId(), snapshot1); + snapshot1 = nodeDAO.createSnapshot(config.getUniqueId(), snapshot1); assertNull(snapshot1.getSnapshotData().getSnapshotItems()); @@ -592,7 +621,7 @@ public void testSnapshotTag() { snapshot.setSnapshotNode(Node.builder().name("testSnapshot").nodeType(NodeType.SNAPSHOT).build()); snapshot.setSnapshotData(new SnapshotData()); - Node snapshotNode = nodeDAO.saveSnapshot(configNode.getUniqueId(), snapshot).getSnapshotNode(); + Node snapshotNode = nodeDAO.createSnapshot(configNode.getUniqueId(), snapshot).getSnapshotNode(); Tag tag = Tag.builder().name("tag1").comment("comment1").userName("testUser1").build(); snapshotNode.addTag(tag); @@ -624,7 +653,7 @@ public void testSnapshotTag() { snapshot2.setSnapshotNode(Node.builder().name("testSnapshot2").nodeType(NodeType.SNAPSHOT).build()); snapshot2.setSnapshotData(new SnapshotData()); - Node snapshotNode2 = nodeDAO.saveSnapshot(configNode.getUniqueId(), snapshot2).getSnapshotNode(); + Node snapshotNode2 = nodeDAO.createSnapshot(configNode.getUniqueId(), snapshot2).getSnapshotNode(); Tag newTag = Tag.builder().name("newtag").comment("comment1").userName("testUser1").build(); @@ -636,19 +665,19 @@ public void testSnapshotTag() { assertEquals(2, updatedNodes.size()); Node n1 = updatedNodes.get(0); List tagList1 = n1.getTags(); - assertTrue(tagList1.stream().filter(t -> t.getName().equals(newTag.getName())).findFirst().isPresent()); + assertTrue(tagList1.stream().anyMatch(t -> t.getName().equals(newTag.getName()))); Node n2 = updatedNodes.get(1); List tagList2 = n2.getTags(); - assertTrue(tagList2.stream().filter(t -> t.getName().equals(newTag.getName())).findFirst().isPresent()); + assertTrue(tagList2.stream().anyMatch(t -> t.getName().equals(newTag.getName()))); updatedNodes = nodeDAO.deleteTag(tagData); assertEquals(2, updatedNodes.size()); n1 = updatedNodes.get(0); tagList1 = n1.getTags(); - assertFalse(tagList1.stream().filter(t -> t.getName().equals(newTag.getName())).findFirst().isPresent()); + assertFalse(tagList1.stream().anyMatch(t -> t.getName().equals(newTag.getName()))); n2 = updatedNodes.get(1); tagList2 = n2.getTags(); - assertFalse(tagList2.stream().filter(t -> t.getName().equals(newTag.getName())).findFirst().isPresent()); + assertFalse(tagList2.stream().anyMatch(t -> t.getName().equals(newTag.getName()))); clearAllData(); } @@ -728,23 +757,23 @@ public void testUpdateConfig() throws Exception { Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).name("name").userName("user").description("comment").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(item1, item2)); + snapshotData.setSnapshotItems(Arrays.asList(item1, item2)); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(config.getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(config.getUniqueId(), snapshot); // Save another snapshot with same data Snapshot snapshot1 = new Snapshot(); snapshot1.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).name("name1").userName("user").description("comment").build()); SnapshotData snapshotData1 = new SnapshotData(); - snapshotData1.setSnasphotItems(Arrays.asList(item1, item2)); + snapshotData1.setSnapshotItems(Arrays.asList(item1, item2)); snapshot1.setSnapshotData(snapshotData1); - nodeDAO.saveSnapshot(config.getUniqueId(), snapshot1); + nodeDAO.createSnapshot(config.getUniqueId(), snapshot1); List snapshotItems = snapshot.getSnapshotData().getSnapshotItems(); - assertEquals(7.7, ((VDouble) snapshotItems.get(0).getValue()).getValue().doubleValue(), 0.01); + assertEquals(7.7, ((VDouble) snapshotItems.get(0).getValue()).getValue(), 0.01); assertEquals(7, ((VInt) snapshotItems.get(1).getValue()).getValue().intValue()); List snapshots = nodeDAO.getSnapshots(config.getUniqueId()); @@ -1090,87 +1119,6 @@ public void testGetFullPath() { clearAllData(); } - @Test - public void testIsMoveAllowedRootNode() { - Node rootNode = nodeDAO.getRootNode(); - assertFalse(nodeDAO.isMoveOrCopyAllowed(List.of(rootNode), rootNode)); - } - - @Test - public void testIsMoveAllowedSnapshotNode() { - Node rootNode = nodeDAO.getRootNode(); - - Node node1 = new Node(); - node1.setName("SnapshotData node"); - node1.setNodeType(NodeType.SNAPSHOT); - node1.setUniqueId(UUID.randomUUID().toString()); - - Node node2 = new Node(); - node2.setName("Configuration node"); - node2.setNodeType(NodeType.CONFIGURATION); - node2.setUniqueId(UUID.randomUUID().toString()); - - assertFalse(nodeDAO.isMoveOrCopyAllowed(List.of(node1), rootNode)); - - assertFalse(nodeDAO.isMoveOrCopyAllowed(List.of(node2), rootNode)); - } - - @Test - public void testIsMoveAllowedSameType() { - Node rootNode = nodeDAO.getRootNode(); - - Node topLevelFolderNode = - nodeDAO.createNode(rootNode.getUniqueId(), Node.builder().name("top level folder").build()); - - Node node1 = new Node(); - node1.setName("SnapshotData node"); - node1.setNodeType(NodeType.CONFIGURATION); - node1 = nodeDAO.createNode(topLevelFolderNode.getUniqueId(), node1); - - Node node2 = new Node(); - node2.setName("Configuration node"); - node2.setNodeType(NodeType.FOLDER); - node2 = nodeDAO.createNode(topLevelFolderNode.getUniqueId(), node2); - - Node targetNode = new Node(); - targetNode.setUniqueId(Node.ROOT_FOLDER_UNIQUE_ID); - targetNode.setName("Target node"); - targetNode.setNodeType(NodeType.FOLDER); - - assertFalse(nodeDAO.isMoveOrCopyAllowed(Arrays.asList(node1, node2), targetNode)); - - clearAllData(); - } - - @Test - public void testIsMoveAllowedSameParentFolder() { - Node rootNode = nodeDAO.getRootNode(); - - Node node1 = new Node(); - node1.setName("SnapshotData node"); - node1.setNodeType(NodeType.FOLDER); - node1 = nodeDAO.createNode(rootNode.getUniqueId(), node1); - - Node folderNode = new Node(); - folderNode.setName("Folder node"); - folderNode.setNodeType(NodeType.FOLDER); - folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); - - Node node2 = new Node(); - node2.setName("Configuration node"); - node2.setNodeType(NodeType.CONFIGURATION); - node2 = nodeDAO.createNode(folderNode.getUniqueId(), node2); - - Node targetNode = new Node(); - targetNode.setName("Target node"); - targetNode.setNodeType(NodeType.FOLDER); - targetNode.setUniqueId(node1.getUniqueId()); - - assertFalse(nodeDAO.isMoveOrCopyAllowed(Arrays.asList(node1, node2), targetNode)); - - clearAllData(); - } - @Test public void testMoveNodesInvalidId() { Node rootNode = nodeDAO.getRootNode(); @@ -1194,115 +1142,67 @@ public void testMoveNodesInvalidId() { } @Test - public void testMoveNodesNameAndTypeClash() { - Node rootNode = nodeDAO.getRootNode(); - - Node node1 = new Node(); - node1.setName("Node1"); - node1.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(rootNode.getUniqueId(), node1); - - Node node2 = new Node(); - node2.setName("Node2"); - node2.setNodeType(NodeType.FOLDER); - node2 = nodeDAO.createNode(rootNode.getUniqueId(), node2); - - Node node3 = new Node(); - node3.setName("Node1"); - node3.setNodeType(NodeType.FOLDER); - node3 = nodeDAO.createNode(node2.getUniqueId(), node3); - - Node node4 = new Node(); - node4.setName("Node4"); - node4.setNodeType(NodeType.FOLDER); - node4 = nodeDAO.createNode(node2.getUniqueId(), node4); - - assertFalse(nodeDAO.isMoveOrCopyAllowed(Arrays.asList(node3, node4), rootNode)); - - clearAllData(); - + public void testMoveNodesToNonExistingTarget() { + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.moveNodes(Collections.emptyList(), "non existing", "user")); } @Test - public void testIsMoveAllowedTargetNotInSelectionTree() { + public void testMoveConfiguration() { Node rootNode = nodeDAO.getRootNode(); - Node firstLevelFolder1 = new Node(); - firstLevelFolder1.setName("First level folder 1"); - firstLevelFolder1.setNodeType(NodeType.FOLDER); - firstLevelFolder1 = nodeDAO.createNode(rootNode.getUniqueId(), firstLevelFolder1); + Node topLevelFolderNode = + nodeDAO.createNode(rootNode.getUniqueId(), Node.builder().name("top level folder").build()); - Node firstLevelFolder2 = new Node(); - firstLevelFolder2.setName("First Level folder 2"); - firstLevelFolder2.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(rootNode.getUniqueId(), firstLevelFolder2); + Node configNode = new Node(); + configNode.setName("Config"); + configNode.setNodeType(NodeType.CONFIGURATION); - Node secondLevelFolder1 = new Node(); - secondLevelFolder1.setName("Second level folder 1"); - secondLevelFolder1.setNodeType(NodeType.FOLDER); - secondLevelFolder1 = nodeDAO.createNode(firstLevelFolder1.getUniqueId(), secondLevelFolder1); + configNode = nodeDAO.createNode(topLevelFolderNode.getUniqueId(), configNode); - Node secondLevelFolder2 = new Node(); - secondLevelFolder2.setName("Second level folder 2"); - secondLevelFolder2.setNodeType(NodeType.FOLDER); - secondLevelFolder2 = nodeDAO.createNode(firstLevelFolder1.getUniqueId(), secondLevelFolder2); + Node topLevelFolderNode2 = + nodeDAO.createNode(rootNode.getUniqueId(), Node.builder().name("top level folder 2").build()); - assertTrue(nodeDAO.isMoveOrCopyAllowed(Arrays.asList(secondLevelFolder1, secondLevelFolder2), rootNode)); + topLevelFolderNode2 = nodeDAO.moveNodes(List.of(configNode.getUniqueId()), topLevelFolderNode2.getUniqueId(), "user"); - clearAllData(); + assertEquals(1, nodeDAO.getChildNodes(topLevelFolderNode2.getUniqueId()).size()); + assertEquals(0, nodeDAO.getChildNodes(topLevelFolderNode.getUniqueId()).size()); + clearAllData(); } @Test - public void testIsMoveAllowedTargetInSelectionTree() { + public void testMoveConfigurationNameClash() { Node rootNode = nodeDAO.getRootNode(); - Node firstLevelFolder1 = new Node(); - firstLevelFolder1.setName("First level folder 1"); - firstLevelFolder1.setNodeType(NodeType.FOLDER); - firstLevelFolder1 = nodeDAO.createNode(rootNode.getUniqueId(), firstLevelFolder1); - - Node firstLevelFolder2 = new Node(); - firstLevelFolder2.setName("First Level folder 2"); - firstLevelFolder2.setNodeType(NodeType.FOLDER); - firstLevelFolder2 = nodeDAO.createNode(rootNode.getUniqueId(), firstLevelFolder2); - - Node secondLevelFolder1 = new Node(); - secondLevelFolder1.setName("Second level folder 1"); - secondLevelFolder1.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(firstLevelFolder1.getUniqueId(), secondLevelFolder1); + Node topLevelFolderNode = + nodeDAO.createNode(rootNode.getUniqueId(), Node.builder().name("top level folder").build()); - Node secondLevelFolder2 = new Node(); - secondLevelFolder2.setName("Second level folder 2"); - secondLevelFolder2.setNodeType(NodeType.FOLDER); - secondLevelFolder2 = nodeDAO.createNode(firstLevelFolder1.getUniqueId(), secondLevelFolder2); + Node configNode = new Node(); + configNode.setName("Config"); + configNode.setNodeType(NodeType.CONFIGURATION); - assertFalse(nodeDAO.isMoveOrCopyAllowed(Arrays.asList(firstLevelFolder1, firstLevelFolder2), secondLevelFolder2)); + configNode = nodeDAO.createNode(topLevelFolderNode.getUniqueId(), configNode); - clearAllData(); - } + Node configNode2 = new Node(); + configNode2.setName("Config"); + configNode2.setNodeType(NodeType.CONFIGURATION); - @Test - public void testIsMoveAllowedMoveSaveSetToRoot() { - Node rootNode = nodeDAO.getRootNode(); + Node topLevelFolderNode2 = + nodeDAO.createNode(rootNode.getUniqueId(), Node.builder().name("top level folder 2").build()); - Node saveSetNode = new Node(); - saveSetNode.setNodeType(NodeType.CONFIGURATION); - saveSetNode.setUniqueId(UUID.randomUUID().toString()); - saveSetNode.setName("Configuration"); + nodeDAO.createNode(topLevelFolderNode2.getUniqueId(), configNode2); - assertFalse(nodeDAO.isMoveOrCopyAllowed(List.of(saveSetNode), rootNode)); + Node _configNode = configNode; - } + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.moveNodes(List.of(_configNode.getUniqueId()), topLevelFolderNode2.getUniqueId(), "user")); - @Test - public void testMoveNodesToNonExistingTarget() { - assertThrows(NodeNotFoundException.class, - () -> nodeDAO.moveNodes(Collections.emptyList(), "non existing", "user")); + clearAllData(); } @Test - public void testMoveNodesToNonFolder() { + public void testMoveSnasphotToRoot() { Node rootNode = nodeDAO.getRootNode(); Node topLevelFolderNode = @@ -1313,15 +1213,21 @@ public void testMoveNodesToNonFolder() { configNode.setNodeType(NodeType.CONFIGURATION); configNode = nodeDAO.createNode(topLevelFolderNode.getUniqueId(), configNode); - Node node = configNode; + Node snapshotNode = new Node(); + snapshotNode.setName("Snapshot"); + snapshotNode.setNodeType(NodeType.SNAPSHOT); + snapshotNode = nodeDAO.createNode(configNode.getUniqueId(), snapshotNode); + + String uniqueId = snapshotNode.getUniqueId(); assertThrows(IllegalArgumentException.class, - () -> nodeDAO.moveNodes(List.of("someId"), node.getUniqueId(), "user")); + () -> nodeDAO.moveNodes(List.of(uniqueId), rootNode.getUniqueId(), + "user")); clearAllData(); } @Test - public void testMoveSnasphot() { + public void testMoveSnasphotToConfiguration() { Node rootNode = nodeDAO.getRootNode(); Node topLevelFolderNode = @@ -1364,14 +1270,23 @@ public void testMoveNodes() { folderNode2.setNodeType(NodeType.FOLDER); folderNode2 = nodeDAO.createNode(folderNode.getUniqueId(), folderNode2); + Node folderNode3 = new Node(); + folderNode3.setName("Folder 3"); + folderNode3.setNodeType(NodeType.FOLDER); + // Create node, but do not include in move + nodeDAO.createNode(folderNode.getUniqueId(), folderNode3); + assertEquals(1, nodeDAO.getChildNodes(rootNode.getUniqueId()).size()); rootNode = nodeDAO.moveNodes(Arrays.asList(folderNode1.getUniqueId(), folderNode2.getUniqueId()), rootNode.getUniqueId(), "user"); + // Target node now has 3 child elements assertEquals(3, nodeDAO.getChildNodes(rootNode.getUniqueId()).size()); - clearAllData(); + // After move parent of source nodes should now have only one element + assertEquals(1, nodeDAO.getChildNodes(folderNode.getUniqueId()).size()); + clearAllData(); } @Test @@ -1410,7 +1325,7 @@ public void testCopyConfigToSameParent() { } @Test - public void testCopyFolderToOtherParent() { + public void testCopyFolderNotSupported() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = new Node(); @@ -1423,10 +1338,10 @@ public void testCopyFolderToOtherParent() { childFolderNode.setNodeType(NodeType.FOLDER); childFolderNode = nodeDAO.createNode(folderNode.getUniqueId(), childFolderNode); - nodeDAO.copyNodes(List.of(childFolderNode.getUniqueId()), rootNode.getUniqueId(), "username"); + Node _childFolderNode = childFolderNode; - List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId()); - assertEquals(2, childNodes.size()); + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.copyNodes(List.of(_childFolderNode.getUniqueId()), rootNode.getUniqueId(), "username")); clearAllData(); } @@ -1483,10 +1398,11 @@ public void testCopyMultipleFolders() { childFolder2.setNodeType(NodeType.FOLDER); childFolder2 = nodeDAO.createNode(folderNode.getUniqueId(), childFolder2); - nodeDAO.copyNodes(Arrays.asList(childFolder1.getUniqueId(), childFolder2.getUniqueId()), rootNode.getUniqueId(), "username"); + String f1 = childFolder1.getUniqueId(); + String f2 = childFolder2.getUniqueId(); - List childNodes = nodeDAO.getChildNodes(rootNode.getUniqueId()); - assertEquals(3, childNodes.size()); + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.copyNodes(Arrays.asList(f1, f2), rootNode.getUniqueId(), "username")); clearAllData(); } @@ -1520,31 +1436,21 @@ public void testCopyFolderAndConfig() { } @Test - public void testCopyFolderWithConfigAndSnapshot() { + public void testCopySnapshotToFolderNotSupported() { Node rootNode = nodeDAO.getRootNode(); + Node folderNode = nodeDAO.createNode(rootNode.getUniqueId(), + Node.builder().name("Folder").build()); - Node folderNode = new Node(); - folderNode.setName("Folder"); - folderNode.setNodeType(NodeType.FOLDER); - folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); - - Node childFolder1 = new Node(); - childFolder1.setName("Child Folder 1"); - childFolder1.setNodeType(NodeType.FOLDER); - childFolder1 = nodeDAO.createNode(folderNode.getUniqueId(), childFolder1); - - Node configNode = new Node(); - configNode.setName("Config"); - configNode.setNodeType(NodeType.CONFIGURATION); + Node config = Node.builder().name("My config 3").nodeType(NodeType.CONFIGURATION).build(); Configuration configuration = new Configuration(); - configuration.setConfigurationNode(configNode); + configuration.setConfigurationNode(config); ConfigurationData configurationData = new ConfigurationData(); configurationData.setPvList(List.of(ConfigPv.builder().pvName("whatever").build())); configuration.setConfigurationData(configurationData); - configuration = nodeDAO.createConfiguration(childFolder1.getUniqueId(), configuration); + configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); SnapshotItem item1 = SnapshotItem.builder().configPv(configuration.getConfigurationData().getPvList().get(0)) .value(VDouble.of(7.7, alarm, time, display)).readbackValue(VDouble.of(7.7, alarm, time, display)) @@ -1553,54 +1459,73 @@ public void testCopyFolderWithConfigAndSnapshot() { Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(Node.builder().name("snapshotName").description("comment").userName("userName").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - nodeDAO.saveSnapshot(configNode.getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); - Node parent = nodeDAO.copyNodes(List.of(childFolder1.getUniqueId()), rootNode.getUniqueId(), "username"); - List childNodes = nodeDAO.getChildNodes(parent.getUniqueId()); - assertEquals(2, childNodes.size()); + String snapshotId = snapshot.getSnapshotNode().getUniqueId(); + + Node folderNode1 = new Node(); + folderNode1.setName("Folder1"); + folderNode1.setNodeType(NodeType.FOLDER); + folderNode1 = nodeDAO.createNode(rootNode.getUniqueId(), folderNode1); - Node copiedFolder = childNodes.stream().filter(node -> node.getName().equals("Child Folder 1")).findFirst().get(); - Node copiedConfiguration = nodeDAO.getChildNodes(copiedFolder.getUniqueId()).get(0); - Node copiedSnapshot = nodeDAO.getSnapshots(copiedConfiguration.getUniqueId()).get(0); - assertEquals("snapshotName", copiedSnapshot.getName()); - assertEquals(1, nodeDAO.getConfigurationData(copiedConfiguration.getUniqueId()).getPvList().size()); + String uniqueId = folderNode1.getUniqueId(); + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.copyNodes(List.of(snapshotId), uniqueId, "username")); clearAllData(); - } @Test - public void testCopySubtree() { + public void testCopySnapshotToConfiguration() { + Node rootNode = nodeDAO.getRootNode(); + Node folderNode = nodeDAO.createNode(rootNode.getUniqueId(), + Node.builder().name("Folder").build()); - Node folderNode = new Node(); - folderNode.setName("Folder"); - folderNode.setNodeType(NodeType.FOLDER); - folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); + Node config = Node.builder().name("My config 3").nodeType(NodeType.CONFIGURATION).build(); - Node childFolder1 = new Node(); - childFolder1.setName("Child Folder 1"); - childFolder1.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(folderNode.getUniqueId(), childFolder1); + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(config); + ConfigurationData configurationData = new ConfigurationData(); + configurationData.setPvList(List.of(ConfigPv.builder().pvName("whatever").build())); + configuration.setConfigurationData(configurationData); - Node targetNode = new Node(); - targetNode.setName("Target Folder"); - targetNode.setNodeType(NodeType.FOLDER); - targetNode = nodeDAO.createNode(rootNode.getUniqueId(), targetNode); + configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); - nodeDAO.copyNodes(List.of(folderNode.getUniqueId()), targetNode.getUniqueId(), "username"); + Node config2 = Node.builder().name("My config 4").nodeType(NodeType.CONFIGURATION).build(); + Configuration configuration2 = new Configuration(); + configuration2.setConfigurationNode(config2); + configuration2.setConfigurationData(configurationData); + + configuration2 = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration2); + + SnapshotItem item1 = SnapshotItem.builder().configPv(configuration.getConfigurationData().getPvList().get(0)) + .value(VDouble.of(7.7, alarm, time, display)).readbackValue(VDouble.of(7.7, alarm, time, display)) + .build(); - assertEquals(2, nodeDAO.getChildNodes(rootNode.getUniqueId()).size()); - assertEquals(1, nodeDAO.getChildNodes(targetNode.getUniqueId()).size()); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder().name("snapshotName").description("comment").userName("userName").build()); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(item1)); + snapshot.setSnapshotData(snapshotData); + + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + + String snapshotId = snapshot.getSnapshotNode().getUniqueId(); + + Node updatedConfigNode = + nodeDAO.copyNodes(List.of(snapshotId), configuration2.getConfigurationNode().getUniqueId(), "useername"); + + assertEquals(1, nodeDAO.getChildNodes(updatedConfigNode.getUniqueId()).size()); clearAllData(); } @Test - public void testCopySnapshot() { + public void testCopySnapshotToConfigurationPvListMismatch() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = nodeDAO.createNode(rootNode.getUniqueId(), @@ -1616,6 +1541,15 @@ public void testCopySnapshot() { configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); + Node config2 = Node.builder().name("My config 4").nodeType(NodeType.CONFIGURATION).build(); + Configuration configuration2 = new Configuration(); + configuration2.setConfigurationNode(config2); + ConfigurationData configurationData2 = new ConfigurationData(); + configurationData2.setPvList(List.of(ConfigPv.builder().pvName("non-matching").build())); + configuration2.setConfigurationData(configurationData2); + + configuration2 = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration2); + SnapshotItem item1 = SnapshotItem.builder().configPv(configuration.getConfigurationData().getPvList().get(0)) .value(VDouble.of(7.7, alarm, time, display)).readbackValue(VDouble.of(7.7, alarm, time, display)) .build(); @@ -1623,36 +1557,89 @@ public void testCopySnapshot() { Snapshot snapshot = new Snapshot(); snapshot.setSnapshotNode(Node.builder().name("snapshotName").description("comment").userName("userName").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); String snapshotId = snapshot.getSnapshotNode().getUniqueId(); + String config2Id = configuration2.getConfigurationNode().getUniqueId(); - Node folderNode1 = new Node(); - folderNode1.setName("Folder1"); - folderNode1.setNodeType(NodeType.FOLDER); - folderNode1 = nodeDAO.createNode(rootNode.getUniqueId(), folderNode1); - - String uniqueId = folderNode1.getUniqueId(); assertThrows(IllegalArgumentException.class, - () -> nodeDAO.copyNodes(List.of(snapshotId), uniqueId, "username")); + () -> nodeDAO.copyNodes(List.of(snapshotId), config2Id, "userName")); clearAllData(); } @Test - public void testCopyConfigWithSnapshots() { + public void testCopyCompositeSnapshot() { Node rootNode = nodeDAO.getRootNode(); + Node folderNode = nodeDAO.createNode(rootNode.getUniqueId(), + Node.builder().name("Folder").build()); + + Node config = Node.builder().name("My config 3").nodeType(NodeType.CONFIGURATION).build(); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(config); + ConfigurationData configurationData = new ConfigurationData(); + configurationData.setPvList(List.of(ConfigPv.builder().pvName("whatever").build())); + configuration.setConfigurationData(configurationData); + + configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); + SnapshotItem item1 = SnapshotItem.builder().configPv(configuration.getConfigurationData().getPvList().get(0)) + .value(VDouble.of(7.7, alarm, time, display)).readbackValue(VDouble.of(7.7, alarm, time, display)) + .build(); + + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder().name("snapshotName").description("comment").userName("userName").build()); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(List.of(item1)); + snapshot.setSnapshotData(snapshotData); + + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + + + Node folderNode1 = nodeDAO.createNode(rootNode.getUniqueId(), + Node.builder().name("Folder 1").build()); + + Node node = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); + + CompositeSnapshotData compositeSnapshotData = new CompositeSnapshotData(); + compositeSnapshotData.setReferencedSnapshotNodes(List.of(snapshot.getSnapshotNode().getUniqueId())); + + CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); + compositeSnapshot.setCompositeSnapshotNode(node); + compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); + + compositeSnapshot = nodeDAO.createCompositeSnapshot(folderNode.getUniqueId(), compositeSnapshot); + + String compositeSnapshotId = compositeSnapshot.getCompositeSnapshotNode().getUniqueId(); + + folderNode1 = nodeDAO.copyNodes(List.of(compositeSnapshotId), folderNode1.getUniqueId(), "user"); + + List childNodes = nodeDAO.getChildNodes(folderNode1.getUniqueId()); + + assertEquals(1, childNodes.size()); + // Make sure referenced nodes have been copied to copied composite snapshot + assertEquals(1, nodeDAO.getCompositeSnapshotData(childNodes.get(0).getUniqueId()).getReferencedSnapshotNodes().size()); + + nodeDAO.deleteNode(childNodes.get(0).getUniqueId()); + nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); + + clearAllData(); + } + + @Test + public void testCopyCompositeSnapshotToConfiguration() { + + Node rootNode = nodeDAO.getRootNode(); Node folderNode = nodeDAO.createNode(rootNode.getUniqueId(), - Node.builder() - .name("Folder") - .build()); + Node.builder().name("Folder").build()); Node config = Node.builder().name("My config 3").nodeType(NodeType.CONFIGURATION).build(); + Configuration configuration = new Configuration(); configuration.setConfigurationNode(config); ConfigurationData configurationData = new ConfigurationData(); @@ -1666,42 +1653,45 @@ public void testCopyConfigWithSnapshots() { .build(); Snapshot snapshot = new Snapshot(); - snapshot.setSnapshotNode(Node.builder() - .nodeType(NodeType.SNAPSHOT) - .name("snapshotName") - .userName("userName") - .description("comment") - .build()); + snapshot.setSnapshotNode(Node.builder().name("snapshotName").description("comment").userName("userName").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - snapshot = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + snapshot = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); - Node snapshotNode = snapshot.getSnapshotNode(); + Node config2 = Node.builder().name("My config 4").nodeType(NodeType.CONFIGURATION).build(); - Tag tag = new Tag(); - tag.setUserName("userName"); - tag.setName("tagname"); - tag.setCreated(new Date()); - tag.setComment("tagcomment"); - snapshotNode.addTag(tag); - nodeDAO.updateNode(snapshotNode, false); + Configuration configuration2 = new Configuration(); + configuration2.setConfigurationNode(config2); + ConfigurationData configurationData2 = new ConfigurationData(); + configurationData2.setPvList(List.of(ConfigPv.builder().pvName("whatever").build())); + configuration2.setConfigurationData(configurationData2); - Node folderNode2 = new Node(); - folderNode2.setName("Folder2"); - folderNode2 = nodeDAO.createNode(rootNode.getUniqueId(), folderNode2); + configuration2 = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration2); + + Node folderNode1 = nodeDAO.createNode(rootNode.getUniqueId(), + Node.builder().name("Folder 1").build()); + + Node node = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); + + CompositeSnapshotData compositeSnapshotData = new CompositeSnapshotData(); + compositeSnapshotData.setReferencedSnapshotNodes(List.of(snapshot.getSnapshotNode().getUniqueId())); + + CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); + compositeSnapshot.setCompositeSnapshotNode(node); + compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); + + compositeSnapshot = nodeDAO.createCompositeSnapshot(folderNode.getUniqueId(), compositeSnapshot); - nodeDAO.copyNodes(List.of(configuration.getConfigurationNode().getUniqueId()), folderNode2.getUniqueId(), "userName"); + String compositeSnapshotId = compositeSnapshot.getCompositeSnapshotNode().getUniqueId(); - Node copiedConfig = nodeDAO.getChildNodes(folderNode.getUniqueId()).get(0); - Node copiedSnapshot = nodeDAO.getChildNodes(copiedConfig.getUniqueId()).get(0); - assertEquals("snapshotName", copiedSnapshot.getName()); - assertEquals("userName", copiedSnapshot.getUserName()); - List tags = copiedSnapshot.getTags(); - assertEquals("userName", tags.get(0).getUserName()); - assertEquals("tagname", tags.get(0).getName()); - assertEquals("tagcomment", tags.get(0).getComment()); + String config2Id = configuration2.getConfigurationNode().getUniqueId(); + + assertThrows(IllegalArgumentException.class, + () -> nodeDAO.copyNodes(List.of(compositeSnapshotId), config2Id, "user")); + + nodeDAO.deleteNode(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); clearAllData(); } @@ -1743,44 +1733,6 @@ public void testDeleteTree() { assertTrue(nodeDAO.getChildNodes(rootNode.getUniqueId()).isEmpty()); } - /** - * This method should verify that the target {@link Node} is not - * found in any of the source {@link Node}s subtrees. - */ - @Test - public void testContainedInSubTree() { - Node rootNode = nodeDAO.getRootNode(); - - Node L1F1 = new Node(); - L1F1.setName("L1F1"); - L1F1.setNodeType(NodeType.FOLDER); - L1F1 = nodeDAO.createNode(rootNode.getUniqueId(), L1F1); - - Node L1F2 = new Node(); - L1F2.setName("L1F2"); - L1F2.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(rootNode.getUniqueId(), L1F2); - - Node L2F1 = new Node(); - L2F1.setName("L2F1"); - L2F1.setNodeType(NodeType.FOLDER); - nodeDAO.createNode(L1F1.getUniqueId(), L2F1); - - Node L2F2 = new Node(); - L2F2.setName("L2F2"); - L2F2.setNodeType(NodeType.FOLDER); - L2F2 = nodeDAO.createNode(L1F1.getUniqueId(), L2F2); - - // OK to copy/move level 2 folders to root - assertTrue(nodeDAO.isMoveOrCopyAllowed(Collections.singletonList(L2F2), rootNode)); - - // NOT OK to copy/move level 1 folders to root as they are already there - assertFalse(nodeDAO.isMoveOrCopyAllowed(List.of(L1F1), rootNode)); - - clearAllData(); - - } - @Test public void testIsContainedInSubTree() { Node rootNode = nodeDAO.getRootNode(); @@ -1894,10 +1846,10 @@ public void testGetAllSnapshots() { .description("comment") .build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(List.of(item1)); + snapshotData.setSnapshotItems(List.of(item1)); snapshot.setSnapshotData(snapshotData); - nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); + nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot); List snapshotNodes = nodeDAO.getAllSnapshots(); assertEquals(1, snapshotNodes.size()); @@ -1906,7 +1858,7 @@ public void testGetAllSnapshots() { } @Test - public void testGetAllNodes(){ + public void testGetAllNodes() { Node rootNode = nodeDAO.getRootNode(); Node folderNode1 = nodeDAO.createNode(rootNode.getUniqueId(), Node.builder() @@ -1925,12 +1877,13 @@ public void testGetAllNodes(){ } /** - * Deletes all child nodes of the root node, i.e. all data except root node. + * Deletes all objects in all indices. */ private void clearAllData() { List childNodes = nodeDAO.getChildNodes(Node.ROOT_FOLDER_UNIQUE_ID); childNodes.forEach(node -> nodeDAO.deleteNode(node.getUniqueId())); nodeDAO.deleteAllFilters(); + } @@ -1960,13 +1913,13 @@ public void dropIndices() { } @Test - public void testCheckForPVNameDuplicates(){ + public void testCheckForPVNameDuplicates() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); - /************ Create snapshot1 ************/ + //************ Create snapshot1 ************/ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 1").build(); Configuration configuration = new Configuration(); configuration.setConfigurationNode(config); @@ -1987,12 +1940,12 @@ public void testCheckForPVNameDuplicates(){ .description("comment") .userName("user").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(item1, item2)); + snapshotData.setSnapshotItems(Arrays.asList(item1, item2)); snapshot.setSnapshotData(snapshotData); - Node newSnapshot1 = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); - /************ End create snapshot1 ************/ + Node newSnapshot1 = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); + //************ End create snapshot1 ************/ - /************ Create snapshot2 ************/ + //************ Create snapshot2 ************/ Node config2 = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 2").build(); Configuration configuration2 = new Configuration(); configuration2.setConfigurationNode(config2); @@ -2013,23 +1966,23 @@ public void testCheckForPVNameDuplicates(){ .description("comment") .userName("user").build()); SnapshotData snapshotData2 = new SnapshotData(); - snapshotData2.setSnasphotItems(Arrays.asList(item12, item22)); + snapshotData2.setSnapshotItems(Arrays.asList(item12, item22)); snapshot2.setSnapshotData(snapshotData2); - Node newSnapshot2 = nodeDAO.saveSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); - /************ End create snapshot2 ************/ + Node newSnapshot2 = nodeDAO.createSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); + //************ End create snapshot2 ************/ List duplicates = nodeDAO.checkForPVNameDuplicates(Arrays.asList(snapshot.getSnapshotNode().getUniqueId(), - snapshot2.getSnapshotNode().getUniqueId())); + snapshot2.getSnapshotNode().getUniqueId())); assertEquals(1, duplicates.size()); assertEquals("pv1", duplicates.get(0)); - /************ Create snapshot3 ************/ + //************ Create snapshot3 ************/ Node config3 = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 3").build(); Configuration configuration3 = new Configuration(); configuration3.setConfigurationNode(config3); ConfigurationData configurationData3 = new ConfigurationData(); - configurationData3.setPvList(Arrays.asList(ConfigPv.builder().pvName("pv4").build())); + configurationData3.setPvList(Collections.singletonList(ConfigPv.builder().pvName("pv4").build())); configuration3.setConfigurationData(configurationData3); configuration3 = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration3); @@ -2042,10 +1995,10 @@ public void testCheckForPVNameDuplicates(){ .description("comment") .userName("user").build()); SnapshotData snapshotData3 = new SnapshotData(); - snapshotData3.setSnasphotItems(Arrays.asList(item13)); + snapshotData3.setSnapshotItems(Collections.singletonList(item13)); snapshot3.setSnapshotData(snapshotData3); - Node newSnapshot3 = nodeDAO.saveSnapshot(configuration3.getConfigurationNode().getUniqueId(), snapshot3).getSnapshotNode(); - /************ End create snapshot3 ************/ + Node newSnapshot3 = nodeDAO.createSnapshot(configuration3.getConfigurationNode().getUniqueId(), snapshot3).getSnapshotNode(); + //************ End create snapshot3 ************/ duplicates = nodeDAO.checkForPVNameDuplicates(Arrays.asList(snapshot.getSnapshotNode().getUniqueId(), snapshot2.getSnapshotNode().getUniqueId(), @@ -2054,7 +2007,7 @@ public void testCheckForPVNameDuplicates(){ assertEquals(1, duplicates.size()); assertEquals("pv1", duplicates.get(0)); - /************ Create composite snapshot ************/ + //************ Create composite snapshot ************/ Node compositeSnapshotNode = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); @@ -2068,7 +2021,7 @@ public void testCheckForPVNameDuplicates(){ compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); compositeSnapshot = nodeDAO.createCompositeSnapshot(folderNode.getUniqueId(), compositeSnapshot); - /************ End create composite snapshot ************/ + //************ End create composite snapshot ************/ duplicates = nodeDAO.checkForPVNameDuplicates(Arrays.asList(snapshot.getSnapshotNode().getUniqueId(), compositeSnapshot.getCompositeSnapshotNode().getUniqueId())); @@ -2082,13 +2035,13 @@ public void testCheckForPVNameDuplicates(){ } @Test - public void testCheckForRejectedReferencedNodesInCompositeSnapshot(){ + public void testCheckForRejectedReferencedNodesInCompositeSnapshot() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); - /************ Create snapshot1 ************/ + //************ Create snapshot1 ************/ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 1").build(); Configuration configuration = new Configuration(); configuration.setConfigurationNode(config); @@ -2109,12 +2062,12 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot(){ .description("comment") .userName("user").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(item1, item2)); + snapshotData.setSnapshotItems(Arrays.asList(item1, item2)); snapshot.setSnapshotData(snapshotData); - Node newSnapshot1 = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); - /************ End create snapshot1 ************/ + Node newSnapshot1 = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); + //************ End create snapshot1 ************/ - /************ Create snapshot2 ************/ + //************ Create snapshot2 ************/ Node config2 = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 2").build(); Configuration configuration2 = new Configuration(); configuration2.setConfigurationNode(config2); @@ -2135,13 +2088,13 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot(){ .description("comment") .userName("user").build()); SnapshotData snapshotData2 = new SnapshotData(); - snapshotData2.setSnasphotItems(Arrays.asList(item12, item22)); + snapshotData2.setSnapshotItems(Arrays.asList(item12, item22)); snapshot2.setSnapshotData(snapshotData2); - Node newSnapshot2 = nodeDAO.saveSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); - /************ End create snapshot2 ************/ + Node newSnapshot2 = nodeDAO.createSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); + //************ End create snapshot2 ************/ - /************ Create composite snapshot ************/ + //************ Create composite snapshot ************/ Node compositeSnapshotNode = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); @@ -2154,7 +2107,7 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot(){ snapshot2.getSnapshotNode().getUniqueId())); compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); - /************ End create composite snapshot ************/ + //************ End create composite snapshot ************/ assertTrue(nodeDAO.checkCompositeSnapshotReferencedNodeTypes(compositeSnapshot)); @@ -2168,13 +2121,13 @@ public void testCheckForRejectedReferencedNodesInCompositeSnapshot(){ } @Test - public void testGetSnapshotItemsFromCompositeSnapshot(){ + public void testGetSnapshotItemsFromCompositeSnapshot() { Node rootNode = nodeDAO.getRootNode(); Node folderNode = Node.builder().name("folder").build(); folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); - /************ Create snapshot1 ************/ + //************ Create snapshot1 ************/ Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 1").build(); Configuration configuration = new Configuration(); configuration.setConfigurationNode(config); @@ -2195,12 +2148,12 @@ public void testGetSnapshotItemsFromCompositeSnapshot(){ .description("comment") .userName("user").build()); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(item1, item2)); + snapshotData.setSnapshotItems(Arrays.asList(item1, item2)); snapshot.setSnapshotData(snapshotData); - Node newSnapshot1 = nodeDAO.saveSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); - /************ End create snapshot1 ************/ + Node newSnapshot1 = nodeDAO.createSnapshot(configuration.getConfigurationNode().getUniqueId(), snapshot).getSnapshotNode(); + //************ End create snapshot1 ************/ - /************ Create snapshot2 ************/ + //************ Create snapshot2 ************/ Node config2 = Node.builder().nodeType(NodeType.CONFIGURATION).name("My config 2").build(); Configuration configuration2 = new Configuration(); configuration2.setConfigurationNode(config2); @@ -2221,12 +2174,12 @@ public void testGetSnapshotItemsFromCompositeSnapshot(){ .description("comment") .userName("user").build()); SnapshotData snapshotData2 = new SnapshotData(); - snapshotData2.setSnasphotItems(Arrays.asList(item12, item22)); + snapshotData2.setSnapshotItems(Arrays.asList(item12, item22)); snapshot2.setSnapshotData(snapshotData2); - Node newSnapshot2 = nodeDAO.saveSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); - /************ End create snapshot2 ************/ + Node newSnapshot2 = nodeDAO.createSnapshot(configuration2.getConfigurationNode().getUniqueId(), snapshot2).getSnapshotNode(); + //************ End create snapshot2 ************/ - /************ Create composite snapshot ************/ + //************ Create composite snapshot ************/ Node compositeSnapshotNode = Node.builder().name("My composite snapshot").nodeType(NodeType.COMPOSITE_SNAPSHOT).build(); CompositeSnapshot compositeSnapshot = new CompositeSnapshot(); @@ -2240,7 +2193,7 @@ public void testGetSnapshotItemsFromCompositeSnapshot(){ compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); compositeSnapshot = nodeDAO.createCompositeSnapshot(folderNode.getUniqueId(), compositeSnapshot); - /************ End create composite snapshot ************/ + //************ End create composite snapshot ************/ List snapshotItems = nodeDAO.getSnapshotItemsFromCompositeSnapshot(compositeSnapshot.getCompositeSnapshotNode().getUniqueId()); @@ -2252,7 +2205,7 @@ public void testGetSnapshotItemsFromCompositeSnapshot(){ } @Test - public void testFilters(){ + public void testFilters() { Filter filter = new Filter(); filter.setName("name"); filter.setQueryString("name=aName"); @@ -2304,4 +2257,67 @@ public void testFilters(){ clearAllData(); } + + @Test + public void testSearchForPvs() { + Node rootNode = nodeDAO.getRootNode(); + Node folderNode = + Node.builder().name("folder").build(); + folderNode = nodeDAO.createNode(rootNode.getUniqueId(), folderNode); + + Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("Myconfig1").build(); + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(config); + ConfigurationData configurationData = new ConfigurationData(); + configurationData.setPvList(Arrays.asList(ConfigPv.builder().pvName("pv1").build(), + ConfigPv.builder().pvName("pv2").build())); + configuration.setConfigurationData(configurationData); + + configuration = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration); + + Node config2 = Node.builder().nodeType(NodeType.CONFIGURATION).name("Myconfig2").build(); + Configuration configuration2 = new Configuration(); + configuration2.setConfigurationNode(config2); + ConfigurationData configurationData2 = new ConfigurationData(); + configurationData2.setPvList(Arrays.asList(ConfigPv.builder().pvName("pv12").build(), + ConfigPv.builder().pvName("pv22").readbackPvName("readbackpv22").build())); + configuration2.setConfigurationData(configurationData2); + + configuration2 = nodeDAO.createConfiguration(folderNode.getUniqueId(), configuration2); + + MultiValueMap searchParameters = new LinkedMultiValueMap<>(); + + searchParameters.put("pvs", List.of("pv1", "pv22", "pv12")); + + SearchResult searchResult = nodeDAO.search(searchParameters); + assertEquals(2, searchResult.getHitCount()); + assertEquals(configuration.getConfigurationNode().getUniqueId(), searchResult.getNodes().get(0).getUniqueId()); + assertEquals(configuration2.getConfigurationNode().getUniqueId(), searchResult.getNodes().get(1).getUniqueId()); + + searchParameters.put("name", List.of("Myconfig2")); + searchResult = nodeDAO.search(searchParameters); + assertEquals(1, searchResult.getHitCount()); + + searchParameters.clear(); + + searchParameters.put("pvs", List.of("pv1", "pv2")); + searchResult = nodeDAO.search(searchParameters); + assertEquals(1, searchResult.getHitCount()); + + searchParameters.put("pvs", List.of("readbackpv22")); + searchResult = nodeDAO.search(searchParameters); + assertEquals(1, searchResult.getHitCount()); + + searchParameters.put("pvs", List.of("invalid")); + searchResult = nodeDAO.search(searchParameters); + assertEquals(0, searchResult.getHitCount()); + + searchParameters.clear(); + searchResult = nodeDAO.search(searchParameters); + // No pvs specified -> find all nodes. + assertEquals(4, searchResult.getHitCount()); + + clearAllData(); + + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/DAOTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/DAOTest.java index 07ec4a3c76..47c92f0b42 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/DAOTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/DAOTest.java @@ -24,12 +24,12 @@ import java.util.Arrays; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; public class DAOTest { @Test - public void testExtractDuplicatePVNames(){ + public void testExtractDuplicatePVNames() { List allPVNames = Arrays.asList("a", "b", "c", "d", "D", "a", "B", "a", "b"); List duplicates = new ElasticsearchDAO().extractDuplicates(allPVNames); assertEquals(2, duplicates.size()); diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAOTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAOTest.java index 9145317bd7..606c9d4884 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAOTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/persistence/dao/impl/elasticsearch/ElasticsearchDAOTest.java @@ -19,42 +19,55 @@ package org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch; -import org.elasticsearch.common.recycler.Recycler.C; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.phoebus.applications.saveandrestore.model.ConfigPv; -import org.phoebus.applications.saveandrestore.model.Configuration; -import org.phoebus.applications.saveandrestore.model.ConfigurationData; -import org.phoebus.applications.saveandrestore.model.Node; -import org.phoebus.applications.saveandrestore.model.NodeType; -import org.phoebus.applications.saveandrestore.model.SnapshotData; -import org.phoebus.applications.saveandrestore.model.SnapshotItem; -import org.phoebus.service.saveandrestore.persistence.config.ElasticConfig; +import org.phoebus.applications.saveandrestore.model.*; +import org.phoebus.service.saveandrestore.model.ESTreeNode; +import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.ElasticsearchDAO.NodeNameComparator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Profile; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import static org.junit.jupiter.api.Assertions.*; - import java.util.Arrays; +import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @EnableConfigurationProperties @ContextHierarchy({@ContextConfiguration(classes = {ElasticTestConfig.class})}) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class}) +@TestPropertySource(locations = "classpath:test_application.properties") @Profile("IT") public class ElasticsearchDAOTest { @Autowired private ElasticsearchDAO elasticsearchDAO; + @Autowired + private ConfigurationDataRepository configurationDataRepository; + + @SuppressWarnings("unused") + @Autowired + private SnapshotDataRepository snapshotDataRepository; + + @Autowired + private ElasticsearchTreeRepository elasticsearchTreeRepository; + + @Autowired + + private static Node node1; private static Node configNode1; @@ -63,7 +76,7 @@ public class ElasticsearchDAOTest { private static Node folderNode1; @BeforeAll - public static void init(){ + public static void init() { node1 = Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId(UUID.randomUUID().toString()).name("node1").build(); configNode1 = Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId(UUID.randomUUID().toString()).name("node1").build(); configNode2 = Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId(UUID.randomUUID().toString()).name("configNode2").build(); @@ -71,25 +84,25 @@ public static void init(){ } @Test - public void testIsNodeNameValid(){ + public void testIsNodeNameValid() { Node newNode = Node.builder().name("node7").uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.CONFIGURATION).build(); assertTrue(elasticsearchDAO.isNodeNameValid(newNode, Arrays.asList(node1, configNode2, folderNode1))); } @Test - public void testIsNodeNameInvalid(){ + public void testIsNodeNameInvalid() { Node newNode = Node.builder().name("node1").uniqueId(UUID.randomUUID().toString()).nodeType(NodeType.CONFIGURATION).build(); assertFalse(elasticsearchDAO.isNodeNameValid(newNode, Arrays.asList(node1, configNode2, folderNode1))); } @Test - public void testNodeNameAndUniqueIdEqual(){ + public void testNodeNameAndUniqueIdEqual() { Node newNode = Node.builder().name("node1").uniqueId(node1.getUniqueId()).nodeType(NodeType.CONFIGURATION).build(); assertFalse(elasticsearchDAO.isNodeNameValid(newNode, Arrays.asList(node1, configNode2, folderNode1))); } @Test - public void testNodeNameAndUniqueIdEqual2(){ + public void testNodeNameAndUniqueIdEqual2() { Node newNode = Node.builder().name("node1").uniqueId(node1.getUniqueId()).nodeType(NodeType.CONFIGURATION).build(); assertFalse(elasticsearchDAO.isNodeNameValid(newNode, Arrays.asList(node1, configNode2, folderNode1, configNode1))); } @@ -101,7 +114,7 @@ public void testNodeNameAndUniqueIdNull() { } @Test - public void testRemoveDuplicateConfigPvs(){ + public void testRemoveDuplicateConfigPvs() { ConfigPv configPv1 = ConfigPv.builder().pvName("a").build(); ConfigPv configPv2 = ConfigPv.builder().pvName("a").build(); ConfigPv configPv3 = ConfigPv.builder().pvName("b").build(); @@ -127,14 +140,14 @@ public void testRemoveDuplicateConfigPvs(){ } @Test - public void testRemoveDuplicateConfigSnapshotItems(){ + public void testRemoveDuplicateConfigSnapshotItems() { ConfigPv configPv1 = ConfigPv.builder().pvName("a").build(); ConfigPv configPv2 = ConfigPv.builder().pvName("a").build(); ConfigPv configPv3 = ConfigPv.builder().pvName("b").build(); ConfigPv configPv4 = ConfigPv.builder().pvName("c").build(); SnapshotData snapshotData = new SnapshotData(); - snapshotData.setSnasphotItems(Arrays.asList(SnapshotItem.builder().configPv(configPv1).build(), + snapshotData.setSnapshotItems(Arrays.asList(SnapshotItem.builder().configPv(configPv1).build(), SnapshotItem.builder().configPv(configPv2).build(), SnapshotItem.builder().configPv(configPv3).build())); @@ -144,7 +157,7 @@ public void testRemoveDuplicateConfigSnapshotItems(){ assertEquals("a", snapshotData.getSnapshotItems().get(0).getConfigPv().getPvName()); assertEquals("b", snapshotData.getSnapshotItems().get(1).getConfigPv().getPvName()); - snapshotData.setSnasphotItems(Arrays.asList(SnapshotItem.builder().configPv(configPv1).build(), + snapshotData.setSnapshotItems(Arrays.asList(SnapshotItem.builder().configPv(configPv1).build(), SnapshotItem.builder().configPv(configPv3).build(), SnapshotItem.builder().configPv(configPv4).build())); @@ -155,4 +168,197 @@ public void testRemoveDuplicateConfigSnapshotItems(){ //This should not throw a NPE assertNull(elasticsearchDAO.removeDuplicateSnapshotItems(null)); } + + @Test + public void testMayCopySnapshot() { + ConfigPv configPva = ConfigPv.builder().pvName("a").build(); + ConfigPv configPvb = ConfigPv.builder().pvName("b").build(); + ConfigPv configPvc = ConfigPv.builder().pvName("c").build(); + ConfigPv configPvd = ConfigPv.builder().pvName("d").build(); + ConfigPv configPve = ConfigPv.builder().pvName("e").build(); + + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setSnapshotItems(Arrays.asList(SnapshotItem.builder().configPv(configPva).build(), + SnapshotItem.builder().configPv(configPvb).build(), + SnapshotItem.builder().configPv(configPvc).build(), + SnapshotItem.builder().configPv(configPvd).build(), + SnapshotItem.builder().configPv(configPve).build())); + + when(snapshotDataRepository.findById("snapshot")).thenReturn(Optional.of(snapshotData)); + + ConfigurationData configurationData = new ConfigurationData(); + configurationData.setPvList(Arrays.asList(configPva, configPvb, configPvc, configPvd, configPve)); + + when(configurationDataRepository.findById("configuration")).thenReturn(Optional.of(configurationData)); + + Node snapshotNode = Node.builder().nodeType(NodeType.SNAPSHOT).uniqueId("snapshot").build(); + Node configurationNode = Node.builder().nodeType(NodeType.CONFIGURATION).uniqueId("configuration").build(); + + assertTrue(elasticsearchDAO.mayMoveOrCopySnapshot(snapshotNode, configurationNode)); + + configurationData.setPvList(Arrays.asList(configPva, configPvb, configPvc, configPvd)); + + assertFalse(elasticsearchDAO.mayMoveOrCopySnapshot(snapshotNode, configurationNode)); + + configurationData.setPvList(Arrays.asList(configPva, configPvb, configPvc, configPvd, configPve)); + + snapshotData.setSnapshotItems(Arrays.asList(SnapshotItem.builder().configPv(configPva).build(), + SnapshotItem.builder().configPv(configPvb).build(), + SnapshotItem.builder().configPv(configPvc).build(), + SnapshotItem.builder().configPv(configPvd).build())); + + assertFalse(elasticsearchDAO.mayMoveOrCopySnapshot(snapshotNode, configurationNode)); + + snapshotData.setSnapshotItems(Arrays.asList(SnapshotItem.builder().configPv(configPva).build(), + SnapshotItem.builder().configPv(configPvb).build(), + SnapshotItem.builder().configPv(configPvc).build(), + SnapshotItem.builder().configPv(configPvd).build(), + SnapshotItem.builder().configPv(configPve).build())); + + assertTrue(elasticsearchDAO.mayMoveOrCopySnapshot(snapshotNode, configurationNode)); + } + + @Test + public void testDetermineNewNodeName() { + Node n1 = Node.builder().uniqueId("abc").name("abc").build(); + Node n2 = Node.builder().uniqueId("def").name("def").build(); + Node n3 = Node.builder().uniqueId("ABC copy").name("ABC copy").build(); + Node n4 = Node.builder().uniqueId("DEF copy 2").name("DEF copy 2").build(); + Node n5 = Node.builder().uniqueId("DEF copy 777").name("DEF copy 777").build(); + Node n6 = Node.builder().uniqueId("XYZ copy 1").name("XYZ copy 1").build(); + Node n7 = Node.builder().uniqueId("XYZ copy abc").name("XYZ copy abc").build(); + + ESTreeNode es1 = new ESTreeNode(); + es1.setNode(n1); + ESTreeNode es2 = new ESTreeNode(); + es2.setNode(n2); + ESTreeNode es3 = new ESTreeNode(); + es3.setNode(n3); + ESTreeNode es4 = new ESTreeNode(); + es4.setNode(n4); + ESTreeNode es5 = new ESTreeNode(); + es5.setNode(n5); + ESTreeNode es6 = new ESTreeNode(); + es6.setNode(n6); + ESTreeNode es7 = new ESTreeNode(); + es7.setNode(n7); + + List nodeIds = + Arrays.asList(n1.getUniqueId(), n2.getUniqueId(), n3.getUniqueId(), n4.getUniqueId(), n5.getUniqueId(), n6.getUniqueId(), n7.getUniqueId()); + + ESTreeNode targetTreeNode = new ESTreeNode(); + Node parentNode = + Node.builder().uniqueId("parent").build(); + targetTreeNode.setChildNodes(nodeIds); + targetTreeNode.setNode(parentNode); + when(elasticsearchTreeRepository.findById("parent")).thenReturn(Optional.of(targetTreeNode)); + when(elasticsearchTreeRepository.findAllById(nodeIds)) + .thenReturn(Arrays.asList(es1, es2, es3, es4, es5, es6, es7)); + + Node s1 = Node.builder().name("abc").build(); + List targetChildNodes = Arrays.asList(n1, n2, n3, n4, n5, n6, n7); + assertEquals("abc copy", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("ABC").build(); + assertEquals("ABC", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("DEF copy 777").build(); + assertEquals("DEF copy 778", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).name("def").build(); + assertEquals("def", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("XYZ copy abc").build(); + assertEquals("XYZ copy abc copy", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + } + + @Test + public void testDetermineNewNodeName2() { + Node n1 = Node.builder().uniqueId("abc").name("abc").build(); + Node n2 = Node.builder().uniqueId("abc copy").name("abc copy").build(); + Node n3 = Node.builder().uniqueId("abc copy 5").name("abc copy 5").build(); + Node n4 = Node.builder().uniqueId("abc copy 10").name("abc copy 10").build(); + + ESTreeNode es1 = new ESTreeNode(); + es1.setNode(n1); + ESTreeNode es2 = new ESTreeNode(); + es2.setNode(n2); + ESTreeNode es3 = new ESTreeNode(); + es3.setNode(n3); + ESTreeNode es4 = new ESTreeNode(); + es4.setNode(n4); + + List nodeIds = + Arrays.asList(n1.getUniqueId(), n2.getUniqueId(), n3.getUniqueId(), n4.getUniqueId()); + + ESTreeNode targetTreeNode = new ESTreeNode(); + Node parentNode = + Node.builder().uniqueId("parent").build(); + targetTreeNode.setChildNodes(nodeIds); + targetTreeNode.setNode(parentNode); + when(elasticsearchTreeRepository.findById("parent")).thenReturn(Optional.of(targetTreeNode)); + when(elasticsearchTreeRepository.findAllById(nodeIds)) + .thenReturn(Arrays.asList(es1, es2, es3, es4)); + + Node s1 = Node.builder().name("abc").build(); + List targetChildNodes = Arrays.asList(n1, n2, n3, n4); + assertEquals("abc copy 11", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("abc copy").build(); + assertEquals("abc copy 11", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("abc copy 7").build(); + assertEquals("abc copy 7", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + when(elasticsearchTreeRepository.findAllById(nodeIds)) + .thenReturn(Arrays.asList(es1, es4)); + + s1 = Node.builder().name("abc copy 7").build(); + assertEquals("abc copy 7", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + s1 = Node.builder().name("abc").build(); + assertEquals("abc copy 11", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + } + + @Test + public void testDetermineNewNodeName3() { + Node n1 = Node.builder().uniqueId("abc").name("abc").build(); + Node n2 = Node.builder().uniqueId("abc copy").name("abc copy").build(); + + ESTreeNode es1 = new ESTreeNode(); + es1.setNode(n1); + ESTreeNode es2 = new ESTreeNode(); + es2.setNode(n2); + + List nodeIds = + Arrays.asList(n1.getUniqueId(), n2.getUniqueId()); + + ESTreeNode targetTreeNode = new ESTreeNode(); + Node parentNode = + Node.builder().uniqueId("parent").build(); + targetTreeNode.setChildNodes(nodeIds); + targetTreeNode.setNode(parentNode); + when(elasticsearchTreeRepository.findById("parent")).thenReturn(Optional.of(targetTreeNode)); + when(elasticsearchTreeRepository.findAllById(nodeIds)) + .thenReturn(Arrays.asList(es1, es2)); + + Node s1 = Node.builder().name("abc copy").build(); + List targetChildNodes = Arrays.asList(n1, n2); + assertEquals("abc copy 2", elasticsearchDAO.determineNewNodeName(s1, targetChildNodes)); + + } + + @Test + public void nodeNameComparatorTest() { + List sorted = Arrays.asList("abc", "abc copy").stream().sorted(new NodeNameComparator()).collect(Collectors.toList()); + assertEquals("abc", sorted.get(0)); + sorted = Arrays.asList("abc copy", "abc").stream().sorted(new NodeNameComparator()).collect(Collectors.toList()); + assertEquals("abc", sorted.get(0)); + sorted = Arrays.asList("abc copy", "abc copy 2").stream().sorted(new NodeNameComparator()).collect(Collectors.toList()); + assertEquals("abc copy", sorted.get(0)); + sorted = Arrays.asList("abc copy 3", "abc copy 2").stream().sorted(new NodeNameComparator()).collect(Collectors.toList()); + assertEquals("abc copy 2", sorted.get(0)); + sorted = Arrays.asList("abc copy", "abc copy").stream().sorted(new NodeNameComparator()).collect(Collectors.toList()); + assertEquals("abc copy", sorted.get(0)); + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java index 4255481f75..8e29211f0d 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/ControllersTestConfig.java @@ -26,18 +26,39 @@ import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.FilterRepository; import org.phoebus.service.saveandrestore.persistence.dao.impl.elasticsearch.SnapshotDataRepository; import org.phoebus.service.saveandrestore.search.SearchUtil; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; +import org.springframework.util.Base64Utils; @SpringBootConfiguration @ComponentScan(basePackages = "org.phoebus.service.saveandrestore.web.controllers") +@Import(WebSecurityConfig.class) @SuppressWarnings("unused") @Profile("!IT") public class ControllersTestConfig { + @Autowired + private String demoUser; + + @Autowired + private String demoUserPassword; + + @Autowired + private String demoAdmin; + + @Autowired + private String demoAdminPassword; + + @Autowired + private String demoReadOnly; + + @Autowired + private String demoReadOnlyPassword; + @Bean public NodeDAO nodeDAO() { return Mockito.mock(NodeDAO.class); @@ -79,4 +100,19 @@ public AcceptHeaderResolver acceptHeaderResolver() { public SearchUtil searchUtil() { return new SearchUtil(); } + + @Bean("userAuthorization") + public String userAuthorization() { + return "Basic " + Base64Utils.encodeToString((demoUser + ":" + demoUserPassword).getBytes()); + } + + @Bean("adminAuthorization") + public String adminAuthorization() { + return "Basic " + Base64Utils.encodeToString((demoAdmin + ":" + demoAdminPassword).getBytes()); + } + + @Bean("readOnlyAuthorization") + public String readOnlyAuthorization() { + return "Basic " + Base64Utils.encodeToString((demoReadOnly + ":" + demoReadOnlyPassword).getBytes()); + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java index cefd95c9e9..da1005650f 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/config/WebConfigTest.java @@ -25,10 +25,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @ContextHierarchy({@ContextConfiguration(classes = {WebConfiguration.class, ControllersTestConfig.class})}) +@TestPropertySource(locations = "classpath:test_application.properties") @SuppressWarnings("unused") public class WebConfigTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java index 522be2e636..d49f0912cf 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/AppMetaDataControllerTest.java @@ -26,6 +26,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -42,6 +43,7 @@ @ContextHierarchy({@ContextConfiguration(classes = {ControllersTestConfig.class})}) @WebMvcTest(AppMetaDataControllerTest.class) @ExtendWith(SpringExtension.class) +@TestPropertySource(locations = "classpath:test_application.properties") @SuppressWarnings("unused") public class AppMetaDataControllerTest { diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java new file mode 100644 index 0000000000..19cec3db71 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerPermitAllTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshotData; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@WebMvcTest(CompositeSnapshotController.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +public class CompositeSnapshotControllerPermitAllTest { + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private String demoUser; + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private static CompositeSnapshot compositeSnapshot; + + @BeforeAll + public static void init() { + compositeSnapshot = new CompositeSnapshot(); + compositeSnapshot.setCompositeSnapshotNode(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT) + .name("name").uniqueId("id").build()); + CompositeSnapshotData compositeSnapshotData = new CompositeSnapshotData(); + compositeSnapshotData.setReferencedSnapshotNodes(List.of("ref")); + compositeSnapshot.setCompositeSnapshotData(compositeSnapshotData); + } + + @Test + public void testCreateCompositeSnapshot() throws Exception { + + when(nodeDAO.createCompositeSnapshot(Mockito.any(String.class), Mockito.any(CompositeSnapshot.class))).thenReturn(compositeSnapshot); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot); + + MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + String s = result.getResponse().getContentAsString(); + // Make sure response contains expected data + objectMapper.readValue(s, CompositeSnapshot.class); + + request = put("/composite-snapshot?parentNodeId=id") + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + reset(nodeDAO); + } + + @Test + public void testUpdateCompositeSnapshot() throws Exception { + + Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot1); + + when(nodeDAO.updateCompositeSnapshot(compositeSnapshot1)).thenReturn(compositeSnapshot1); + when(nodeDAO.getNode("c")).thenReturn(node); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + String s = result.getResponse().getContentAsString(); + // Make sure response contains expected data + objectMapper.readValue(s, CompositeSnapshot.class); + + request = post("/composite-snapshot") + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + reset(nodeDAO); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java index 12dbb26113..8d4162cf24 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/CompositeSnapshotControllerTest.java @@ -34,7 +34,9 @@ import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -53,16 +55,29 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ControllersTestConfig.class) -@WebMvcTest(NodeController.class) +@WebMvcTest(CompositeSnapshotController.class) +@TestPropertySource(locations = "classpath:test_application.properties") public class CompositeSnapshotControllerTest { + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + @Autowired private NodeDAO nodeDAO; + @Autowired + private String demoUser; + @Autowired private MockMvc mockMvc; - private ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); private static CompositeSnapshot compositeSnapshot; @@ -81,8 +96,12 @@ public void testCreateCompositeSnapshot() throws Exception { when(nodeDAO.createCompositeSnapshot(Mockito.any(String.class), Mockito.any(CompositeSnapshot.class))).thenReturn(compositeSnapshot); - MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id").contentType(JSON) - .content(objectMapper.writeValueAsString(compositeSnapshot)); + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot); + + MockHttpServletRequestBuilder request = put("/composite-snapshot?parentNodeId=id") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) .andReturn(); @@ -91,16 +110,58 @@ public void testCreateCompositeSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); + request = put("/composite-snapshot?parentNodeId=id") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/composite-snapshot?parentNodeId=id") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = put("/composite-snapshot?parentNodeId=id") + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + reset(nodeDAO); } + @Test + public void testCreateCompositeSnapshotWrongNodeType() throws Exception{ + Node node = Node.builder().uniqueId("c").nodeType(NodeType.SNAPSHOT).build(); + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + MockHttpServletRequestBuilder request = put("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot1)); + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + @Test public void testUpdateCompositeSnapshot() throws Exception { - when(nodeDAO.updateCompositeSnapshot(Mockito.any(CompositeSnapshot.class))).thenReturn(compositeSnapshot); + Node node = Node.builder().uniqueId("c").nodeType(NodeType.COMPOSITE_SNAPSHOT).userName(demoUser).build(); + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + String compositeSnapshotString = objectMapper.writeValueAsString(compositeSnapshot1); - MockHttpServletRequestBuilder request = post("/composite-snapshot").contentType(JSON) - .content(objectMapper.writeValueAsString(compositeSnapshot)); + when(nodeDAO.updateCompositeSnapshot(compositeSnapshot1)).thenReturn(compositeSnapshot1); + when(nodeDAO.getNode("c")).thenReturn(node); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) .andReturn(); @@ -109,9 +170,55 @@ public void testUpdateCompositeSnapshot() throws Exception { // Make sure response contains expected data objectMapper.readValue(s, CompositeSnapshot.class); + when(nodeDAO.getNode("c")).thenReturn(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).uniqueId("c").userName("notUser").build()); + + request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/composite-snapshot") + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + when(nodeDAO.getNode("c")).thenReturn(Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).uniqueId("c").userName("notUser").build()); + + request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(compositeSnapshotString); + + mockMvc.perform(request).andExpect(status().isOk()); + reset(nodeDAO); } + @Test + public void testUpdateCompositeSnapshotWrongNodeType() throws Exception{ + Node node = Node.builder().uniqueId("c").userName(demoUser).nodeType(NodeType.SNAPSHOT).build(); + CompositeSnapshot compositeSnapshot1 = new CompositeSnapshot(); + compositeSnapshot1.setCompositeSnapshotNode(node); + + when(nodeDAO.getNode("c")).thenReturn(node); + + MockHttpServletRequestBuilder request = post("/composite-snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot1)); + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + @Test public void testGetCompositeSnapshotData() throws Exception { CompositeSnapshotData compositeSnapshotData = new CompositeSnapshotData(); @@ -178,7 +285,9 @@ public void testGetCompositeSnapshotConsistency() throws Exception { when(nodeDAO.checkForPVNameDuplicates(Mockito.any(List.class))).thenReturn(List.of("ref")); - MockHttpServletRequestBuilder request = post("/composite-snapshot-consistency-check").contentType(JSON) + MockHttpServletRequestBuilder request = post("/composite-snapshot-consistency-check") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) .content(objectMapper.writeValueAsString(List.of("id"))); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) @@ -189,6 +298,26 @@ public void testGetCompositeSnapshotConsistency() throws Exception { objectMapper.readValue(s, new TypeReference>() { }); + request = post("/composite-snapshot-consistency-check") + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("id"))); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + + request = post("/composite-snapshot-consistency-check") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("id"))); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/composite-snapshot-consistency-check") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("id"))); + + mockMvc.perform(request).andExpect(status().isOk()); + reset(nodeDAO); } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java new file mode 100644 index 0000000000..502df6b8f3 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerPermitAllTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.phoebus.applications.saveandrestore.model.Configuration; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@WebMvcTest(ConfigurationController.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +public class ConfigurationControllerPermitAllTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private String userAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + + @Test + public void testCreateConfiguration() throws Exception { + + reset(nodeDAO); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(Node.builder().build()); + MockHttpServletRequestBuilder request = put("/config?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/config?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/config?parentNodeId=a") + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testUpdateConfiguration() throws Exception { + + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName(demoUser).build(); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(configurationNode); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + MockHttpServletRequestBuilder request = post("/config") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + request = post("/config") + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java new file mode 100644 index 0000000000..4ab5544360 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/ConfigurationControllerTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.phoebus.applications.saveandrestore.model.Configuration; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@WebMvcTest(ConfigurationController.class) +@TestPropertySource(locations = "classpath:test_application.properties") +public class ConfigurationControllerTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + + @Test + public void testCreateConfiguration() throws Exception { + + reset(nodeDAO); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(Node.builder().build()); + MockHttpServletRequestBuilder request = put("/config?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/config?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/config?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = put("/config?parentNodeId=a") + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testUpdateConfiguration() throws Exception { + + Node configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName(demoUser).build(); + + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(configurationNode); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + MockHttpServletRequestBuilder request = post("/config") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + configurationNode = Node.builder().uniqueId("uniqueId").nodeType(NodeType.CONFIGURATION).userName("someUser").build(); + configuration.setConfigurationNode(configurationNode); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("uniqueId")).thenReturn(configurationNode); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/config") + .contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + + mockMvc.perform(request).andExpect(status().isUnauthorized() + ); + + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java new file mode 100644 index 0000000000..0864cf5f2a --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerPermitAllTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@WebMvcTest(FilterController.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +public class FilterControllerPermitAllTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private String userAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + + @Test + public void testSaveFilter() throws Exception { + + reset(nodeDAO); + + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser("user"); + + String filterString = objectMapper.writeValueAsString(filter); + + when(nodeDAO.saveFilter(Mockito.any(Filter.class))).thenReturn(filter); + + MockHttpServletRequestBuilder request = put("/filter") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(filterString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + String s = result.getResponse().getContentAsString(); + // Make sure response contains expected data + objectMapper.readValue(s, Filter.class); + + request = put("/filter") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(filterString); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/filter") + .contentType(JSON) + .content(filterString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testDeleteFilter() throws Exception { + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser(demoUser); + + when(nodeDAO.getAllFilters()).thenReturn(List.of(filter)); + + MockHttpServletRequestBuilder request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isOk()); + + request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isOk()); + + request = delete("/filter/name") + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java index 1cc9d41e92..dccca5a33e 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/FilterControllerTest.java @@ -28,7 +28,9 @@ import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -37,17 +39,17 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ControllersTestConfig.class) -@WebMvcTest(NodeController.class) +@WebMvcTest(FilterController.class) +@TestPropertySource(locations = "classpath:test_application.properties") public class FilterControllerTest { @Autowired @@ -59,17 +61,36 @@ public class FilterControllerTest { @Autowired private ObjectMapper objectMapper; + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + @Test public void testSaveFilter() throws Exception { + reset(nodeDAO); + Filter filter = new Filter(); filter.setName("name"); filter.setQueryString("query"); + filter.setUser("user"); + + String filterString = objectMapper.writeValueAsString(filter); when(nodeDAO.saveFilter(Mockito.any(Filter.class))).thenReturn(filter); - MockHttpServletRequestBuilder request = put("/filter").contentType(JSON) - .content(objectMapper.writeValueAsString(filter)); + MockHttpServletRequestBuilder request = put("/filter") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(filterString); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) .andReturn(); @@ -77,12 +98,64 @@ public void testSaveFilter() throws Exception { String s = result.getResponse().getContentAsString(); // Make sure response contains expected data objectMapper.readValue(s, Filter.class); + + request = put("/filter") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(filterString); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = put("/filter") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(filterString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = put("/filter") + .contentType(JSON) + .content(filterString); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); } @Test public void testDeleteFilter() throws Exception { - MockHttpServletRequestBuilder request = delete("/filter/name").contentType(JSON); + Filter filter = new Filter(); + filter.setName("name"); + filter.setQueryString("query"); + filter.setUser(demoUser); + + when(nodeDAO.getAllFilters()).thenReturn(List.of(filter)); + + MockHttpServletRequestBuilder request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON); mockMvc.perform(request).andExpect(status().isOk()); + + request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isOk()); + + request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = delete("/filter/name") + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + filter.setUser("notUser"); + when(nodeDAO.getAllFilters()).thenReturn(List.of(filter)); + + request = delete("/filter/name") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON); + mockMvc.perform(request).andExpect(status().isForbidden()); + } @Test diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java index f6df59f368..3843053690 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/HelpResourceTest.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -35,6 +36,7 @@ @ExtendWith(SpringExtension.class) @WebMvcTest(HelpResource.class) @ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application.properties") public class HelpResourceTest{ @Autowired diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java new file mode 100644 index 0000000000..1f591c298e --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerPermitAllTest.java @@ -0,0 +1,206 @@ +/** + * Copyright (C) 2018 European Spallation Source ERIC. + *

      + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + *

      + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

      + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +@WebMvcTest(NodeController.class) +/** + * Main purpose of the tests in this class is to verify that REST end points are + * maintained, i.e. that URLs are not changed and that they return the correct + * data. + * + * @author Georg Weiss, European Spallation Source + * + */ +public class NodeControllerPermitAllTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + private static Node folderFromClient; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private String demoUser; + + @Autowired + private String userAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @BeforeAll + public static void setUp() { + folderFromClient = Node.builder().name("SomeFolder").userName("myusername").uniqueId("11").build(); + } + + @Test + public void testCreateFolder() throws Exception { + + when(nodeDAO.createNode(Mockito.any(String.class), Mockito.any(Node.class))).thenReturn(folderFromClient); + + String content = objectMapper.writeValueAsString(folderFromClient); + + MockHttpServletRequestBuilder request = put("/node?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(content); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + String s = result.getResponse().getContentAsString(); + // Make sure response contains expected data + objectMapper.readValue(s, Node.class); + + request = put("/node?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(content); + mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)); + + request = put("/node?parentNodeId=a") + .contentType(JSON) + .content(content); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testDeleteFolder() throws Exception { + + MockHttpServletRequestBuilder request = + post("/node"); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testGetFolderIllegalArgument() throws Exception { + when(nodeDAO.getNode("a")).thenThrow(IllegalArgumentException.class); + + MockHttpServletRequestBuilder request = get("/node/a"); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + + } + + @Test + public void testUpdateNode() throws Exception { + + reset(nodeDAO); + + Node node = Node.builder().name("foo").uniqueId("a").userName(demoUser).build(); + + when(nodeDAO.getNode("a")).thenReturn(node); + when(nodeDAO.updateNode(Mockito.any(Node.class), Mockito.anyBoolean())).thenReturn(node); + + MockHttpServletRequestBuilder request = post("/node") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); + + node = Node.builder().name("foo").uniqueId("a").userName("notDemoUser").build(); + + when(nodeDAO.getNode("a")).thenReturn(node); + when(nodeDAO.updateNode(Mockito.any(Node.class), Mockito.anyBoolean())).thenReturn(node); + + + request = post("/node") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/node") + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java index 23d679ad2e..60bc8075cc 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/NodeControllerTest.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.phoebus.applications.saveandrestore.model.Configuration; import org.phoebus.applications.saveandrestore.model.Node; @@ -35,7 +34,9 @@ import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -49,24 +50,21 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ControllersTestConfig.class) -@WebMvcTest(NodeController.class) /** * Main purpose of the tests in this class is to verify that REST end points are * maintained, i.e. that URLs are not changed and that they return the correct * data. * * @author Georg Weiss, European Spallation Source - * */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application.properties") +@WebMvcTest(NodeController.class) public class NodeControllerTest { @Autowired @@ -79,9 +77,22 @@ public class NodeControllerTest { private static Node config1; - private static Node snapshot; + private final ObjectMapper objectMapper = new ObjectMapper(); - private ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private String demoUser; + + @Autowired + private String demoAdmin; + + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; @BeforeAll public static void setUp() { @@ -90,10 +101,6 @@ public static void setUp() { .userName("myusername").build(); folderFromClient = Node.builder().name("SomeFolder").userName("myusername").uniqueId("11").build(); - - snapshot = Node.builder().nodeType(NodeType.SNAPSHOT).nodeType(NodeType.SNAPSHOT).name("name") - .build(); - } @Test @@ -101,8 +108,12 @@ public void testCreateFolder() throws Exception { when(nodeDAO.createNode(Mockito.any(String.class), Mockito.any(Node.class))).thenReturn(folderFromClient); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=a").contentType(JSON) - .content(objectMapper.writeValueAsString(folderFromClient)); + String content = objectMapper.writeValueAsString(folderFromClient); + + MockHttpServletRequestBuilder request = put("/node?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(content); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) .andReturn(); @@ -110,17 +121,23 @@ public void testCreateFolder() throws Exception { String s = result.getResponse().getContentAsString(); // Make sure response contains expected data objectMapper.readValue(s, Node.class); - } - - @Test - public void testCreateFolderNoUsername() throws Exception { - Node folder = Node.builder().name("SomeFolder").uniqueId("11").build(); + request = put("/node?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(content); + mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON) - .content(objectMapper.writeValueAsString(folder)); + request = put("/node?parentNodeId=a") + .contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .content(content); + mockMvc.perform(request).andExpect(status().isForbidden()); - mockMvc.perform(request).andExpect(status().isBadRequest()); + request = put("/node?parentNodeId=a") + .contentType(JSON) + .content(content); + mockMvc.perform(request).andExpect(status().isUnauthorized()); } @Test @@ -129,7 +146,9 @@ public void testCreateFolderParentIdDoesNotExist() throws Exception { when(nodeDAO.createNode(Mockito.anyString(), Mockito.any(Node.class))) .thenThrow(new IllegalArgumentException("Parent folder does not exist")); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON) + MockHttpServletRequestBuilder request = put("/node?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) .content(objectMapper.writeValueAsString(folderFromClient)); mockMvc.perform(request).andExpect(status().isBadRequest()); @@ -146,13 +165,12 @@ public void testCreateConfig() throws Exception { Configuration configuration = new Configuration(); configuration.setConfigurationNode(config); - when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer(new Answer() { - public Configuration answer(InvocationOnMock invocation) { - return configuration; - } - }); + when(nodeDAO.createConfiguration(Mockito.any(String.class), Mockito.any(Configuration.class))).thenAnswer((Answer) invocation -> configuration); - MockHttpServletRequestBuilder request = put("/config?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(configuration)); + MockHttpServletRequestBuilder request = put("/config?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(configuration)); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) .andReturn(); @@ -162,30 +180,78 @@ public Configuration answer(InvocationOnMock invocation) { } @Test - public void testCreateNodeBadRequests() throws Exception { + public void testUpdateConfig() throws Exception { reset(nodeDAO); - Node config = Node.builder().nodeType(NodeType.FOLDER).name("config").uniqueId("hhh") - .build(); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(config)); - mockMvc.perform(request).andExpect(status().isBadRequest()); + Node config = Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh") + .userName("user").build(); - config = Node.builder().nodeType(NodeType.FOLDER).name("config").uniqueId("hhh") - .userName("").build(); + Configuration configuration = new Configuration(); + configuration.setConfigurationNode(config); - request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(config)); - mockMvc.perform(request).andExpect(status().isBadRequest()); + String confurationAsString = objectMapper.writeValueAsString(configuration); + + Configuration updatedConfiguration = new Configuration(); + updatedConfiguration.setConfigurationNode(Node.builder().nodeType(NodeType.CONFIGURATION).name("config").uniqueId("hhh") + .userName(demoAdmin).build()); - config = Node.builder().nodeType(NodeType.FOLDER).uniqueId("hhh") + when(nodeDAO.updateConfiguration(updatedConfiguration)).thenReturn(updatedConfiguration); + + MockHttpServletRequestBuilder request = post("/config") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(confurationAsString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Configuration.class); + + when(nodeDAO.getNode("hhh")).thenReturn(Node.builder().nodeType(NodeType.CONFIGURATION).userName("notUser").build()); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(confurationAsString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(confurationAsString); + + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("hhh")).thenReturn(Node.builder().nodeType(NodeType.CONFIGURATION).userName("notUser").build()); + + request = post("/config") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(confurationAsString); + + mockMvc.perform(request).andExpect(status().isForbidden()); + } + + @Test + public void testCreateNodeBadRequests() throws Exception { + reset(nodeDAO); + + Node config = Node.builder().nodeType(NodeType.FOLDER).uniqueId("hhh") .userName("valid").build(); - request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(config)); + MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .content(objectMapper.writeValueAsString(config)); mockMvc.perform(request).andExpect(status().isBadRequest()); config = Node.builder().nodeType(NodeType.FOLDER).name("").uniqueId("hhh") .userName("valid").build(); - request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(config)); + request = put("/node?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON).content(objectMapper.writeValueAsString(config)); mockMvc.perform(request).andExpect(status().isBadRequest()); } @@ -193,11 +259,7 @@ public void testCreateNodeBadRequests() throws Exception { public void testGetChildNodes() throws Exception { reset(nodeDAO); - when(nodeDAO.getChildNodes("p")).thenAnswer(new Answer>() { - public List answer(InvocationOnMock invocation) throws Throwable { - return Arrays.asList(config1); - } - }); + when(nodeDAO.getChildNodes("p")).thenAnswer((Answer>) invocation -> Collections.singletonList(config1)); MockHttpServletRequestBuilder request = get("/node/p/children").contentType(JSON); @@ -205,7 +267,7 @@ public List answer(InvocationOnMock invocation) throws Throwable { .andReturn(); // Make sure response contains expected data - List childNodes = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference>() { + List childNodes = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<>() { }); assertEquals(1, childNodes.size()); @@ -242,15 +304,104 @@ public void testGetSnapshotsForNonExistingConfig() throws Exception { } @Test - public void testDeleteFolder() throws Exception { - MockHttpServletRequestBuilder request = delete("/node/a"); + public void testDeleteSnapshot() throws Exception { + + when(nodeDAO.getNode("a")).thenReturn(Node.builder() + .nodeType(NodeType.SNAPSHOT) + .uniqueId("a").userName(demoUser).build()); + + MockHttpServletRequestBuilder request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); mockMvc.perform(request).andExpect(status().isOk()); } @Test - public void testDeleteNode() throws Exception { - MockHttpServletRequestBuilder request = delete("/node/s"); + public void testDeleteFolder() throws Exception { + + MockHttpServletRequestBuilder request = + post("/node"); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName("notDemoUser").build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isForbidden()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").userName(demoUser).build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); + mockMvc.perform(request).andExpect(status().isForbidden()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(Collections.emptyList()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(Collections.emptyList()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isForbidden()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + mockMvc.perform(request).andExpect(status().isForbidden()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.CONFIGURATION).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, adminAuthorization); + mockMvc.perform(request).andExpect(status().isOk()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder().uniqueId("a").nodeType(NodeType.FOLDER).userName(demoUser).build()); + when(nodeDAO.getChildNodes("a")).thenReturn(List.of(Node.builder().build())); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, adminAuthorization); mockMvc.perform(request).andExpect(status().isOk()); } @@ -314,63 +465,6 @@ public void testGetNonExistingFolder() throws Exception { } - @Test - public void testMoveNode() throws Exception { - when(nodeDAO.moveNodes(Arrays.asList("a"), "b", "username")).thenReturn(Node.builder().uniqueId("2").uniqueId("a").build()); - - MockHttpServletRequestBuilder request = post("/move") - .contentType(JSON) - .content(objectMapper.writeValueAsString(Arrays.asList("a"))) - .param("to", "b") - .param("username", "username"); - - MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) - .andReturn(); - - // Make sure response contains expected data - objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); - } - - @Test - public void testMoveNodeUsernameEmpty() throws Exception { - MockHttpServletRequestBuilder request = post("/move") - .contentType(JSON) - .content(objectMapper.writeValueAsString(Arrays.asList("a"))) - .param("to", "b") - .param("username", ""); - - mockMvc.perform(request).andExpect(status().isBadRequest()); - } - - @Test - public void testMoveNodeTargetIdEmpty() throws Exception { - MockHttpServletRequestBuilder request = post("/move") - .contentType(JSON) - .content(objectMapper.writeValueAsString(Arrays.asList("a"))) - .param("to", "") - .param("username", "user"); - - mockMvc.perform(request).andExpect(status().isBadRequest()); - } - - @Test - public void testMoveNodeSourceNodeListEmpty() throws Exception { - MockHttpServletRequestBuilder request = post("/move") - .contentType(JSON) - .content(objectMapper.writeValueAsString(Collections.emptyList())) - .param("to", "targetId") - .param("username", "user"); - - mockMvc.perform(request).andExpect(status().isBadRequest()); - } - - @Test - public void testMoveNodeNoUsername() throws Exception { - MockHttpServletRequestBuilder request = post("/move").param("to", "b"); - - mockMvc.perform(request).andExpect(status().isBadRequest()); - } - @Test public void testGetFolderIllegalArgument() throws Exception { when(nodeDAO.getNode("a")).thenThrow(IllegalArgumentException.class); @@ -384,11 +478,15 @@ public void testGetFolderIllegalArgument() throws Exception { @Test public void testUpdateNode() throws Exception { - Node node = Node.builder().name("foo").uniqueId("a").build(); + reset(nodeDAO); + Node node = Node.builder().name("foo").uniqueId("a").userName(demoUser).build(); + + when(nodeDAO.getNode("a")).thenReturn(node); when(nodeDAO.updateNode(Mockito.any(Node.class), Mockito.anyBoolean())).thenReturn(node); MockHttpServletRequestBuilder request = post("/node") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .param("customTimeForMigration", "false") .contentType(JSON) .content(objectMapper.writeValueAsString(node)); @@ -399,6 +497,44 @@ public void testUpdateNode() throws Exception { // Make sure response contains expected data objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); + node = Node.builder().name("foo").uniqueId("a").userName("notDemoUser").build(); + + when(nodeDAO.getNode("a")).thenReturn(node); + when(nodeDAO.updateNode(Mockito.any(Node.class), Mockito.anyBoolean())).thenReturn(node); + + request = post("/node") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/node") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isOk()); + + + request = post("/node") + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + request = post("/node") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/node") + .param("customTimeForMigration", "false") + .contentType(JSON) + .content(objectMapper.writeValueAsString(node)); + mockMvc.perform(request).andExpect(status().isUnauthorized()); } @Test @@ -411,11 +547,11 @@ public void testGetFromPath() throws Exception { mockMvc.perform(request).andExpect(status().isBadRequest()); Node node = Node.builder().name("name").uniqueId("uniqueId").build(); - when(nodeDAO.getFromPath("/a/b/c")).thenReturn(Arrays.asList(node)); + when(nodeDAO.getFromPath("/a/b/c")).thenReturn(Collections.singletonList(node)); request = get("/path?path=/a/b/c"); MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andReturn(); - List nodes = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference>() { + List nodes = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<>() { }); assertEquals(1, nodes.size()); @@ -447,7 +583,10 @@ public void testCreateNodeWithInvalidTags() throws Exception { tag2.setName("goLDeN"); compositeSnapshot.setTags(Arrays.asList(tag1, tag2)); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(compositeSnapshot)); + MockHttpServletRequestBuilder request = put("/node?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot)); mockMvc.perform(request).andExpect(status().isBadRequest()); } @@ -456,7 +595,7 @@ public void testCreateNodeWithInvalidTags() throws Exception { public void testUpdateNodeWithInvalidTags() throws Exception { Node compositeSnapshot = Node.builder().nodeType(NodeType.COMPOSITE_SNAPSHOT).name("composite snapshot").uniqueId("hhh") - .userName("user").build(); + .userName(demoUser).build(); Tag tag1 = new Tag(); tag1.setName("a"); @@ -464,7 +603,12 @@ public void testUpdateNodeWithInvalidTags() throws Exception { tag2.setName("goLDeN"); compositeSnapshot.setTags(Arrays.asList(tag1, tag2)); - MockHttpServletRequestBuilder request = post("/node").contentType(JSON).content(objectMapper.writeValueAsString(compositeSnapshot)); + when(nodeDAO.getNode("hhh")).thenReturn(compositeSnapshot); + + MockHttpServletRequestBuilder request = post("/node") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot)); mockMvc.perform(request).andExpect(status().isBadRequest()); } @@ -483,7 +627,10 @@ public void testCreateNodeWithValidTags1() throws Exception { when(nodeDAO.createNode(Mockito.any(String.class), Mockito.any(Node.class))).thenReturn(compositeSnapshot); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(compositeSnapshot)); + MockHttpServletRequestBuilder request = put("/node?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot)); mockMvc.perform(request).andExpect(status().isOk()); @@ -504,7 +651,10 @@ public void testCreateNodeWithValidTags2() throws Exception { when(nodeDAO.createNode(Mockito.any(String.class), Mockito.any(Node.class))).thenReturn(compositeSnapshot); - MockHttpServletRequestBuilder request = put("/node?parentNodeId=p").contentType(JSON).content(objectMapper.writeValueAsString(compositeSnapshot)); + MockHttpServletRequestBuilder request = put("/node?parentNodeId=p") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(compositeSnapshot)); mockMvc.perform(request).andExpect(status().isOk()); diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java index a65d0ea781..07ee27333e 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SearchControllerTest.java @@ -19,24 +19,29 @@ package org.phoebus.service.saveandrestore.web.controllers; +import co.elastic.clients.elasticsearch.core.SearchRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.search.SearchResult; import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.search.SearchUtil; import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; +import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import java.util.List; @@ -49,7 +54,8 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ControllersTestConfig.class) -@WebMvcTest(NodeController.class) +@WebMvcTest(SearchController.class) +@TestPropertySource(locations = "classpath:test_application.properties") public class SearchControllerTest { @Autowired @@ -61,13 +67,18 @@ public class SearchControllerTest { @Autowired private ObjectMapper objectMapper; + @Autowired + private SearchUtil searchUtil; + + @Value("${elasticsearch.configuration_node.index:saveandrestore_configuration}") + public String ES_CONFIGURATION_INDEX; + @Test - public void testSearch() throws Exception{ + public void testSearch() throws Exception { SearchResult searchResult = new SearchResult(); searchResult.setHitCount(1); searchResult.setNodes(List.of(Node.builder().name("node").build())); - when(nodeDAO.search(Mockito.any())).thenReturn(searchResult); MockHttpServletRequestBuilder request = get("/search").contentType(JSON).params(new LinkedMultiValueMap<>()); @@ -80,4 +91,16 @@ public void testSearch() throws Exception{ SearchResult searchResult1 = objectMapper.readValue(s, SearchResult.class); assertEquals(1, searchResult1.getHitCount()); } + + @Test + public void testSearchForPVs() { + MultivaluedMap> searchParams = new MultivaluedHashMap<>(); + searchParams.put("type", List.of(List.of(NodeType.CONFIGURATION.toString()))); + searchParams.put("pvs", List.of(List.of("abc"))); + + SearchRequest searchRequest = searchUtil.buildSearchRequestForPvs(List.of("abc")); + assertEquals(ES_CONFIGURATION_INDEX, searchRequest.index().get(0)); + assertEquals("pvList", searchRequest.query().bool().must().get(0).disMax().queries().get(0).match().field()); + assertEquals("abc", searchRequest.query().bool().must().get(0).disMax().queries().get(0).match().query().stringValue()); + } } diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java new file mode 100644 index 0000000000..a56b005c04 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerPermitAllTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.Snapshot; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +@WebMvcTest(SnapshotController.class) +public class SnapshotControllerPermitAllTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private String userAuthorization; + + @Autowired + private String readOnlyAuthorization; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private String demoUser; + + @Test + public void testSaveNewSnapshot() throws Exception { + Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + when(nodeDAO.createSnapshot(Mockito.any(String.class), Mockito.any(Snapshot.class))) + .thenAnswer((Answer) invocation -> snapshot); + + MockHttpServletRequestBuilder request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(snapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); + + request = put("/snapshot?parentNodeId=a") + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isOk()); + } + + @Test + public void testDeleteSnapshot() throws Exception{ + + when(nodeDAO.getNode("a")).thenReturn(Node.builder() + .nodeType(NodeType.SNAPSHOT) + .uniqueId("a").userName(demoUser).build()); + + MockHttpServletRequestBuilder request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testUpdateSnapshot() throws Exception { + Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + when(nodeDAO.getNode("s")).thenReturn(node); + when(nodeDAO.updateSnapshot(Mockito.any(Snapshot.class))) + .thenAnswer((Answer) invocation -> snapshot); + + MockHttpServletRequestBuilder request = post("/snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(snapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); + + request = put("/snapshot") + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + request = post("/snapshot") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isOk()); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java new file mode 100644 index 0000000000..5424ce6965 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/SnapshotControllerTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.Snapshot; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application.properties") +@WebMvcTest(SnapshotController.class) +public class SnapshotControllerTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private MockMvc mockMvc; + + @Autowired + private String demoUser; + + + @Test + public void testSaveSnapshotWrongNodeType() throws Exception { + + Node node = Node.builder().uniqueId("uniqueId").userName(demoUser).nodeType(NodeType.FOLDER).build(); + + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + + MockHttpServletRequestBuilder request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(snapshot)); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + + @Test + public void testSaveSnapshotNoParentNodeId() throws Exception { + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).build()); + + MockHttpServletRequestBuilder request = put("/snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(snapshot)); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + + @Test + public void testCreateSnapshot() throws Exception { + Node node = Node.builder().uniqueId("uniqueId").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + when(nodeDAO.createSnapshot(Mockito.any(String.class), Mockito.any(Snapshot.class))) + .thenAnswer((Answer) invocation -> snapshot); + + MockHttpServletRequestBuilder request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(snapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); + + request = put("/snapshot?parentNodeId=a") + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = put("/snapshot?parentNodeId=a") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isOk()); + } + + @Test + public void testUpdateSnapshot() throws Exception { + Node node = Node.builder().uniqueId("s").nodeType(NodeType.SNAPSHOT).userName(demoUser).build(); + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(node); + + String snapshotString = objectMapper.writeValueAsString(snapshot); + + when(nodeDAO.getNode("s")).thenReturn(node); + when(nodeDAO.updateSnapshot(Mockito.any(Snapshot.class))) + .thenAnswer((Answer) invocation -> snapshot); + + MockHttpServletRequestBuilder request = post("/snapshot") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(snapshotString); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Snapshot.class); + + request = put("/snapshot") + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + + request = post("/snapshot") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/snapshot") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(snapshotString); + mockMvc.perform(request).andExpect(status().isOk()); + } + + @Test + public void testDeleteSnapshot() throws Exception{ + + when(nodeDAO.getNode("a")).thenReturn(Node.builder() + .nodeType(NodeType.SNAPSHOT) + .uniqueId("a").userName(demoUser).build()); + + MockHttpServletRequestBuilder request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, userAuthorization); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + when(nodeDAO.getNode("a")).thenReturn(Node.builder() + .nodeType(NodeType.SNAPSHOT) + .uniqueId("a").userName("otherUser").build()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))) + .header(HttpHeaders.AUTHORIZATION, adminAuthorization); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = + delete("/node") + .contentType(JSON).content(objectMapper.writeValueAsString(List.of("a"))); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java new file mode 100644 index 0000000000..c82b44e5ae --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerPermitAllTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +@WebMvcTest(StructureController.class) +public class StructureControllerPermitAllTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private String demoUser; + + @Autowired + private String demoAdmin; + + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + + @Test + public void testMoveNode() throws Exception { + when(nodeDAO.moveNodes(List.of("a"), "b", demoAdmin)) + .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoAdmin).build()); + + MockHttpServletRequestBuilder request = post("/move") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); + + when(nodeDAO.moveNodes(List.of("a"), "b", demoUser)) + .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoUser).build()); + + request = post("/move") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isOk()); + + + request = post("/move") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/move") + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of + ("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testCopyNodes() throws Exception { + MockHttpServletRequestBuilder request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/copy") + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java new file mode 100644 index 0000000000..df736f8acc --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/StructureControllerTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2023 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@TestPropertySource(locations = "classpath:test_application.properties") +@WebMvcTest(StructureController.class) +public class StructureControllerTest { + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private String demoUser; + + @Autowired + private String demoAdmin; + + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + + @Test + public void testMoveNode() throws Exception { + when(nodeDAO.moveNodes(List.of("a"), "b", demoAdmin)) + .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoAdmin).build()); + + MockHttpServletRequestBuilder request = post("/move") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + MvcResult result = mockMvc.perform(request).andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + // Make sure response contains expected data + objectMapper.readValue(result.getResponse().getContentAsString(), Node.class); + + when(nodeDAO.moveNodes(List.of("a"), "b", demoUser)) + .thenReturn(Node.builder().uniqueId("2").uniqueId("a").userName(demoUser).build()); + + request = post("/move") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + + request = post("/move") + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/move") + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "b") + .param("username", "username"); + + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testMoveNodeSourceNodeListEmpty() throws Exception { + MockHttpServletRequestBuilder request = post("/move") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(Collections.emptyList())) + .param("to", "targetId") + .param("username", "user"); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + + @Test + public void testMoveNodeTargetIdEmpty() throws Exception { + MockHttpServletRequestBuilder request = post("/move") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "") + .param("username", "user"); + + mockMvc.perform(request).andExpect(status().isBadRequest()); + } + + @Test + public void testCopyNodes() throws Exception { + MockHttpServletRequestBuilder request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/copy") + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isUnauthorized()); + } + + @Test + public void testCopyNodesBadRequest() throws Exception { + Node node = Node.builder().uniqueId("uniqueId").userName(demoUser).build(); + MockHttpServletRequestBuilder request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))) + .param("to", ""); + mockMvc.perform(request).andExpect(status().isBadRequest()); + + request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(List.of("a"))); + mockMvc.perform(request).andExpect(status().isBadRequest()); + + request = post("/copy") + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .contentType(JSON) + .content(objectMapper.writeValueAsString(Collections.emptyList())) + .param("to", "target"); + mockMvc.perform(request).andExpect(status().isBadRequest()); + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java new file mode 100644 index 0000000000..bc11cfc137 --- /dev/null +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerPermitAllTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +package org.phoebus.service.saveandrestore.web.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.Tag; +import org.phoebus.applications.saveandrestore.model.TagData; +import org.phoebus.service.saveandrestore.persistence.dao.NodeDAO; +import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ControllersTestConfig.class) +@WebMvcTest(TagController.class) +@TestPropertySource(locations = "classpath:test_application_permit_all.properties") +public class TagControllerPermitAllTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private NodeDAO nodeDAO; + + @Autowired + private String userAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void testAddTag() throws Exception{ + Tag tag = new Tag(); + tag.setName("tag"); + + Node node = Node.builder().name("name").uniqueId("uniqueId").userName(demoUser).tags(List.of(tag)).build(); + + TagData tagData = new TagData(); + tagData.setTag(tag); + tagData.setUniqueNodeIds(List.of("uniqueId")); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + when(nodeDAO.addTag(tagData)).thenReturn(List.of(node)); + + MockHttpServletRequestBuilder request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + MvcResult result = mockMvc.perform(request) + .andExpect(status().isOk()).andExpect(content().contentType(JSON)) + .andReturn(); + + String s = result.getResponse().getContentAsString(); + // Make sure response contains expected data + objectMapper.readValue(s, List.class); + + request = post("/tags").contentType(JSON) + .content(objectMapper.writeValueAsString(tagData)); + mockMvc.perform(request) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testGoldenTag() throws Exception{ + Tag tag = new Tag(); + tag.setName(Tag.GOLDEN); + tag.setUserName(demoUser); + + TagData tagData = new TagData(); + tagData.setTag(tag); + tagData.setUniqueNodeIds(List.of("uniqueId")); + + MockHttpServletRequestBuilder request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request).andExpect(status().isOk()); + } + + @Test + public void testDeleteTag() throws Exception{ + Tag tag = new Tag(); + tag.setName("tag"); + tag.setUserName(demoUser); + + TagData tagData = new TagData(); + tagData.setTag(tag); + tagData.setUniqueNodeIds(List.of("uniqueId")); + + Node node = Node.builder().name("name").uniqueId("uniqueId").userName("otherUser").tags(List.of(tag)).build(); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + + MockHttpServletRequestBuilder request = delete("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request) + .andExpect(status().isOk()); + + request = delete("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request) + .andExpect(status().isOk()); + + request = delete("/tags").contentType(JSON) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request) + .andExpect(status().isUnauthorized()); + + } +} diff --git a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java index dd78e6becd..b7f1afb71c 100644 --- a/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java +++ b/services/save-and-restore/src/test/java/org/phoebus/service/saveandrestore/web/controllers/TagControllerTest.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.applications.saveandrestore.model.TagData; @@ -30,7 +29,9 @@ import org.phoebus.service.saveandrestore.web.config.ControllersTestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -38,17 +39,16 @@ import java.util.List; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; import static org.phoebus.service.saveandrestore.web.controllers.BaseController.JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; - -import static org.springframework.test.web.servlet.MockMvc.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ControllersTestConfig.class) -@WebMvcTest(NodeController.class) +@WebMvcTest(TagController.class) +@TestPropertySource(locations = "classpath:test_application.properties") public class TagControllerTest { @Autowired @@ -57,10 +57,22 @@ public class TagControllerTest { @Autowired private NodeDAO nodeDAO; - private ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private String userAuthorization; + + @Autowired + private String adminAuthorization; + + @Autowired + private String readOnlyAuthorization; + + @Autowired + private String demoUser; + + private final ObjectMapper objectMapper = new ObjectMapper(); @Test - public void testGetAllTags() throws Exception{ + public void testGetAllTags() throws Exception { Tag tag = new Tag(); tag.setName("tag"); List tags = List.of(tag); @@ -76,18 +88,21 @@ public void testGetAllTags() throws Exception{ } @Test - public void testAddTag() throws Exception{ + public void testAddTag() throws Exception { Tag tag = new Tag(); tag.setName("tag"); - Node node = Node.builder().name("name").uniqueId("uniqueId").tags(List.of(tag)).build(); + Node node = Node.builder().name("name").uniqueId("uniqueId").userName(demoUser).tags(List.of(tag)).build(); TagData tagData = new TagData(); tagData.setTag(tag); tagData.setUniqueNodeIds(List.of("uniqueId")); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); when(nodeDAO.addTag(tagData)).thenReturn(List.of(node)); MockHttpServletRequestBuilder request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .content(objectMapper.writeValueAsString(tagData)); MvcResult result = mockMvc.perform(request) .andExpect(status().isOk()).andExpect(content().contentType(JSON)) @@ -99,11 +114,46 @@ public void testAddTag() throws Exception{ } @Test - public void testAddTagBadData() throws Exception{ + public void testGoldenTag() throws Exception { + Tag tag = new Tag(); + tag.setName(Tag.GOLDEN); + tag.setUserName(demoUser); TagData tagData = new TagData(); + tagData.setTag(tag); tagData.setUniqueNodeIds(List.of("uniqueId")); + MockHttpServletRequestBuilder request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request).andExpect(status().isOk()); + + request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, readOnlyAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request).andExpect(status().isForbidden()); + + } + + @Test + public void testAddTagBadData() throws Exception { + + TagData tagData = new TagData(); + tagData.setUniqueNodeIds(List.of("uniqueId")); + + Node node = Node.builder().name("name").uniqueId("uniqueId").userName(demoUser).build(); + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + + MockHttpServletRequestBuilder request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .content(objectMapper.writeValueAsString(tagData)); mockMvc.perform(request) .andExpect(status().isBadRequest()); @@ -112,6 +162,7 @@ public void testAddTagBadData() throws Exception{ tag.setName(null); tagData.setTag(tag); request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .content(objectMapper.writeValueAsString(tagData)); mockMvc.perform(request) .andExpect(status().isBadRequest()); @@ -119,6 +170,7 @@ public void testAddTagBadData() throws Exception{ tag.setName(""); tagData.setTag(tag); request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .content(objectMapper.writeValueAsString(tagData)); mockMvc.perform(request) .andExpect(status().isBadRequest()); @@ -127,13 +179,40 @@ public void testAddTagBadData() throws Exception{ tagData.setTag(tag); tagData.setUniqueNodeIds(null); request = post("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) .content(objectMapper.writeValueAsString(tagData)); mockMvc.perform(request) .andExpect(status().isBadRequest()); } @Test - public void testDeleteTag() throws Exception{ + public void testDeleteTag() throws Exception { + Tag tag = new Tag(); + tag.setName("tag"); + tag.setUserName(demoUser); + + TagData tagData = new TagData(); + tagData.setTag(tag); + tagData.setUniqueNodeIds(List.of("uniqueId")); + + Node node = Node.builder().name("name").uniqueId("uniqueId").userName("otherUser").tags(List.of(tag)).build(); + + when(nodeDAO.getNode("uniqueId")).thenReturn(node); + + MockHttpServletRequestBuilder request = delete("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, userAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request) + .andExpect(status().isForbidden()); + + request = delete("/tags").contentType(JSON) + .header(HttpHeaders.AUTHORIZATION, adminAuthorization) + .content(objectMapper.writeValueAsString(tagData)); + + mockMvc.perform(request) + .andExpect(status().isOk() + ); } } diff --git a/services/save-and-restore/src/test/resources/test_application.properties b/services/save-and-restore/src/test/resources/test_application.properties index 8cddd387d4..a1068517b7 100644 --- a/services/save-and-restore/src/test/resources/test_application.properties +++ b/services/save-and-restore/src/test/resources/test_application.properties @@ -1,7 +1,12 @@ +auth.impl = demo +authorization.permitall=false + logging.level.org.springframework=INFO app.version=@project.version@ app.name=@project.name@ +server.servlet.contextPath=/save-restore + # Elasticsearch connection parameters elasticsearch.network.host=localhost elasticsearch.http.port=9200 @@ -15,3 +20,5 @@ elasticsearch.configuration_node.index=test_saveandrestore_configuration elasticsearch.snapshot_node.index:test_saveandrestore_snapshot elasticsearch.composite_snapshot_node.index=test_saveandrestore_composite_snapshot elasticsearch.filter.index:test_saveandrestore_filter + + diff --git a/services/save-and-restore/src/test/resources/test_application_permit_all.properties b/services/save-and-restore/src/test/resources/test_application_permit_all.properties new file mode 100644 index 0000000000..7e2f457406 --- /dev/null +++ b/services/save-and-restore/src/test/resources/test_application_permit_all.properties @@ -0,0 +1,2 @@ +auth.impl = demo +authorization.permitall=true diff --git a/services/scan-server/.classpath b/services/scan-server/.classpath index 803e952708..619e2afc62 100644 --- a/services/scan-server/.classpath +++ b/services/scan-server/.classpath @@ -10,6 +10,6 @@ - + diff --git a/services/scan-server/pom.xml b/services/scan-server/pom.xml index 14818de184..fdc4ab1f81 100644 --- a/services/scan-server/pom.xml +++ b/services/scan-server/pom.xml @@ -3,7 +3,7 @@ org.phoebus services - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT service-scan-server @@ -66,22 +66,22 @@ org.phoebus core-framework - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-util - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus core-pv - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT org.phoebus app-scan-model - 4.7.2-SNAPSHOT + 4.7.4-SNAPSHOT @@ -109,7 +109,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.3.0 @@ -120,49 +120,6 @@ - - - - maven-antrun-plugin - - - verify - - true - - - - - - - - - - - - - - - - - - - - - - - run - - - - org.sonatype.plugins nexus-staging-maven-plugin diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/ScanCommandImpl.java b/services/scan-server/src/main/java/org/csstudio/scan/server/ScanCommandImpl.java index 6715b9ec37..cad09050de 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/ScanCommandImpl.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/ScanCommandImpl.java @@ -36,7 +36,7 @@ * A loop on the other hand will perform one unit of work per loop * iteration. * - *

      The {@link ExecutableScan} queries each command for the number of work + *

      The @see ExecutableScan queries each command for the number of work * units that it will perform, and the command must then update * the {@link ScanContext} with the number of performed work * units. diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationContext.java b/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationContext.java index 2c5d23053e..504e6da5bd 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationContext.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationContext.java @@ -59,7 +59,7 @@ public class SimulationContext /** Initialize * @param jython {@link JythonSupport} * @param log_stream Stream for simulation progress log - * @throws Exception on error while initializing {@link SimulationInfo} + * @throws Exception on error while initializing see SimulationInfo */ public SimulationContext(final JythonSupport jython, final PrintStream log_stream) throws Exception { diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationHook.java b/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationHook.java index d6a3783c7a..ec82798191 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationHook.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/SimulationHook.java @@ -11,7 +11,7 @@ /** Hook for simulation customization * - *

      The scan configuration file has a <simulation_hook> + *

      The scan configuration file has a {@literal } * entry to provide the name of a Jython class that implements * a custom simulation hook. * diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/command/IncludeCommandImpl.java b/services/scan-server/src/main/java/org/csstudio/scan/server/command/IncludeCommandImpl.java index dba305f4a7..6c3db4bd49 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/command/IncludeCommandImpl.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/command/IncludeCommandImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013-2018 Oak Ridge National Laboratory. + * Copyright (c) 2013-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -47,6 +47,8 @@ public IncludeCommandImpl(final IncludeCommand command, final JythonSupport jyth // Parse scan final List paths = ScanServerInstance.getScanConfig().getScriptPaths(); final InputStream scan_stream = PathStreamTool.openStream(paths, command.getScanFile()); + if (scan_stream == null) + throw new Exception(command + " cannot open '" + command.getScanFile() + "'"); final List commands = XMLCommandReader.readXMLStream(scan_stream); // Implement diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ServletHelper.java b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ServletHelper.java index 7c6431e056..9e77a00660 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ServletHelper.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ServletHelper.java @@ -76,7 +76,6 @@ public static void write(final XMLStreamWriter writer, final String name, final * @param writer {@link XMLStreamWriter} * @param name Name of XML element * @param number Number content - * @return XML element */ public static void write(final XMLStreamWriter writer, final String name, final long number) throws Exception { @@ -84,10 +83,9 @@ public static void write(final XMLStreamWriter writer, final String name, final } /** Create XML element for date, encoded as milliseconds since epoch - * @param doc Parent document + * @param writer Parent document * @param name Name of XML element * @param date Date content - * @return XML element */ public static void write(final XMLStreamWriter writer,final String name, final Instant date) throws Exception { diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java index e68aafa809..0b64550218 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java @@ -58,7 +58,7 @@ /** Scan that can be executed: Commands, device context, state * - *

      Combines a {@link DeviceContext} with {@link ScanContextImpl}ementations + *

      Combines a {@link DeviceContext} with {@link ScanContext}ementations * and can execute them. * When a command is executed, it receives a {@link ScanContext} view * of the scan for limited access to the devices, data logger etc. diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/log/derby/RDBDataLogger.java b/services/scan-server/src/main/java/org/csstudio/scan/server/log/derby/RDBDataLogger.java index e6ddcd6762..6fdbf90123 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/log/derby/RDBDataLogger.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/log/derby/RDBDataLogger.java @@ -278,7 +278,7 @@ public void log(final long scan_id, final String device, final ScanSample sample * @param scan_id ID of the scan * @return Serial of last sample in scan data or -1 if nothing has been logged * @throws Exception on error - * @see #getScanData() + * @see RDBDataLogger getScanData() */ public long getLastScanDataSerial(final long scan_id) throws Exception {