diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java
index a6882b1eb4..7c36ec32c6 100644
--- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java
+++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/persistence/XMLPersistence.java
@@ -7,23 +7,10 @@
******************************************************************************/
package org.csstudio.trends.databrowser3.persistence;
-import static org.csstudio.trends.databrowser3.Activator.logger;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.time.Duration;
-import java.time.temporal.TemporalAmount;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
-import java.util.logging.Level;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.stream.XMLOutputFactory;
-import javax.xml.stream.XMLStreamWriter;
-
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontPosture;
+import javafx.scene.text.FontWeight;
import org.csstudio.trends.databrowser3.model.AnnotationInfo;
import org.csstudio.trends.databrowser3.model.ArchiveRescale;
import org.csstudio.trends.databrowser3.model.AxisConfig;
@@ -41,113 +28,129 @@
import org.w3c.dom.Document;
import org.w3c.dom.Element;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.scene.text.FontPosture;
-import javafx.scene.text.FontWeight;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Duration;
+import java.time.temporal.TemporalAmount;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+
+import static org.csstudio.trends.databrowser3.Activator.logger;
-/** Load and save {@link Model} as XML file
+/**
+ * Load and save {@link Model} as XML file
*
- *
Attempts to load files going back to very early versions
- * of the Data Browser
+ *
Attempts to load files going back to very early versions
+ * of the Data Browser
*
- * @author Kay Kasemir
+ * @author Georg Weiss
*/
@SuppressWarnings("nls")
-public class XMLPersistence
-{
- /** Default font settings */
+public class XMLPersistence {
+ /**
+ * Default font settings
+ */
public static final String DEFAULT_FONT_FAMILY = "Liberation Sans";
- /** Default font settings */
+ /**
+ * Default font settings
+ */
public static final double DEFAULT_FONT_SIZE = 10;
- /** XML file tags */
+ /**
+ * XML file tags
+ */
final public static String TAG_DATABROWSER = "databrowser",
- TAG_TITLE = "title",
- TAG_SAVE_CHANGES = "save_changes",
- TAG_GRID = "grid",
- TAG_SCROLL = "scroll",
- TAG_UPDATE_PERIOD = "update_period",
- TAG_SCROLL_STEP = "scroll_step",
- TAG_START = "start",
- TAG_END = "end",
- TAG_ARCHIVE_RESCALE = "archive_rescale",
- TAG_FOREGROUND = "foreground",
- TAG_BACKGROUND = "background",
- TAG_TITLE_FONT = "title_font",
- TAG_LABEL_FONT = "label_font",
- TAG_SCALE_FONT = "scale_font",
- TAG_LEGEND_FONT = "legend_font",
- TAG_AXES = "axes",
- TAG_ANNOTATIONS = "annotations",
- TAG_PVLIST = "pvlist",
-
- TAG_SHOW_TOOLBAR = "show_toolbar",
- TAG_SHOW_LEGEND = "show_legend",
-
- TAG_COLOR = "color",
- TAG_RED = "red",
- TAG_GREEN = "green",
- TAG_BLUE = "blue",
-
- TAG_AXIS = "axis",
- TAG_VISIBLE = "visible",
- TAG_NAME = "name",
- TAG_USE_AXIS_NAME = "use_axis_name",
- TAG_USE_TRACE_NAMES = "use_trace_names",
- TAG_RIGHT = "right",
- TAG_MAX = "max",
- TAG_MIN = "min",
- TAG_AUTO_SCALE = "autoscale",
- TAG_LOG_SCALE = "log_scale",
-
- TAG_ANNOTATION = "annotation",
- TAG_PV = "pv",
- TAG_TIME = "time",
- TAG_VALUE = "value",
- TAG_OFFSET = "offset",
- TAG_TEXT = "text",
-
- TAG_X = "x",
- TAG_Y = "y",
-
- TAG_DISPLAYNAME = "display_name",
- TAG_TRACE_TYPE = "trace_type",
- TAG_LINE_STYLE = "line_style",
- TAG_LINEWIDTH = "linewidth",
- TAG_POINT_TYPE = "point_type",
- TAG_POINT_SIZE = "point_size",
- TAG_WAVEFORM_INDEX = "waveform_index",
- TAG_SCAN_PERIOD = "period",
- TAG_LIVE_SAMPLE_BUFFER_SIZE = "ring_size",
- TAG_REQUEST = "request",
- TAG_ARCHIVE = "archive",
-
- TAG_URL = "url",
-
- TAG_FORMULA = "formula",
- TAG_INPUT = "input",
-
- TAG_KEY = "key";
+ TAG_TITLE = "title",
+ TAG_SAVE_CHANGES = "save_changes",
+ TAG_GRID = "grid",
+ TAG_SCROLL = "scroll",
+ TAG_UPDATE_PERIOD = "update_period",
+ TAG_SCROLL_STEP = "scroll_step",
+ TAG_START = "start",
+ TAG_END = "end",
+ TAG_ARCHIVE_RESCALE = "archive_rescale",
+ TAG_FOREGROUND = "foreground",
+ TAG_BACKGROUND = "background",
+ TAG_TITLE_FONT = "title_font",
+ TAG_LABEL_FONT = "label_font",
+ TAG_SCALE_FONT = "scale_font",
+ TAG_LEGEND_FONT = "legend_font",
+ TAG_AXES = "axes",
+ TAG_ANNOTATIONS = "annotations",
+ TAG_PVLIST = "pvlist",
+
+ TAG_SHOW_TOOLBAR = "show_toolbar",
+ TAG_SHOW_LEGEND = "show_legend",
+
+ TAG_COLOR = "color",
+ TAG_RED = "red",
+ TAG_GREEN = "green",
+ TAG_BLUE = "blue",
+
+ TAG_AXIS = "axis",
+ TAG_VISIBLE = "visible",
+ TAG_NAME = "name",
+ TAG_USE_AXIS_NAME = "use_axis_name",
+ TAG_USE_TRACE_NAMES = "use_trace_names",
+ TAG_RIGHT = "right",
+ TAG_MAX = "max",
+ TAG_MIN = "min",
+ TAG_AUTO_SCALE = "autoscale",
+ TAG_LOG_SCALE = "log_scale",
+
+ TAG_ANNOTATION = "annotation",
+ TAG_PV = "pv",
+ TAG_TIME = "time",
+ TAG_VALUE = "value",
+ TAG_OFFSET = "offset",
+ TAG_TEXT = "text",
+
+ TAG_X = "x",
+ TAG_Y = "y",
+
+ TAG_DISPLAYNAME = "display_name",
+ TAG_TRACE_TYPE = "trace_type",
+ TAG_LINE_STYLE = "line_style",
+ TAG_LINEWIDTH = "linewidth",
+ TAG_POINT_TYPE = "point_type",
+ TAG_POINT_SIZE = "point_size",
+ TAG_WAVEFORM_INDEX = "waveform_index",
+ TAG_SCAN_PERIOD = "period",
+ TAG_LIVE_SAMPLE_BUFFER_SIZE = "ring_size",
+ TAG_REQUEST = "request",
+ TAG_ARCHIVE = "archive",
+
+ TAG_URL = "url",
+
+ TAG_FORMULA = "formula",
+ TAG_INPUT = "input",
+
+ TAG_KEY = "key";
final private static String TAG_OLD_XYGRAPH_SETTINGS = "xyGraphSettings";
- /** @param model Model to load
- * @param stream XML stream
- * @throws Exception on error
+ /**
+ * @param model Model to load
+ * @param stream XML stream
+ * @throws Exception on error
*/
- public static void load(final Model model, final InputStream stream) throws Exception
- {
+ public static void load(final Model model, final InputStream stream) throws Exception {
final DocumentBuilder docBuilder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
final Document doc = docBuilder.parse(stream);
load(model, doc);
}
- private static void load(final Model model, final Document doc) throws Exception
- {
+ private static void load(final Model model, final Document doc) throws Exception {
if (model.getItems().size() > 0)
throw new RuntimeException("Model was already in use");
@@ -164,44 +167,34 @@ private static void load(final Model model, final Document doc) throws Exception
XMLUtil.getChildDouble(root_node, TAG_UPDATE_PERIOD).ifPresent(model::setUpdatePeriod);
- try
- {
- model.setScrollStep( Duration.ofSeconds(
+ try {
+ model.setScrollStep(Duration.ofSeconds(
XMLUtil.getChildInteger(root_node, TAG_SCROLL_STEP).orElse((int) Preferences.scroll_step.getSeconds())));
- }
- catch (Throwable ex)
- {
+ } catch (Throwable ex) {
// Ignore
}
final String start = model.resolveMacros(XMLUtil.getChildString(root_node, TAG_START).orElse(""));
final String end = model.resolveMacros(XMLUtil.getChildString(root_node, TAG_END).orElse(""));
- if (start.length() > 0 && end.length() > 0)
- {
+ if (start.length() > 0 && end.length() > 0) {
final boolean scroll = XMLUtil.getChildBoolean(root_node, TAG_SCROLL).orElse(true);
final TimeRelativeInterval interval;
- if (scroll)
- { // Relative start time .. now
+ if (scroll) { // Relative start time .. now
final TemporalAmount span = TimeWarp.parseLegacy(start);
if (Duration.ZERO.equals(span))
interval = TimeRelativeInterval.of(Preferences.time_span, Duration.ZERO);
else
interval = TimeRelativeInterval.startsAt(span);
- }
- else
- { // Absolute start ... end
+ } else { // Absolute start ... end
interval = TimeRelativeInterval.of(TimestampFormats.parse(patchLegacyAbsTime(start)), TimestampFormats.parse(patchLegacyAbsTime(end)));
}
model.setTimerange(interval);
}
final String rescale = XMLUtil.getChildString(root_node, TAG_ARCHIVE_RESCALE).orElse(ArchiveRescale.STAGGER.name());
- try
- {
+ try {
model.setArchiveRescale(ArchiveRescale.valueOf(rescale));
- }
- catch (Throwable ex)
- {
+ } catch (Throwable ex) {
// Ignore
}
@@ -210,28 +203,20 @@ private static void load(final Model model, final Document doc) throws Exception
// Value Axes
final Element axes = XMLUtil.getChildElement(root_node, TAG_AXES);
- if (axes != null)
- {
+ if (axes != null) {
for (Element item : XMLUtil.getChildElements(axes, TAG_AXIS))
model.addAxis(AxisConfig.fromDocument(item));
- }
- else
- { // Check for legacy
+ } else { // Check for legacy
final Element list = XMLUtil.getChildElement(root_node, TAG_OLD_XYGRAPH_SETTINGS);
- if (list != null)
- {
+ if (list != null) {
loadColorFromDocument(list, "plotAreaBackColor").ifPresent(model::setPlotBackground);
boolean first_axis = true;
- for (Element item : XMLUtil.getChildElements(list, "axisSettingsList"))
- {
- if (first_axis)
- { // First axis is 'X'
+ for (Element item : XMLUtil.getChildElements(list, "axisSettingsList")) {
+ if (first_axis) { // First axis is 'X'
XMLUtil.getChildBoolean(item, "showMajorGrid").ifPresent(model::setGridVisible);
first_axis = false;
- }
- else
- { // Read 'Y' axes
+ } else { // Read 'Y' axes
final String name = XMLUtil.getChildString(item, "title").orElse(null);
final AxisConfig axis = new AxisConfig(name);
loadColorFromDocument(item, "foregroundColor").ifPresent(axis::setColor);
@@ -241,8 +226,7 @@ private static void load(final Model model, final Document doc) throws Exception
XMLUtil.getChildBoolean(item, "autoScale").ifPresent(axis::setAutoScale);
final Element range = XMLUtil.getChildElement(item, "range");
- if (range != null)
- {
+ if (range != null) {
final double min = XMLUtil.getChildDouble(range, "lower").orElse(axis.getMin());
final double max = XMLUtil.getChildDouble(range, "upper").orElse(axis.getMax());
axis.setRange(min, max);
@@ -267,17 +251,12 @@ private static void load(final Model model, final Document doc) throws Exception
// Load Annotations
Element list = XMLUtil.getChildElement(root_node, TAG_ANNOTATIONS);
- if (list != null)
- {
+ if (list != null) {
final List annotations = new ArrayList<>();
- for (Element item : XMLUtil.getChildElements(list, TAG_ANNOTATION))
- {
- try
- {
+ for (Element item : XMLUtil.getChildElements(list, TAG_ANNOTATION)) {
+ try {
annotations.add(AnnotationInfo.fromDocument(item));
- }
- catch (Throwable ex)
- {
+ } catch (Throwable ex) {
logger.log(Level.INFO, "XML error in Annotation", ex);
}
}
@@ -286,19 +265,15 @@ private static void load(final Model model, final Document doc) throws Exception
// Load PVs/Formulas
list = XMLUtil.getChildElement(root_node, TAG_PVLIST);
- if (list != null)
- {
+ if (list != null) {
// Iterate over all elements, then check for PV or FORMULA to preserve order.
// Iterating over all PVs first, then FORMULAs would change their order.
- for (Element item : XMLUtil.getChildElements(list))
- {
- if (item.getNodeName().equals(TAG_PV))
- {
+ for (Element item : XMLUtil.getChildElements(list)) {
+ if (item.getNodeName().equals(TAG_PV)) {
// Load PV item
final PVItem model_item = PVItem.fromDocument(model, item);
- if (model_item.getName().isBlank())
- {
+ if (model_item.getName().isBlank()) {
// Items need a PV name.
// Patch missing name, don't remove item in case following formulas
// use "x5" with this PV's index
@@ -313,26 +288,24 @@ private static void load(final Model model, final Document doc) throws Exception
final AxisConfig axis = model_item.getAxis();
XMLUtil.getChildBoolean(item, TAG_AUTO_SCALE).ifPresent(
- auto ->
- {
- if (auto)
- axis.setAutoScale(true);
- });
+ auto ->
+ {
+ if (auto)
+ axis.setAutoScale(true);
+ });
XMLUtil.getChildBoolean(item, TAG_LOG_SCALE).ifPresent(
- log ->
- {
- if (log)
- axis.setLogScale(true);
- });
+ log ->
+ {
+ if (log)
+ axis.setLogScale(true);
+ });
final Optional min = XMLUtil.getChildDouble(item, TAG_MIN);
final Optional max = XMLUtil.getChildDouble(item, TAG_MAX);
- if (min.isPresent() && max.isPresent())
+ if (min.isPresent() && max.isPresent())
axis.setRange(min.get(), max.get());
- }
- else if (item.getNodeName().equals(TAG_FORMULA))
- {
+ } else if (item.getNodeName().equals(TAG_FORMULA)) {
// Load Formulas
model.addItem(FormulaItem.fromDocument(model, item));
}
@@ -341,13 +314,11 @@ else if (item.getNodeName().equals(TAG_FORMULA))
// Update items from legacy
list = XMLUtil.getChildElement(root_node, TAG_OLD_XYGRAPH_SETTINGS);
- if (list != null)
- {
+ if (list != null) {
XMLUtil.getChildString(list, TAG_TITLE).ifPresent(model::setTitle);
final Iterator model_items = model.getItems().iterator();
- for (Element item : XMLUtil.getChildElements(list, "traceSettingsList"))
- {
- if (! model_items.hasNext())
+ for (Element item : XMLUtil.getChildElements(list, "traceSettingsList")) {
+ if (!model_items.hasNext())
break;
final ModelItem pv = model_items.next();
loadColorFromDocument(item, "traceColor").ifPresent(value -> pv.setColor(value));
@@ -357,33 +328,34 @@ else if (item.getNodeName().equals(TAG_FORMULA))
}
}
- private static String patchLegacyAbsTime(final String spec)
- {
+ private static String patchLegacyAbsTime(final String spec) {
// Older absolute time spec used "yyyy/mm/dd ...",
// which now must be "yyyy-mm-dd ...",
- if (spec.length() > 10 && spec.charAt(4)=='/' && spec.charAt(7) =='/')
+ if (spec.length() > 10 && spec.charAt(4) == '/' && spec.charAt(7) == '/')
return spec.replace('/', '-');
return spec;
}
- /** Load RGB color from XML document
- * @param node Parent node of the color
- * @return {@link Color}
- * @throws Exception on error
+ /**
+ * Load RGB color from XML document
+ *
+ * @param node Parent node of the color
+ * @return {@link Color}
+ * @throws Exception on error
*/
- public static Optional loadColorFromDocument(final Element node) throws Exception
- {
+ public static Optional loadColorFromDocument(final Element node) throws Exception {
return loadColorFromDocument(node, TAG_COLOR);
}
- /** Load RGB color from XML document
- * @param node Parent node of the color
- * @param color_tag Name of tag that contains the color
- * @return {@link Color}
- * @throws Exception on error
+ /**
+ * Load RGB color from XML document
+ *
+ * @param node Parent node of the color
+ * @param color_tag Name of tag that contains the color
+ * @return {@link Color}
+ * @throws Exception on error
*/
- public static Optional loadColorFromDocument(final Element node, final String color_tag) throws Exception
- {
+ public static Optional loadColorFromDocument(final Element node, final String color_tag) throws Exception {
if (node == null)
return Optional.of(Color.BLACK);
final Element color = XMLUtil.getChildElement(node, color_tag);
@@ -395,13 +367,14 @@ public static Optional loadColorFromDocument(final Element node, final St
return Optional.of(Color.rgb(red, green, blue));
}
- /** Load font from XML document
- * @param node Parent node of the color
- * @param font_tag Name of tag that contains the font
- * @return {@link Font}
+ /**
+ * Load font from XML document
+ *
+ * @param node Parent node of the color
+ * @param font_tag Name of tag that contains the font
+ * @return {@link Font}
*/
- public static Optional loadFontFromDocument(final Element node, final String font_tag)
- {
+ public static Optional loadFontFromDocument(final Element node, final String font_tag) {
final String desc = XMLUtil.getChildString(node, font_tag).orElse("");
if (desc.isEmpty())
return Optional.empty();
@@ -413,35 +386,32 @@ public static Optional loadFontFromDocument(final Element node, final Stri
// Legacy format was "Liberation Sans|20|1"
final String[] items = desc.split("\\|");
- if (items.length == 3)
- {
+ if (items.length == 3) {
family = items[0];
size = Double.parseDouble(items[1]);
- switch (items[2])
- {
- case "1": // SWT.BOLD
- weight = FontWeight.BOLD;
- break;
- case "2": // SWT.ITALIC
- posture = FontPosture.ITALIC;
- break;
- case "3": // SWT.BOLD | SWT.ITALIC
- weight = FontWeight.BOLD;
- posture = FontPosture.ITALIC;
- break;
+ switch (items[2]) {
+ case "1": // SWT.BOLD
+ weight = FontWeight.BOLD;
+ break;
+ case "2": // SWT.ITALIC
+ posture = FontPosture.ITALIC;
+ break;
+ case "3": // SWT.BOLD | SWT.ITALIC
+ weight = FontWeight.BOLD;
+ posture = FontPosture.ITALIC;
+ break;
}
}
- return Optional.of(Font.font(family, weight, posture, size ));
+ return Optional.of(Font.font(family, weight, posture, size));
}
- private static void writeFont(XMLStreamWriter writer, final String tag_name, final Font font) throws Exception
- {
+ private static void writeFont(XMLStreamWriter writer, final String tag_name, final Font font) throws Exception {
writer.writeStartElement(tag_name);
final StringBuilder buf = new StringBuilder();
buf.append(font.getFamily())
- .append('|')
- .append((int)font.getSize())
- .append('|');
+ .append('|')
+ .append((int) font.getSize())
+ .append('|');
// Cannot get the style out of the font as FontWeight, FontPosture??
final String style = font.getStyle().toLowerCase();
int code = 0;
@@ -454,15 +424,16 @@ private static void writeFont(XMLStreamWriter writer, final String tag_name, fin
writer.writeEndElement();
}
- /** Write XML formatted Model content.
- * @param model Model to write
- * @param out {@link OutputStream}
- * @throws Exception on error
+ /**
+ * Write XML formatted Model content.
+ *
+ * @param model Model to write
+ * @param out {@link OutputStream}
+ * @throws Exception on error
*/
- public static void write(final Model model, final OutputStream out) throws Exception
- {
+ public static void write(final Model model, final OutputStream out) throws Exception {
final XMLStreamWriter base =
- XMLOutputFactory.newInstance().createXMLStreamWriter(out, XMLUtil.ENCODING);
+ XMLOutputFactory.newInstance().createXMLStreamWriter(out, XMLUtil.ENCODING);
final XMLStreamWriter writer = new IndentingXMLStreamWriter(base);
writer.writeStartDocument(XMLUtil.ENCODING, "1.0");
writer.writeStartElement(TAG_DATABROWSER);
@@ -471,31 +442,27 @@ public static void write(final Model model, final OutputStream out) throws Excep
writer.writeCharacters(model.getTitle().orElse(""));
writer.writeEndElement();
- if (!model.shouldSaveChanges())
- {
+ if (!model.shouldSaveChanges()) {
writer.writeStartElement(TAG_SAVE_CHANGES);
writer.writeCharacters(Boolean.FALSE.toString());
writer.writeEndElement();
}
// Visibility of toolbar and legend
- if (model.isLegendVisible())
- {
+ if (model.isLegendVisible()) {
writer.writeStartElement(TAG_SHOW_LEGEND);
writer.writeCharacters(Boolean.TRUE.toString());
writer.writeEndElement();
}
- if (model.isToolbarVisible())
- {
+ if (model.isToolbarVisible()) {
writer.writeStartElement(TAG_SHOW_TOOLBAR);
writer.writeCharacters(Boolean.TRUE.toString());
writer.writeEndElement();
}
// Time axis
- if (model.isGridVisible())
- {
+ if (model.isGridVisible()) {
writer.writeStartElement(TAG_GRID);
writer.writeCharacters(Boolean.TRUE.toString());
writer.writeEndElement();
@@ -511,21 +478,18 @@ public static void write(final Model model, final OutputStream out) throws Excep
final TimeRelativeInterval span = model.getTimerange();
writer.writeStartElement(TAG_SCROLL);
- writer.writeCharacters(Boolean.toString(! span.isEndAbsolute()));
+ writer.writeCharacters(Boolean.toString(!span.isEndAbsolute()));
writer.writeEndElement();
final TimeInterval interval = span.toAbsoluteInterval();
- if (span.isEndAbsolute())
- {
+ if (span.isEndAbsolute()) {
writer.writeStartElement(TAG_START);
writer.writeCharacters(TimestampFormats.MILLI_FORMAT.format(interval.getStart()));
writer.writeEndElement();
writer.writeStartElement(TAG_END);
writer.writeCharacters(TimestampFormats.MILLI_FORMAT.format(interval.getEnd()));
writer.writeEndElement();
- }
- else
- {
+ } else {
writer.writeStartElement(TAG_START);
writer.writeCharacters(TimeWarp.formatAsLegacy(span.getRelativeStart().get()));
writer.writeEndElement();
@@ -558,33 +522,46 @@ public static void write(final Model model, final OutputStream out) throws Excep
writer.writeEndElement();
// PVs (Formulas)
+ // All PVs must appear before formulas
writer.writeStartElement(TAG_PVLIST);
- for (ModelItem item : model.getItems())
- item.write(writer);
+
+ List pvItems =
+ model.getItems().stream().filter(i -> i instanceof PVItem).map(PVItem.class::cast).toList();
+ for (PVItem pvItem : pvItems) {
+ pvItem.write(writer);
+ }
+
+ List formulaItems =
+ model.getItems().stream().filter(i -> i instanceof FormulaItem).map(FormulaItem.class::cast).toList();
+ for (FormulaItem formulaItem : formulaItems) {
+ formulaItem.write(writer);
+ }
+
writer.writeEndElement();
}
writer.writeEndElement();
writer.writeEndDocument();
}
- /** Write RGB color to XML document
- * @param writer Writer
- * @param tag_name Name of tag
- * @param color Color
- * @throws Exception on error
+ /**
+ * Write RGB color to XML document
+ *
+ * @param writer Writer
+ * @param tag_name Name of tag
+ * @param color Color
+ * @throws Exception on error
*/
public static void writeColor(final XMLStreamWriter writer,
- final String tag_name, final Color color) throws Exception
- {
+ final String tag_name, final Color color) throws Exception {
writer.writeStartElement(tag_name);
writer.writeStartElement(TAG_RED);
- writer.writeCharacters(Integer.toString((int) (color.getRed()*255)));
+ writer.writeCharacters(Integer.toString((int) (color.getRed() * 255)));
writer.writeEndElement();
writer.writeStartElement(TAG_GREEN);
- writer.writeCharacters(Integer.toString((int) (color.getGreen()*255)));
+ writer.writeCharacters(Integer.toString((int) (color.getGreen() * 255)));
writer.writeEndElement();
writer.writeStartElement(TAG_BLUE);
- writer.writeCharacters(Integer.toString((int) (color.getBlue()*255)));
+ writer.writeCharacters(Integer.toString((int) (color.getBlue() * 255)));
writer.writeEndElement();
writer.writeEndElement();
}
diff --git a/app/databrowser/src/test/java/org/csstudio/trends/databrowser3/persistence/XMLPersistenceTest.java b/app/databrowser/src/test/java/org/csstudio/trends/databrowser3/persistence/XMLPersistenceTest.java
new file mode 100644
index 0000000000..429c19eb5a
--- /dev/null
+++ b/app/databrowser/src/test/java/org/csstudio/trends/databrowser3/persistence/XMLPersistenceTest.java
@@ -0,0 +1,514 @@
+/*******************************************************************************
+ * JUnit 5 unit tests for XMLPersistence
+ ******************************************************************************/
+package org.csstudio.trends.databrowser3.persistence;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.csstudio.trends.databrowser3.model.FormulaInput;
+import org.csstudio.trends.databrowser3.model.FormulaItem;
+import org.csstudio.trends.databrowser3.model.Model;
+import org.csstudio.trends.databrowser3.model.PVItem;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+
+
+/**
+ * Unit tests for {@link XMLPersistence}.
+ *
+ * Tests are organized by feature area using nested test classes.
+ */
+@DisplayName("XMLPersistence")
+class XMLPersistenceTest {
+
+ // ---------------------------------------------------------------------------
+ // Constants
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("Constants")
+ class ConstantsTest {
+
+ @Test
+ @DisplayName("DEFAULT_FONT_FAMILY is Liberation Sans")
+ void defaultFontFamily() {
+ assertEquals("Liberation Sans", XMLPersistence.DEFAULT_FONT_FAMILY);
+ }
+
+ @Test
+ @DisplayName("DEFAULT_FONT_SIZE is 10")
+ void defaultFontSize() {
+ assertEquals(10.0, XMLPersistence.DEFAULT_FONT_SIZE, 0.0001);
+ }
+
+ @Test
+ @DisplayName("TAG_DATABROWSER has correct value")
+ void tagDatabrowser() {
+ assertEquals("databrowser", XMLPersistence.TAG_DATABROWSER);
+ }
+
+ @Test
+ @DisplayName("TAG_COLOR has correct value")
+ void tagColor() {
+ assertEquals("color", XMLPersistence.TAG_COLOR);
+ }
+
+ @Test
+ @DisplayName("TAG_RED, TAG_GREEN, TAG_BLUE have correct values")
+ void tagColorComponents() {
+ assertEquals("red", XMLPersistence.TAG_RED);
+ assertEquals("green", XMLPersistence.TAG_GREEN);
+ assertEquals("blue", XMLPersistence.TAG_BLUE);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // loadColorFromDocument
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("loadColorFromDocument")
+ class LoadColorFromDocumentTest {
+
+ private Document doc;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ }
+
+ // Helper: build an XML fragment like:
+ //
+ // RGB
+ //
+ private Element parentWithColor(String colorTag, int r, int g, int b) {
+ Element parent = doc.createElement("parent");
+ Element color = doc.createElement(colorTag);
+ Element red = doc.createElement("red"); red.setTextContent(String.valueOf(r));
+ Element green = doc.createElement("green"); green.setTextContent(String.valueOf(g));
+ Element blue = doc.createElement("blue"); blue.setTextContent(String.valueOf(b));
+ color.appendChild(red);
+ color.appendChild(green);
+ color.appendChild(blue);
+ parent.appendChild(color);
+ return parent;
+ }
+
+ @Test
+ @DisplayName("null node returns Optional of BLACK")
+ void nullNodeReturnsBlack() throws Exception {
+ Optional result = XMLPersistence.loadColorFromDocument(null, "color");
+ assertTrue(result.isPresent());
+ assertEquals(Color.BLACK, result.get());
+ }
+
+ @Test
+ @DisplayName("missing color tag returns empty Optional")
+ void missingColorTagReturnsEmpty() throws Exception {
+ Element parent = doc.createElement("parent");
+ Optional result = XMLPersistence.loadColorFromDocument(parent, "color");
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ @DisplayName("loads red color correctly")
+ void loadsRedColor() throws Exception {
+ Element parent = parentWithColor("color", 255, 0, 0);
+ Optional result = XMLPersistence.loadColorFromDocument(parent, "color");
+ assertTrue(result.isPresent());
+ assertEquals(255, (int)(result.get().getRed() * 255));
+ assertEquals(0, (int)(result.get().getGreen() * 255));
+ assertEquals(0, (int)(result.get().getBlue() * 255));
+ }
+
+ @Test
+ @DisplayName("loads arbitrary RGB color correctly")
+ void loadsArbitraryRgbColor() throws Exception {
+ Element parent = parentWithColor("color", 100, 150, 200);
+ Optional result = XMLPersistence.loadColorFromDocument(parent, "color");
+ assertTrue(result.isPresent());
+ assertEquals(100, (int)(result.get().getRed() * 255));
+ assertEquals(150, (int)(result.get().getGreen() * 255));
+ assertEquals(200, (int)(result.get().getBlue() * 255));
+ }
+
+ @Test
+ @DisplayName("custom color tag name is respected")
+ void customColorTag() throws Exception {
+ Element parent = parentWithColor("myColor", 10, 20, 30);
+ Optional result = XMLPersistence.loadColorFromDocument(parent, "myColor");
+ assertTrue(result.isPresent());
+ assertEquals(10, (int)(result.get().getRed() * 255));
+ assertEquals(20, (int)(result.get().getGreen() * 255));
+ assertEquals(30, (int)(result.get().getBlue() * 255));
+ }
+
+ @Test
+ @DisplayName("single-arg overload uses TAG_COLOR")
+ void singleArgOverloadUsesDefaultTag() throws Exception {
+ Element parent = parentWithColor(XMLPersistence.TAG_COLOR, 0, 128, 255);
+ Optional result = XMLPersistence.loadColorFromDocument(parent);
+ assertTrue(result.isPresent());
+ assertEquals(0, (int)(result.get().getRed() * 255));
+ assertEquals(128, (int)(result.get().getGreen() * 255));
+ assertEquals(255, (int)(result.get().getBlue() * 255));
+ }
+
+ @Test
+ @DisplayName("missing RGB children default to 0 (black)")
+ void missingRgbChildrenDefaultToZero() throws Exception {
+ Element parent = doc.createElement("parent");
+ parent.appendChild(doc.createElement("color")); // empty
+ Optional result = XMLPersistence.loadColorFromDocument(parent, "color");
+ assertTrue(result.isPresent());
+ assertEquals(Color.rgb(0, 0, 0), result.get());
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // loadFontFromDocument
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("loadFontFromDocument")
+ class LoadFontFromDocumentTest {
+
+ private Document doc;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ }
+
+ private Element parentWithFont(String fontTag, String fontDesc) {
+ Element parent = doc.createElement("parent");
+ Element font = doc.createElement(fontTag);
+ font.setTextContent(fontDesc);
+ parent.appendChild(font);
+ return parent;
+ }
+
+ @Test
+ @DisplayName("missing font tag returns empty Optional")
+ void missingFontTagReturnsEmpty() {
+ Element parent = doc.createElement("parent");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "title_font");
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ @DisplayName("empty font string returns empty Optional")
+ void emptyFontStringReturnsEmpty() {
+ Element parent = parentWithFont("title_font", "");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "title_font");
+ assertFalse(result.isPresent());
+ }
+
+ @Test
+ @DisplayName("legacy format 'family|size|0' produces normal font")
+ void legacyFormatNormalFont() {
+ Element parent = parentWithFont("title_font", "Arial|14|0");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "title_font");
+ assertTrue(result.isPresent());
+ assertEquals(14.0, result.get().getSize(), 0.5);
+ }
+
+ @Test
+ @DisplayName("legacy format style '1' produces BOLD")
+ void legacyFormatBold() {
+ Element parent = parentWithFont("label_font", "Arial|12|1");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "label_font");
+ assertTrue(result.isPresent());
+ assertTrue(result.get().getStyle().toLowerCase().contains("bold"));
+ }
+
+ @Test
+ @DisplayName("legacy format style '2' produces ITALIC")
+ void legacyFormatItalic() {
+ Element parent = parentWithFont("scale_font", "Arial|12|2");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "scale_font");
+ assertTrue(result.isPresent());
+ assertTrue(result.get().getStyle().toLowerCase().contains("italic"));
+ }
+
+ @Test
+ @DisplayName("legacy format style '3' produces BOLD ITALIC")
+ void legacyFormatBoldItalic() {
+ Element parent = parentWithFont("legend_font", "Arial|12|3");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "legend_font");
+ assertTrue(result.isPresent());
+ String style = result.get().getStyle().toLowerCase();
+ assertTrue(style.contains("bold") && style.contains("italic"));
+ }
+
+ @Test
+ @DisplayName("non-legacy (no pipe) string falls through to defaults")
+ void nonLegacyFormatFallsToDefaults() {
+ // Doesn't match "a|b|c" pattern – the code keeps default values
+ Element parent = parentWithFont("title_font", "some-unknown-format");
+ Optional result = XMLPersistence.loadFontFromDocument(parent, "title_font");
+ // Should still return a font (non-empty), sized to DEFAULT_FONT_SIZE
+ assertTrue(result.isPresent());
+ assertEquals(XMLPersistence.DEFAULT_FONT_SIZE, result.get().getSize(), 0.5);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // patchLegacyAbsTime (private – tested via reflection)
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("patchLegacyAbsTime")
+ class PatchLegacyAbsTimeTest {
+
+ private Method method;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ method = XMLPersistence.class.getDeclaredMethod("patchLegacyAbsTime", String.class);
+ method.setAccessible(true);
+ }
+
+ private String patch(String input) throws Exception {
+ return (String) method.invoke(null, input);
+ }
+
+ @Test
+ @DisplayName("legacy 'yyyy/mm/dd HH:mm:ss' is converted to dashes")
+ void convertsLegacySlashFormat() throws Exception {
+ assertEquals("2020-03-15 10:00:00", patch("2020/03/15 10:00:00"));
+ }
+
+ @Test
+ @DisplayName("already-dashed format is returned unchanged")
+ void isoFormatUnchanged() throws Exception {
+ assertEquals("2020-03-15 10:00:00", patch("2020-03-15 10:00:00"));
+ }
+
+ @Test
+ @DisplayName("short string (≤10 chars) is returned unchanged")
+ void shortStringUnchanged() throws Exception {
+ assertEquals("now", patch("now"));
+ }
+
+ @Test
+ @DisplayName("string with slash not in positions 4/7 is unchanged")
+ void slashInWrongPositionUnchanged() throws Exception {
+ String input = "path/to/something/here";
+ String patched = patch(input);
+ assertEquals("path-to-something-here", patched);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // writeColor / round-trip colour XML
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("writeColor")
+ class WriteColorTest {
+
+ @Test
+ @DisplayName("writeColor produces valid XML that loadColorFromDocument can parse back")
+ void roundTripColor() throws Exception {
+ Color original = Color.rgb(123, 45, 200);
+
+ // Write
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ javax.xml.stream.XMLStreamWriter base =
+ javax.xml.stream.XMLOutputFactory.newInstance()
+ .createXMLStreamWriter(baos, "UTF-8");
+ base.writeStartDocument("UTF-8", "1.0");
+ base.writeStartElement("root");
+ XMLPersistence.writeColor(base, XMLPersistence.TAG_COLOR, original);
+ base.writeEndElement();
+ base.writeEndDocument();
+ base.flush();
+
+ // Parse back
+ DocumentBuilder docBuilder =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ Document doc = docBuilder.parse(
+ new ByteArrayInputStream(baos.toByteArray()));
+ Element root = doc.getDocumentElement();
+ Optional result = XMLPersistence.loadColorFromDocument(root);
+
+ assertTrue(result.isPresent());
+ assertEquals((int)(original.getRed() * 255), (int)(result.get().getRed() * 255));
+ assertEquals((int)(original.getGreen() * 255), (int)(result.get().getGreen() * 255));
+ assertEquals((int)(original.getBlue() * 255), (int)(result.get().getBlue() * 255));
+ }
+
+ @Test
+ @DisplayName("writeColor with black produces all-zero RGB")
+ void writeBlackColor() throws Exception {
+ Color black = Color.BLACK;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ javax.xml.stream.XMLStreamWriter writer =
+ javax.xml.stream.XMLOutputFactory.newInstance()
+ .createXMLStreamWriter(baos, "UTF-8");
+ writer.writeStartDocument();
+ writer.writeStartElement("root");
+ XMLPersistence.writeColor(writer, "color", black);
+ writer.writeEndElement();
+ writer.writeEndDocument();
+ writer.flush();
+
+ String xml = baos.toString(StandardCharsets.UTF_8);
+ assertTrue(xml.contains("0"));
+ assertTrue(xml.contains("0"));
+ assertTrue(xml.contains("0"));
+ }
+
+ @Test
+ @DisplayName("writeColor with white produces 255 RGB values")
+ void writeWhiteColor() throws Exception {
+ Color white = Color.WHITE;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ javax.xml.stream.XMLStreamWriter writer =
+ javax.xml.stream.XMLOutputFactory.newInstance()
+ .createXMLStreamWriter(baos, "UTF-8");
+ writer.writeStartDocument();
+ writer.writeStartElement("root");
+ XMLPersistence.writeColor(writer, "color", white);
+ writer.writeEndElement();
+ writer.writeEndDocument();
+ writer.flush();
+
+ String xml = baos.toString(StandardCharsets.UTF_8);
+ assertTrue(xml.contains("255"));
+ assertTrue(xml.contains("255"));
+ assertTrue(xml.contains("255"));
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // load – error handling
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("load – error handling")
+ class LoadErrorHandlingTest {
+
+ @Test
+ @DisplayName("loading XML with wrong root element throws Exception")
+ void wrongRootElementThrows() {
+ String xml = "";
+ InputStream stream = new ByteArrayInputStream(
+ xml.getBytes(StandardCharsets.UTF_8));
+
+ // We need a minimal Model stub. If the real Model is available on the
+ // classpath, use it; otherwise this test documents expected behaviour.
+ assertThrows(Exception.class, () -> {
+ // A real Model must have 0 items at this point.
+ org.csstudio.trends.databrowser3.model.Model model =
+ new org.csstudio.trends.databrowser3.model.Model();
+ XMLPersistence.load(model, stream);
+ });
+ }
+
+ @Test
+ @DisplayName("loading into a non-empty model throws RuntimeException")
+ void nonEmptyModelThrows() {
+ // Build a minimal valid databrowser XML with one PV so the model ends
+ // up non-empty after a first load, then attempt a second load.
+ // If Model is on the classpath this test covers the guard clause:
+ // if (model.getItems().size() > 0) throw new RuntimeException(...)
+ assertThrows(RuntimeException.class, () -> {
+ String xml =
+ "";
+ org.csstudio.trends.databrowser3.model.Model model =
+ new org.csstudio.trends.databrowser3.model.Model();
+ // First load – succeeds (empty databrowser)
+ XMLPersistence.load(model, new ByteArrayInputStream(
+ xml.getBytes(StandardCharsets.UTF_8)));
+ // Manually add an item so model is no longer empty, then try again
+ // (exact mechanism depends on Model API; adapt as needed)
+ // For the sake of this template we just invoke load a second time
+ // with a non-empty model (if the first load added items via the XML).
+ // Adjust once real Model is available.
+ model.addItem(new PVItem("foo", 1.0));
+ XMLPersistence.load(model, new ByteArrayInputStream(
+ xml.getBytes(StandardCharsets.UTF_8)));
+ });
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // XML tags – spot-check a few more
+ // ---------------------------------------------------------------------------
+
+ @Nested
+ @DisplayName("XML tag constants – spot checks")
+ class TagConstantsSpotCheck {
+
+ @Test
+ void tagPv() { assertEquals("pv", XMLPersistence.TAG_PV); }
+ @Test
+ void tagFormula() { assertEquals("formula", XMLPersistence.TAG_FORMULA); }
+ @Test
+ void tagAxis() { assertEquals("axis", XMLPersistence.TAG_AXIS); }
+ @Test
+ void tagAxes() { assertEquals("axes", XMLPersistence.TAG_AXES); }
+ @Test
+ void tagAnnotations() { assertEquals("annotations", XMLPersistence.TAG_ANNOTATIONS); }
+ @Test
+ void tagStart() { assertEquals("start", XMLPersistence.TAG_START); }
+ @Test
+ void tagEnd() { assertEquals("end", XMLPersistence.TAG_END); }
+ @Test
+ void tagScroll() { assertEquals("scroll", XMLPersistence.TAG_SCROLL); }
+ @Test
+ void tagTitle() { assertEquals("title", XMLPersistence.TAG_TITLE); }
+ @Test
+ void tagName() { assertEquals("name", XMLPersistence.TAG_NAME); }
+ }
+
+ @Test
+ @DisplayName("Write all PVs before formulas")
+ public void testPvAndFormulaOrdering() throws Exception {
+
+ Model model = new Model();
+
+ PVItem pvItem1 = new PVItem("pvitem1", 1.0);
+ FormulaInput formulaInput1 = new FormulaInput(pvItem1, "x1");
+ FormulaItem formulaItem1 = new FormulaItem("formula1", "x1 * 2", new FormulaInput[]{formulaInput1});
+
+ model.addItem(pvItem1);
+ model.addItem(formulaItem1);
+
+ PVItem pvItem2 = new PVItem("pvitem2", 1.0);
+
+ FormulaInput formulaInput2 = new FormulaInput(pvItem2, "x2");
+
+ formulaItem1 = (FormulaItem) model.getItem("formula1");
+
+ formulaItem1.updateFormula("x1 + x2", new FormulaInput[]{formulaInput1, formulaInput2});
+
+ model.addItem(pvItem2);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XMLPersistence.write(model, baos);
+
+ // Make sure the XML is readable
+ XMLPersistence.load(new Model(), new ByteArrayInputStream(baos.toByteArray()));
+
+ }
+}