// SPDX-FileCopyrightText: 2020 Roberto Posenato // // SPDX-License-Identifier: LGPL-3.0-or-later package it.univr.di.cstnu.util; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.univr.di.cstnu.graph.*; import it.univr.di.cstnu.gui.NodePositions; import it.univr.di.cstnu.gui.StoredNodePositions; import it.univr.di.cstnu.gui.layout.FruchtermanReingoldLayout; import it.univr.di.labeledvalue.ALabelAlphabet.ALetter; import it.univr.di.labeledvalue.Constants; import it.univr.di.labeledvalue.Label; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.awt.geom.Rectangle2D; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; /** * Utility class for converting a (C)STN(U) file in Luke format to GraphML format. * * @author posenato * @version $Rev: 732 $ */ public class Luke2GraphML { /** * Result of parsing one Luke file, including the type declared in its header. */ private record ParsedNetwork(@Nonnull TemporalConstraintNetworkType networkType, @Nonnull DenseTCGraph graph) {} /** * class logger */ static final Logger LOG = Logger.getLogger(Luke2GraphML.class.getName()); /** * Version */ // static final String VERSIONandDATE = "1.1, March, 11 2016"; // static final String VERSIONandDATE = "1.2, May, 30 2021";//just tweaking // static final String VERSIONandDATE = "1.3, January, 04 2024";//extending to STNU static final String VERSIONandDATE = "1.4, August, 13 2026";//extending to ESTNU waits /** * Pattern for edges in CSTN file. */ private static final String regExEdgeCSTN = "EDGE \\(|,|\\): "; private static final Pattern PATTERN4EDGECSTN = Pattern.compile(regExEdgeCSTN); @SuppressWarnings("RegExpRedundantEscape") private static final Pattern PATTERN4EDGECSTN1 = Pattern.compile("<|,\\[|\\]>"); private static final String regExEdgeSTNU = "\\s"; private static final Pattern PATTERN4EDGESTNU = Pattern.compile(regExEdgeSTNU); @SuppressWarnings("RegExpRedundantEscape") private static final String regExNodeCSTN = "TP\\(|\\):[\\s\u00A0]+|,\\s+\\[|\\],\\s+|\\]"; private static final Pattern PATTERN4NODECSTN = Pattern.compile(regExNodeCSTN); private static final String regExNodeSTNU = "\\s"; private static final Pattern PATTERN4NODESTNU = Pattern.compile(regExNodeSTNU); /** * Converts a Luke plain-format file to GraphML, choosing a sibling {@code .stnu} output file. * * @param inputFile Luke plain-format input * * @return the generated GraphML file * * @throws IOException if the input is malformed or conversion cannot be written */ @Nonnull public static File convert(@Nonnull final File inputFile) throws IOException { final String inputName = inputFile.getAbsolutePath(); final String outputName = inputName.replaceFirst("(?i)\\.plainstnu$", ".stnu"); final File outputFile = outputName.equals(inputName) ? new File(inputName + ".stnu") : new File(outputName); convert(inputFile, outputFile); return outputFile; } /** * Converts a Luke plain-format file to GraphML at the requested destination. * * @param inputFile Luke plain-format input * @param outputFile GraphML destination * * @throws IOException if the input is malformed or conversion cannot be written */ public static void convert(@Nonnull final File inputFile, @Nonnull final File outputFile) throws IOException { final ParsedNetwork parsed = parse(inputFile); writeGraphML(parsed.graph(), outputFile); } /** * Converts a Luke file only when its header declares an STNU network. * * @param inputFile Luke plain-format input * @param outputFile GraphML destination * * @throws IOException if the input is malformed or declares another network kind */ public static void convertSTNU(@Nonnull final File inputFile, @Nonnull final File outputFile) throws IOException { final ParsedNetwork parsed = parse(inputFile); if (parsed.networkType() != TemporalConstraintNetworkType.STNU) { throw new IOException("The Luke file declares " + parsed.networkType() + "; an STNU file is required."); } writeGraphML(parsed.graph(), outputFile); } /** * @param args a CSTN file in Luke's format. * * @throws IOException if any file cannot be read or written */ public static void main(final String[] args) throws IOException { // System.out.println(Arrays.toString("<694,[A(-E)]>".split("<|,\\[|\\]>"))); // System.out.println("<694,[A(-E)]>".split("<|,\\[|\\]>")[2].replace("(", // "").replace(")", "").replace("-", "¬")); LOG.finest("Start..."); System.out.println("Start of execution..."); final Checker tester = new Checker(); final Luke2GraphML converter = new Luke2GraphML(); if (!converter.manageParameters(args)) { return; } LOG.finest("Parameters ok!"); System.out.println("Parameters ok!"); if (converter.versionReq) { System.out.print(tester.getClass().getName() + " " + VERSIONandDATE + ". Academic and non-commercial use only.\n" + "Copyright © 2016-2022 Roberto Posenato"); return; } final ParsedNetwork parsed = parse(converter.inputTNFile); converter.networkType = parsed.networkType(); converter.prepareFileOutput(); writeGraphML(parsed.graph(), converter.fOutput); System.out.println("DenseTCGraph saved into file " + converter.fOutput); } /** * @param reader the reader * @param line the considered line * @param g the graph * @param int2Node the map index->node * * @throws IOException if the reader has a reading problem. */ private static void addEdgeCSTN(final BufferedReader reader, final String line, final DenseTCGraph g, final Int2ObjectMap int2Node) throws IOException { final String[] nodeParts = PATTERN4EDGECSTN.split(line); // nodeParts[0] is empty! final int sI = Integer.parseInt(nodeParts[1]); final int dI = Integer.parseInt(nodeParts[2]); final LabeledNode sourceNode = int2Node.get(sI); final LabeledNode destNode = int2Node.get(dI); final CSTNEdge edge = g.getEdgeFactory().get(sourceNode.getName() + "-" + destNode.getName()); Label label; String[] labelParts; while (reader.ready()) { final String line1 = reader.readLine(); if (line1 == null) { break; } if (line1.startsWith("<*POS-INF*") || !line1.isEmpty() && line1.charAt(0) == ';' || line1.isEmpty()) { continue; } if (line1.startsWith("---")) { break; } LOG.info("line1: " + line1); labelParts = PATTERN4EDGECSTN1.split(line1); if (line1.contains("[]")) { // Empty label is not captured by split label = Label.emptyLabel; } else { label = Label.parse(toLabel(labelParts[2])); LOG.info("line1: " + line1 + ". label parts: " + Arrays.toString(labelParts) + ". label: " + label); } final int value = Integer.parseInt(labelParts[1]); edge.mergeLabeledValue(label, value); } if (edge.getLabeledValues().size() > 0) { g.addEdge(edge, sourceNode, destNode); } } /** * @param reader the reader * @param line the considered line * @param g the graph * * @throws IOException if the reader has a reading problem. */ @SuppressWarnings("unchecked") private static void addEdgeSTNU(@Nonnull final BufferedReader reader, @Nonnull String line, @Nonnull final DenseTCGraph g, final boolean estnu) throws IOException { // Lines for ordinary edges are like // # Ordinary Edges // a -1 aa // y 1 w // ... // # Contingent Links //a 5 9 c //... //EOF //check if it is the right start if (!line.startsWith("# Ordinary Edges")) { LOG.warning("Line %s is not the start of the edges section. It should be '# Ordinary Edges'.".formatted(line)); return; } while ((line = reader.readLine()) != null && !line.startsWith("# Contingent Links")) { if (line.trim().isEmpty()) {continue;} final String[] edgeParts = PATTERN4EDGESTNU.split(line.replace("'", "")); if (LOG.isLoggable(Level.FINEST)) { LOG.finest("Edge parts: %s. Length: %d".formatted(Arrays.toString(edgeParts), edgeParts.length)); } final String sourceNodeName = edgeParts[0]; final String destNodeName = edgeParts[2]; final int value = Integer.parseInt(edgeParts[1]); if (value == Constants.INT_NULL || value == Constants.INT_POS_INFINITE || value == Constants.INT_NEG_INFINITE) { continue; } final E edge = g.getEdgeFactory().get(sourceNodeName + "-" + destNodeName); edge.setValue(value); edge.setConstraintType(Edge.ConstraintType.requirement); g.addEdge(edge, sourceNodeName, destNodeName); if (LOG.isLoggable(Level.INFO)) { LOG.info("Added edge " + edge); } } //contingent parts if (line != null && !line.startsWith("# Contingent Links")) { return; } final DenseTCGraph g1 = (DenseTCGraph) g; while ((line = reader.readLine()) != null && !line.startsWith("# Waits")) { if (line.trim().isEmpty()) {continue;} final String[] edgeParts = PATTERN4EDGESTNU.split(line.replace("'", "")); if (LOG.isLoggable(Level.FINEST)) { LOG.finest("Edge parts: " + Arrays.toString(edgeParts) + ". Length: " + edgeParts.length); } final String sourceNodeName = edgeParts[0]; final String destNodeName = edgeParts[3]; final int lower = Integer.parseInt(edgeParts[1]); final int upper = Integer.parseInt(edgeParts[2]); if (lower < 0 || upper < 0 || upper <= lower || upper == Constants.INT_POS_INFINITE) { continue; } STNUEdge edge = g1.getEdgeFactory().get(sourceNodeName + "-" + destNodeName); edge.setValue(upper); edge.setConstraintType(Edge.ConstraintType.contingent); g1.addEdge(edge, sourceNodeName, destNodeName); if (LOG.isLoggable(Level.INFO)) { LOG.info("Added edge " + edge); } edge = g1.getEdgeFactory().get(destNodeName + "-" + sourceNodeName); edge.setValue(-lower); edge.setConstraintType(Edge.ConstraintType.contingent); g1.addEdge(edge, destNodeName, sourceNodeName); if (LOG.isLoggable(Level.INFO)) { LOG.info("Added edge " + edge); } } if (line != null && line.startsWith("# Waits")) { if (!estnu) { throw new IOException("The '# Waits' section is allowed only in an ESTNU file."); } addWaits(reader, g1); } } /** * @param line the considered line * @param g the graph * @param int2Node the map index->node */ private static void addNodeCSTN(@Nonnull final String line, @Nonnull final DenseTCGraph g, @Nonnull final Int2ObjectMap int2Node) { final String[] nodeParts = PATTERN4NODECSTN.split(line); // nodeParts[0] is empty! LOG.info("NodeParts: " + Arrays.toString(nodeParts) + ". Length: " + nodeParts.length); LOG.info("nodeParts[2]: '" + nodeParts[2] + "'");// . Leading char code: "+ Character.codePointAt(nodeParts[2], 0)); final LabeledNode node = new LabeledNode(nodeParts[2]); final boolean added = g.addVertex(node); if (!added) { throw new IllegalStateException("Node " + node + " cannot be inserted."); } if (int2Node.put(Integer.parseInt(nodeParts[1]), node) != null) { throw new IllegalStateException("Node " + node + " already inserted."); } if (nodeParts.length == 3) { node.setLabel(Label.emptyLabel); } else { node.setLabel(Label.parse(toLabel(nodeParts[3]))); } if (nodeParts.length == 5) { //It is an observation time point LOG.info("nodeParts[4]: " + nodeParts[4]); node.setObservable(nodeParts[4].trim().charAt(0)); } } /** * @param line the considered line * @param g the graph */ private static void addNodeSTNU(@Nonnull final String line, @Nonnull final DenseTCGraph g) { //line has a format like //a c u v w x y p aa final String[] nodes = PATTERN4NODESTNU.split(line.replace('\'', ' ')); if (LOG.isLoggable(Level.FINEST)) { LOG.finest("Nodes: " + Arrays.toString(nodes) + ". Length: " + nodes.length); } if (nodes.length == 0) { throw new IllegalStateException("The line of nodes %s is wrong.".formatted(line)); } for (final String nodeName : nodes) { final LabeledNode node = new LabeledNode(nodeName); final boolean added = g.addVertex(node); if (!added) { throw new IllegalStateException("Node " + node + " cannot be inserted."); } if (LOG.isLoggable(Level.INFO)) { LOG.info("Added node " + node); } } } /** * Reads ESTNU wait constraints. A wait line has the form * {@code node contingent value activation}; the historical {@code contingent:value} spelling * emitted by {@link GraphML2Luke} is accepted as well for backwards compatibility. */ private static void addWaits(@Nonnull final BufferedReader reader, @Nonnull final DenseTCGraph graph) throws IOException { String line; while ((line = reader.readLine()) != null) { final String trimmed = line.trim(); if (trimmed.isEmpty() || trimmed.startsWith(";")) {continue;} if (trimmed.startsWith("#")) { throw new IOException("Unexpected section after '# Waits': " + trimmed); } final String[] parts = trimmed.replace("'", "").split("\\s+"); final String nodeName; final String contingentName; final String valueText; final String activationName; if (parts.length == 4) { nodeName = parts[0]; contingentName = parts[1]; valueText = parts[2]; activationName = parts[3]; } else if (parts.length == 3 && parts[1].contains(":")) { nodeName = parts[0]; final int separator = parts[1].lastIndexOf(':'); contingentName = parts[1].substring(0, separator); valueText = parts[1].substring(separator + 1); activationName = parts[2]; } else { throw new IOException("Invalid ESTNU wait '" + trimmed + "': expected 'node contingent value activation'."); } if (nodeName.isEmpty() || contingentName.isEmpty() || activationName.isEmpty()) { throw new IOException("Invalid ESTNU wait '" + trimmed + "': node names must not be empty."); } final int value; try { value = Integer.parseInt(valueText); } catch (NumberFormatException ex) { throw new IOException("Invalid ESTNU wait '" + trimmed + "': the value must be an integer.", ex); } if (value >= 0) { throw new IOException("Invalid ESTNU wait '" + trimmed + "': a wait value must be negative."); } if (graph.getNode(nodeName) == null || graph.getNode(contingentName) == null || graph.getNode(activationName) == null) { throw new IOException("Invalid ESTNU wait '" + trimmed + "': every referenced node must exist."); } STNUEdge edge = graph.getEdge(nodeName + "-" + activationName); if (edge == null) { edge = graph.getEdgeFactory().get(nodeName + "-" + activationName); graph.addEdge(edge, nodeName, activationName); } edge.setLabeledValue(new ALetter(contingentName), value, true); } } /** * @param networkType the input network type * * @return the default edge implementation class corresponding to the given network type. If the conversion is not defined for the given type, it returns null. */ @Nullable static private Class getInputEdgeImplClass(@Nonnull final TemporalConstraintNetworkType networkType) { return switch (networkType) { case STN -> EdgeSupplier.DEFAULT_STN_EDGE_CLASS; case STNU -> EdgeSupplier.DEFAULT_STNU_EDGE_CLASS; case CSTN -> EdgeSupplier.DEFAULT_CSTN_EDGE_CLASS; default -> null; // case CSTNPSU -> EdgeSupplier.DEFAULT_CSTNPSU_EDGE_CLASS; // case CSTNU, PCSTNU -> EdgeSupplier.DEFAULT_CSTNU_EDGE_CLASS; }; } /** * @param line the input string. It must be an upper-case string representing one type in {@link TemporalConstraintNetworkType}. * * @return the network type converting the string 'line' * * @see TemporalConstraintNetworkType */ @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "Dm", justification = "DM_CONVERT_CASE is not relevant.") private static TemporalConstraintNetworkType getNetworkType(@Nonnull final String line) { if ("ESTNU".equalsIgnoreCase(line)) { return TemporalConstraintNetworkType.STNU; } return TemporalConstraintNetworkType.valueOf(line.toUpperCase()); } /** * Parses Luke's sections and rejects malformed/unsupported input with a checked error. */ @SuppressWarnings("unchecked") @Nonnull private static ParsedNetwork parse(@Nonnull final File inputFile) throws IOException { if (!inputFile.isFile()) { throw new IOException("Input file does not exist or is not a regular file: " + inputFile); } DenseTCGraph graph = null; TemporalConstraintNetworkType networkType = null; boolean estnu = false; final Int2ObjectMap int2Node = new Int2ObjectOpenHashMap<>(); int2Node.defaultReturnValue(null); try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { if (LOG.isLoggable(Level.FINEST)) {LOG.finest("Line: " + line);} final String trimmed = line.trim(); if (trimmed.isEmpty() || trimmed.startsWith(";") || trimmed.startsWith("--") || trimmed.startsWith("==")) {continue;} if (trimmed.startsWith("# KIND OF NETWORK")) { final String kindLine = reader.readLine(); if (kindLine == null || kindLine.trim().isEmpty()) {throw new IOException("Missing network kind after '# KIND OF NETWORK'.");} try { estnu = "ESTNU".equalsIgnoreCase(kindLine.trim()); networkType = getNetworkType(kindLine.trim()); } catch (IllegalArgumentException ex) { throw new IOException("Unknown network kind '" + kindLine.trim() + "'.", ex); } final Class edgeClass = getInputEdgeImplClass(networkType); if (edgeClass == null) {throw new IOException("Conversion of " + networkType + " is not supported.");} if (graph != null) {throw new IOException("The file contains more than one network header.");} graph = new DenseTCGraph<>(inputFile.getName() + "Converted", edgeClass); continue; } // Luke files commonly begin with descriptive comment/header lines (for example // "# Nodes and contingent links saved in random order.") before the kind marker. if (graph == null) { if (trimmed.startsWith("#")) {continue;} throw new IOException("Missing '# KIND OF NETWORK' section."); } if (trimmed.startsWith("TP(")) { addNodeCSTN(trimmed, graph, int2Node); } else if (trimmed.startsWith("EDGE")) { addEdgeCSTN(reader, trimmed, (DenseTCGraph) graph, int2Node); } else if (trimmed.startsWith("# Time-Point Names")) { final String names = reader.readLine(); if (names == null || names.trim().isEmpty()) {throw new IOException("Missing time-point names.");} addNodeSTNU(names, graph); } else if (trimmed.startsWith("# Ordinary Edges")) { addEdgeSTNU(reader, trimmed, (DenseTCGraph) graph, estnu); } else if (trimmed.startsWith("# Waits") && !estnu) { throw new IOException("The '# Waits' section is allowed only in an ESTNU file."); } } } catch (RuntimeException ex) { throw new IOException("Invalid Luke plain format in '" + inputFile.getName() + "': " + ex.getMessage(), ex); } if (graph == null) {throw new IOException("Missing '# KIND OF NETWORK' section.");} if (graph.getVertexCount() == 0) {throw new IOException("The Luke network contains no time points.");} return new ParsedNetwork(networkType, graph); } /** * @param lukeFormat the input formatted in Luke style * * @return label as string */ private static String toLabel(final String lukeFormat) { return lukeFormat.trim().replace("(", "").replace(")", "").replace("-", "¬"); } /** * Assigns coordinates (Luke has none) and writes GraphML. */ private static void writeGraphML(@Nonnull final DenseTCGraph graph, @Nonnull final File outputFile) throws IOException { final NodePositions positions = new StoredNodePositions(); new FruchtermanReingoldLayout().apply(graph, positions, new Rectangle2D.Double(0, 0, 1024, 800)); for (final LabeledNode node : graph.getVertices()) { node.setX(positions.getX(node)); node.setY(positions.getY(node)); } try { new TCGraphMLWriter().save(graph, outputFile); } catch (RuntimeException ex) { throw new IOException("Cannot write GraphML output '" + outputFile + "': " + ex.getMessage(), ex); } } /** * The input file names. Each file has to contain a CSTN tNGraph in GraphML format. */ @Argument(required = true, usage = "Input file. It has to be a (C)STN(U) tNGraph in Luke's format.", metaVar = "file_name") private String fileNameInput; /** * Type of the network. It is not possible to deduce from the filename suffix. */ private TemporalConstraintNetworkType networkType; /** * Output file where to write the CSTN in GraphML format. */ @Option(name = "-o", aliases = "--output", usage = "Output to this file in GraphML format.", metaVar = "outputFile") private File fOutput; /** * The input file. */ private File inputTNFile; /** * Software Version. */ @Option(name = "-v", aliases = "--version", usage = "Version") private boolean versionReq; /** * Simple method to manage command line parameters using the args4j library. * * @param args the input parameters * * @return false if a parameter is missing or wrong. True if every parameter is given in the right format. */ private boolean manageParameters(final String[] args) { final CmdLineParser parser = new CmdLineParser(this); try { parser.parseArgument(args); } catch (final CmdLineException e) { // If there's a problem in the command line, you'll get this exception. This will report an error message. System.err.println(e.getMessage()); System.err.println("java -cp CSTNU-.jar -cp it.univr.di.cstnu.Luke2GraphML [options...] argument."); //Print the list of available options parser.printUsage(System.err); System.err.println(); // print option sample. This is useful sometimes // System.err.println("Example: java -jar Checker.jar" + // parser.printExample(OptionHandlerFilter.REQUIRED) + // " ..."); return false; } if (LOG.isLoggable(Level.FINEST)) { LOG.finest("File name: " + fileNameInput); } inputTNFile = new File(fileNameInput); if (!inputTNFile.exists()) { System.err.println("File " + inputTNFile + " does not exit."); parser.printUsage(System.err); System.err.println(); return false; } if (LOG.isLoggable(Level.FINEST)) { LOG.finest("File: " + inputTNFile); } return true; } /** * Once the network type is known, it is possible to build a good suffix for the output file. */ @SuppressWarnings("DynamicRegexReplaceableByCompiledPattern") @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "Dm", justification = "DM_CONVERT_CASE is not " + "relevant.") private void prepareFileOutput() { if (networkType == null) { return; } final String finalSuffix = "." + networkType.name().toLowerCase(); if (fOutput == null) { final String outputFileName = fileNameInput.replaceFirst("(\\.stnu)*$", finalSuffix); fOutput = new File(outputFileName); } if (fOutput.isDirectory()) { throw new IllegalStateException("Output file is a directory."); } if (!fOutput.getName().endsWith(finalSuffix)) { final File newOutput = new File(fOutput.getAbsolutePath() + finalSuffix); if (!fOutput.exists()) { fOutput = newOutput; } } if (fOutput.exists()) { if (!fOutput.renameTo(new File(fOutput.getAbsoluteFile() + ".old"))) { final String m = "File " + fOutput.getAbsolutePath() + " cannot be renamed in .old."; LOG.severe(m); throw new IllegalStateException(m); } } } }