Commit e19ddc9e authored by Martti Käärik's avatar Martti Käärik
Browse files

ConstraintExporter that writes the constraints into the metamodel

parent 7e89d419
Loading
Loading
Loading
Loading
+3 −0
Changes for plugins/org.etsi.mts.tdl.constraints.helper/META-INF/MANIFEST.MF: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -5,3 +5,6 @@ Bundle-SymbolicName: org.etsi.mts.tdl.constraints.helper
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: European Telecommunications Standards Institute (ETSI)
Automatic-Module-Name: org.etsi.mts.tdl.constraints.helper
Require-Bundle: org.eclipse.emf.ecore,
 org.eclipse.emf.ecore.xmi
Bundle-RequiredExecutionEnvironment: JavaSE-21
+24 −0
Changes for plugins/org.etsi.mts.tdl.constraints.helper/README.md: 24 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,3 +7,27 @@ This tool is intended to help with the maintenance, implementation, and validati
The extraction and transformation relies on correct and consistent formatting, therefore it is important to address such issues first and make sure that all constraints are identified, extracted, and transformed correctly. Tracked changes may need to be accepted first. In the future, some preliminary validation to ensure that the documents are suitable for processing may be added.

The implementation of the tool makes use of the Apache POI and related libraries to process the Word versions of the specifications. 

## Command line

    ConstraintExporter --format evl|ocl|ecore --in <file> [--ecore <file>] [--exclude Class::Name,...]
                       [--import <uri>,...] [--generated-package <name>]

Classpath: the compiled classes, `lib/*` and the plug-ins of an Eclipse Modeling installation (for EMF).

- `--in` is the specification (`.docx`). For `--format ecore` it may also be a Complete OCL document
  (`.ocl`), for example one of the checked-in `model/tdl-*-constraints.ocl`.
- `--format ocl` (and `evl`) writes `resources/generated/tdl-generated-<title>.ocl`.
- `--format ecore` additionally writes the invariants into the metamodel given with `--ecore`, in the
  form the OCLinEcore code generator compiles: per class the `constraints` list of the Ecore annotation,
  and per invariant the details `<Name>` (the expression) and `<Name>$message` (the description of the
  constraint followed by `self.toString()`). `error(...)` and `warning(...)` wrappers around an
  expression are dropped. The file is written with the EMF API,
  so it keeps the formatting of the Ecore editor. The invariants that are no longer in the input are removed
   from the classes that are in the input. Operation bodies are not touched. 
   A report lists what was added, updated, left unchanged and
  removed, the classes it did not find, and the invariants that use `allInstances()`.
- `--exclude` keeps the named invariants out of the metamodel (they stay in the OCL output), for
  invariants the code generator cannot compile.

Writing the invariants into a metamodel makes the EMF generator produce a validator for it.
+106 −0
Changes for plugins/org.etsi.mts.tdl.constraints.helper/src/org/etsi/mts/tdl/constraints/helper/CompleteOclReader.java: 106 added lines, 0 removed lines.
Original line number Diff line number Diff line
package org.etsi.mts.tdl.constraints.helper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Reads the invariants of a Complete OCL document of the form produced by
 * {@link ConstraintExporter} ({@code context X} / {@code inv Name ('message' +
 * self.toString()): expression}) into {@link ConstraintRecord}s, so that
 * existing {@code .ocl} files can be converted to OCLinEcore invariants.
 */
public final class CompleteOclReader {

	private static final Pattern CONTEXT = Pattern.compile("^context\\s+(\\w+)\\s*$");
	private static final Pattern KEYWORD = Pattern.compile("(def|package|endpackage|import)\\b");
	private static final Pattern INV = Pattern.compile("^\\s*inv\\s+(\\w+)\\s*(?:\\('((?:[^'\\\\]|\\\\.)*)'\\s*\\+\\s*self\\.(?:toString|trace)\\(\\)\\))?\\s*:\\s*$"); // trace(): hand-edited form in the repo .ocl
	private static final Pattern WRAPPER = Pattern.compile("^\\s*(error|warning)\\((.*)\\)\\s*$", Pattern.DOTALL);

	private CompleteOclReader() {
	}

	public static List<ConstraintRecord> read(Path file) throws IOException {
		List<ConstraintRecord> records = new ArrayList<>();
		String context = null;
		String name = null;
		String description = null;
		String title = "";
		// The last comment seen since the last line of an expression: if an invariant follows, it is its title.
		String pendingTitle = "";
		StringBuilder expression = null;
		for (String line : Files.readAllLines(file, StandardCharsets.UTF_8)) {
			Matcher c = CONTEXT.matcher(line);
			if (c.matches()) {
				flush(records, context, name, title, description, expression);
				context = c.group(1);
				name = null;
				expression = null;
				pendingTitle = "";
				continue;
			}
			Matcher i = INV.matcher(line);
			if (i.matches()) {
				flush(records, context, name, title, description, expression);
				name = i.group(1);
				description = i.group(2) == null ? "" : i.group(2).replace("\\'", "'");
				title = pendingTitle;
				pendingTitle = "";
				expression = new StringBuilder();
				continue;
			}
			String trimmed = line.trim();
			if (trimmed.startsWith("--")) {
				pendingTitle = trimmed.substring(2).trim();
				continue;
			}
			if (KEYWORD.matcher(trimmed).lookingAt()) {
				flush(records, context, name, title, description, expression);
				name = null;
				expression = null;
				pendingTitle = "";
				continue;
			}
			// A blank line does not end an expression: long invariants are written in paragraphs. Only the next
			// invariant, context or top-level keyword does.
			if (expression != null && !trimmed.isEmpty()) {
				expression.append(expression.length() == 0 ? "" : "\n").append(trimmed);
				pendingTitle = "";
			}
		}
		flush(records, context, name, title, description, expression);
		return records;
	}

	private static void flush(List<ConstraintRecord> records, String context, String name, String title, String description, StringBuilder expression) {
		if (context == null || name == null || expression == null || expression.length() == 0) {
			return;
		}
		String text = expression.toString().trim();
		Matcher w = WRAPPER.matcher(text);
		if (w.matches() && balanced(w.group(2))) {
			text = w.group(2).trim();
		}
		records.add(new ConstraintRecord(context, name, title, description, text));
	}

	/** True when the parentheses in {@code s} are balanced (so the outer wrapper really encloses all of it). */
	private static boolean balanced(String s) {
		int depth = 0;
		for (char ch : s.toCharArray()) {
			if (ch == '(') {
				depth++;
			} else if (ch == ')') {
				if (--depth < 0) {
					return false;
				}
			}
		}
		return depth == 0;
	}
}
+58 −25
Changes for plugins/org.etsi.mts.tdl.constraints.helper/src/org/etsi/mts/tdl/constraints/helper/ConstraintExporter.java: 58 added lines, 25 removed lines.
Original line number Diff line number Diff line
@@ -25,52 +25,82 @@ public class ConstraintExporter {
	private OutputFormat format;

	enum OutputFormat {
		EVL, OCL
		EVL, OCL, ECORE
	}

	public static void main(String[] args) {
	/** Constraints extracted by the last OCL/ECORE export, in document order. */
	private final java.util.List<ConstraintRecord> records = new java.util.ArrayList<>();

	public java.util.List<ConstraintRecord> getRecords() {
		return records;
	}

	public static void main(String[] args) throws IOException {

		OutputFormat format = OutputFormat.EVL;
		String filename = "resources/es_20311901v010802e.docx";
//		filename = "resources/es_20311904v010502e.docx";
		String[] extraImports = new String[0];
		String generatedPackage = "tdl";
		String ecore = null;
		java.util.Set<String> excluded = new java.util.HashSet<>();

		for (int i = 0; i < args.length; i++) {
			if (args[i].equalsIgnoreCase("--format"))
				if (args.length > i + 1) {
			if (args[i].equalsIgnoreCase("--format") && args.length > i + 1) {
				if (args[i + 1].equalsIgnoreCase("ocl"))
					format = OutputFormat.OCL;
				else if (args[i + 1].equalsIgnoreCase("ecore"))
					format = OutputFormat.ECORE;
			}
			if (args[i].equalsIgnoreCase("--in"))
				if (args.length > i + 1) {
			if (args[i].equalsIgnoreCase("--in") && args.length > i + 1)
				filename = args[i + 1];
				}
			if (args[i].equalsIgnoreCase("--import"))
				if (args.length > i + 1) {
			if (args[i].equalsIgnoreCase("--import") && args.length > i + 1)
				extraImports = args[i + 1].split(",");
				}
			if (args[i].equalsIgnoreCase("--generated-package"))
				if (args.length > i + 1) {
			if (args[i].equalsIgnoreCase("--generated-package") && args.length > i + 1)
				generatedPackage = args[i + 1];
				}
			if (args[i].equalsIgnoreCase("--ecore") && args.length > i + 1)
				ecore = args[i + 1];
			if (args[i].equalsIgnoreCase("--exclude") && args.length > i + 1)
				excluded.addAll(java.util.Arrays.asList(args[i + 1].split(",")));
		}

		Document doc = DocumentHandler.loadDocument(new File(filename).getAbsolutePath());
		ConstraintExporter exporter = new ConstraintExporter();
		exporter.format = format;
		String title = doc.getTitle();
		title = title.replaceAll(" ", "-");
		String targetPath = "resources/generated/tdl-generated-" + title
				+ (format == OutputFormat.EVL ? ".evl" : ".ocl");
		java.util.List<ConstraintRecord> constraints;
		if (filename.endsWith(".ocl")) {
			// A Complete OCL document as input, only the ecore target makes sense.
			constraints = CompleteOclReader.read(Path.of(filename));
			System.out.println("Read " + constraints.size() + " invariants from " + filename);
		} else {
			Document doc = DocumentHandler.loadDocument(new File(filename).getAbsolutePath());
			String title = doc.getTitle().replaceAll(" ", "-");
			String targetPath = "resources/generated/tdl-generated-" + title + (format == OutputFormat.EVL ? ".evl" : ".ocl");
			exporter.exportConstraints(doc, generatedPackage, extraImports, targetPath);
			exporter.exportModel(doc, generatedPackage, extraImports, "resources/generated/tdl-generated-" + title + ".txt", true);
			constraints = exporter.getRecords();
			System.out.println("Extracted " + constraints.size() + " invariants from " + filename + " -> " + targetPath);
			// part 8
		filename = "resources/es_20311908v010302e.docx";
//		filename = "resources/es_20311904v010502e.docx";
		Document grammarDoc = DocumentHandler.loadDocument(new File(filename).getAbsolutePath(), false);
			String grammarFile = "resources/es_20311908v010302e.docx";
			if (new File(grammarFile).exists()) {
				Document grammarDoc = DocumentHandler.loadDocument(new File(grammarFile).getAbsolutePath(), false);
				exporter.exportGrammar(grammarDoc, generatedPackage, extraImports, "resources/generated/tdl-generated-" + title + "-grammar.txt", true);
			}
		}
		if (format == OutputFormat.ECORE) {
			if (ecore == null)
				throw new IllegalArgumentException("--format ecore requires --ecore <path to .ecore>");
			// Invariants the OCL code generator cannot compile (Class::Name) are kept in the Complete OCL text only (temporary)
			java.util.List<ConstraintRecord> compilable = new java.util.ArrayList<>();
			for (ConstraintRecord r : constraints) {
				if (excluded.contains(r.toString()))
					System.out.println("Excluded from ecore (Java fallback): " + r);
				else
					compilable.add(r);
			}
			EcoreConstraintEmitter.Report report = EcoreConstraintEmitter.apply(Path.of(ecore), compilable);
			System.out.println("Wrote invariants into " + ecore);
			System.out.print(report);
		}
	}

	public void exportGrammar(Document doc, String generatedPackage, String[] extraImports, String generatedConstraintsPath, boolean skipSemantics) {
		generatedGrammar = "";
@@ -209,8 +239,9 @@ public class ConstraintExporter {

	public void exportConstraints(Document doc, String generatedPackage, String[] extraImports, String generatedConstraintsPath) {
		generatedConstraints = "";
		records.clear();
		String imports = "";
		if (this.format == OutputFormat.OCL) {
		if (this.format == OutputFormat.OCL || this.format == OutputFormat.ECORE) {
			imports = "import 'http://www.etsi.org/spec/TDL/1.4.1'\n";
			for (String imp : extraImports) {
				imports += "import '" + imp + "'\n";
@@ -222,7 +253,7 @@ public class ConstraintExporter {
					+ "import \"../library/debug.eol\";\n" + "\n";
		generatedConstraints += imports;
		processDocumentModel(doc);
		if (this.format == OutputFormat.OCL)
		if (this.format == OutputFormat.OCL || this.format == OutputFormat.ECORE)
			generatedConstraints += "\n" + "endpackage" + "\n";
		Path gcp = Path.of(generatedConstraintsPath);
		try {
@@ -258,7 +289,7 @@ public class ConstraintExporter {
	}

	private String getConstraint(Content cx) {
		if (this.format == OutputFormat.OCL)
		if (this.format == OutputFormat.OCL || this.format == OutputFormat.ECORE)
			return getOclConstraint(cx);
		else
			return getEvlConstraint(cx);
@@ -376,6 +407,8 @@ public class ConstraintExporter {
				//Escape reserved words
				ocl = Pattern.compile("(?<=\\.)(body)(?=\\s|\\.|-|\\))").matcher(ocl).replaceAll("_'$1'");
				
				records.add(new ConstraintRecord(cx.getParent().getText().replaceAll(".+\\s", ""), id.trim(), title, description,
						ocl.replaceAll("\\n\\s+", "\n").trim()));
				content += "  -- " + title + "\n";
				content += "  inv " + id + " ('" + description.replaceAll("\\'", "\\\\\\'") + "' + self.toString()):\n";
				content += "    " + ocl + "\n";
+31 −0
Changes for plugins/org.etsi.mts.tdl.constraints.helper/src/org/etsi/mts/tdl/constraints/helper/ConstraintRecord.java: 31 added lines, 0 removed lines.
Original line number Diff line number Diff line
package org.etsi.mts.tdl.constraints.helper;

/**
 * One constraint as extracted from a standard document.
 */
public final class ConstraintRecord {

	public final String className;
	public final String name;
	public final String title;
	public final String description;
	public final String expression;

	public ConstraintRecord(String className, String name, String title, String description, String expression) {
		this.className = className;
		this.name = name;
		this.title = title == null ? "" : title;
		this.description = description == null ? "" : description;
		this.expression = expression;
	}

	/** The OCL message expression used by both the Complete OCL and the OCLinEcore output. */
	public String messageExpression() {
		return "'" + description.replace("'", "\\'") + "' + self.toString()";
	}

	@Override
	public String toString() {
		return className + "::" + name;
	}
}
Loading