diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 79f693e..c80d09d 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -86,4 +86,28 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload ${{ github.event.release.tag_name }} $(ls reta-*.msi reta*.deb reta*.rpm reta-*.zip 2>/dev/null) - \ No newline at end of file + + publish-page: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + - name: Build Site + run: ./mvnw -B clean site site:stage + - name: Publish GitHub Pages + run: | + git config user.name ben12 + git config user.email ben12@users.noreply.github.com + git clone --depth 1 --branch gh-pages https://github.com/ben12/reta.git gh-pages + cd gh-pages + rm -rf * + cp -r ../target/staging/* ./ + git add --all + git commit -m "Update GitHub Pages" + git push + diff --git a/README.md b/README.md index 371f718..82ae7de 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,53 @@ -reta -==== - -Requirement Engineering Traceability Analysis +Requirement Engineering Traceability Analysis (RETA) +=== http://reta.ben12.eu +RETA (Requirement Engineering Traceability Analysis) can read any source of documents or code, extracts the requirements and requirement references, and analyses them for generate the traceability matrix. + +# Installation + +Download then install [the last release](https://github.com/ben12/reta/releases/latest). + +# Plugins + +## TIKA plugin + +TIKA plugin uses [Apache Tika](https://tika.apache.org/) to read any document (doc, xls, pdf, ...) and extracts requirements and requirement references from it. + +### Configuration + +Requirements and references are extracted from documents using regular expression. You can find documentation on the web and for example here : [Pattern JAVADOC](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html#construct) + +So, if your requirements are formatted like this in your document : + +> REQ_RETA_SRS_*NNNN*_*v* - *summary* +> +> *content* +> +> REQ_END + +Where "NNNN" is the requirement number, "v" the version, "summary" the summary description and "content" the detailed description. + +Then, regular expression to match requirement start may be : + `^[ \t]*REQ_((RETA_SRS_\d+)_(\w+)[\s-]+(.*?))$` +And regular expression to match requirement end may be : + `^[ \t]*REQ_END[ \t]*$` + +Bracket will capture part of text which will be used, for example, for identify the requirement reference using "Id" attribute ([Pattern JAVADOC](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html#cg)). +For done this, you must identify each capture group using "Index of regular expression groups in requirement starts" table and "Index of regular expression groups in references" table. +In this sample: + - "Text" attribute must be set to *1* for capture "RETA_SRS_*NNNN*_*v* - *summary*", + - "Id" attribute must be set to *2* for capture "RETA_SRS_*NNNN*", + - "Version" attribute must be set to *3* for capture "*v*", + - "Summary" attribute must be set to *4* for capture "*summary*", + +Text and Id attributes are required to build the matrix. + +# Export + +RETA allows you to export the analysis result to an Excel file. + +# Alternatives to RETA + +- Reqtify (https://www.3ds.com/products/catia/reqtify) \ No newline at end of file diff --git a/RETA-api/pom.xml b/RETA-api/pom.xml index 3a1d4d8..425e0a8 100644 --- a/RETA-api/pom.xml +++ b/RETA-api/pom.xml @@ -42,9 +42,9 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 RETA API JavaDocs +
RETA ${project.version} ]]>
@@ -61,7 +61,7 @@ GNU General Public License (GPL) - http://www.gnu.org/licenses/gpl-3.0.txt + ../LICENSE \ No newline at end of file diff --git a/RETA-api/src/main/java/com/ben12/reta/beans/property/validation/BeanPropertyValidation.java b/RETA-api/src/main/java/com/ben12/reta/beans/property/validation/BeanPropertyValidation.java index 9b60c71..97a7ef3 100644 --- a/RETA-api/src/main/java/com/ben12/reta/beans/property/validation/BeanPropertyValidation.java +++ b/RETA-api/src/main/java/com/ben12/reta/beans/property/validation/BeanPropertyValidation.java @@ -44,8 +44,8 @@ * * @param * value type to validate - * @see javax.validation.Validation - * @see javax.validation.Validator + * @see jakarta.validation.Validation + * @see jakarta.validation.Validator * @author Benoît Moreau (ben.12) */ public interface BeanPropertyValidation extends PropertyValidation diff --git a/RETA-core/pom.xml b/RETA-core/pom.xml index 6a70a86..3409e8d 100644 --- a/RETA-core/pom.xml +++ b/RETA-core/pom.xml @@ -10,9 +10,6 @@ reta-parent 1.1.0 - - 5.4.0 - ben.12 @@ -33,14 +30,6 @@ reta-api - - - com.ben12 - reta-tika-plugin - runtime - - - org.apache.poi poi @@ -56,6 +45,12 @@ poi-scratchpad ${poi.version} + + + net.sourceforge.plantuml + plantuml + 1.2025.2 + @@ -91,9 +86,9 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 RETA CORE JavaDocs +
RETA ${project.version} ]]>
@@ -110,7 +105,7 @@ GNU General Public License (GPL) - http://www.gnu.org/licenses/gpl-3.0.txt + ../LICENSE \ No newline at end of file diff --git a/RETA-core/src/main/java/com/ben12/reta/Main.java b/RETA-core/src/main/java/com/ben12/reta/Main.java index 24cda83..a85dcec 100644 --- a/RETA-core/src/main/java/com/ben12/reta/Main.java +++ b/RETA-core/src/main/java/com/ben12/reta/Main.java @@ -62,11 +62,14 @@ public void start(final Stage stage) throws Exception loader.setResources(labels); final Parent root = (Parent) loader.load(); + final var title = labels.getString("title"); + final var controller = (MainConfigurationController) loader.getController(); + stage.titleProperty().bind(controller.bufferingProperty().map(b -> title + (b.booleanValue() ? " *" : ""))); + stage.setScene(new Scene(root)); - stage.setTitle(labels.getString("title")); stage.getIcons() .add(new Image(Main.class.getResourceAsStream("/com/ben12/reta/resources/images/reta.png"))); - stage.sizeToScene(); + stage.setMaximized(true); stage.show(); final Parameters parameters = getParameters(); diff --git a/RETA-core/src/main/java/com/ben12/reta/export/ExcelExporter.java b/RETA-core/src/main/java/com/ben12/reta/export/ExcelExporter.java index 8e351f7..f18ce0c 100644 --- a/RETA-core/src/main/java/com/ben12/reta/export/ExcelExporter.java +++ b/RETA-core/src/main/java/com/ben12/reta/export/ExcelExporter.java @@ -1,7 +1,7 @@ // Package : com.ben12.reta.export // File : ExcelExporter.java // -// Copyright (C) 2025 benmo +// Copyright (C) 2025 Benoît Moreau (ben.12) // // This file is part of RETA (Requirement Engineering Traceability Analysis). // @@ -452,7 +452,7 @@ private void exportCoverBy(final InputRequirementSource source, final XSSFSheet { final var rateRow = addNewRow(sheet, 1); final var rateCell = addNewCell(rateRow); - rateCell.setCellValue(source.getName() + " is cover by " + coverBy.getKey().getName() + " at " + rateCell.setCellValue(source.getName() + " is covered by " + coverBy.getKey().getName() + " at " + (coverBy.getValue() * 100) + " %"); addColspan(sheet, rateCell, 2); diff --git a/RETA-core/src/main/java/com/ben12/reta/model/GraphData.java b/RETA-core/src/main/java/com/ben12/reta/model/GraphData.java new file mode 100644 index 0000000..0c736b7 --- /dev/null +++ b/RETA-core/src/main/java/com/ben12/reta/model/GraphData.java @@ -0,0 +1,138 @@ +// Package : com.ben12.reta.model +// File : GraphData.java +// +// Copyright (C) 2025 Benoît Moreau (ben.12) +// +// This file is part of RETA (Requirement Engineering Traceability Analysis). +// +// RETA 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. +// +// RETA 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 RETA. If not, see . +package com.ben12.reta.model; + +import java.util.Map; + +/** + * @author Benoît Moreau (ben.12) + */ +public class GraphData +{ + private final String html; + + private final Map sourceEntities; + + private final Map links; + + /** + * @param pHtml + * SVG source + * @param pSourceEntities + * requirement source alias + * @param pLinks + * links identifiers + */ + public GraphData(final String pHtml, final Map pSourceEntities, + final Map pLinks) + { + this.html = pHtml; + this.sourceEntities = pSourceEntities; + this.links = pLinks; + } + + /** + * @return the svg source + */ + public String getHtml() + { + return html; + } + + /** + * @return the requirement source alias + */ + public Map getSourceEntities() + { + return sourceEntities; + } + + /** + * @return the links identifiers + */ + public Map getLinks() + { + return links; + } + + public static class Link + { + private final String line; + + private final InputRequirementSource source1; + + private final RequirementImpl req1; + + private final InputRequirementSource source2; + + private final RequirementImpl req2; + + public Link(final String pLine, final InputRequirementSource pSource1, final RequirementImpl pReq1, + final InputRequirementSource pSource2, final RequirementImpl pReq2) + { + this.line = pLine; + this.source1 = pSource1; + this.req1 = pReq1; + this.source2 = pSource2; + this.req2 = pReq2; + + } + + /** + * @return the line + */ + public String getLine() + { + return line; + } + + /** + * @return the source1 + */ + public InputRequirementSource getSource1() + { + return source1; + } + + /** + * @return the req1 + */ + public RequirementImpl getReq1() + { + return req1; + } + + /** + * @return the source2 + */ + public InputRequirementSource getSource2() + { + return source2; + } + + /** + * @return the req2 + */ + public RequirementImpl getReq2() + { + return req2; + } + } +} diff --git a/RETA-core/src/main/java/com/ben12/reta/model/RequirementImpl.java b/RETA-core/src/main/java/com/ben12/reta/model/RequirementImpl.java index 15d25a3..86e5273 100644 --- a/RETA-core/src/main/java/com/ben12/reta/model/RequirementImpl.java +++ b/RETA-core/src/main/java/com/ben12/reta/model/RequirementImpl.java @@ -59,7 +59,7 @@ public class RequirementImpl implements Requirement, Comparable /** Requirement extra attributes name and value. */ private Map attributes = null; - /** Set of requirements cover by this requirement. */ + /** Set of requirements covered by this requirement. */ private Set references = null; /** Set of requirements covering this requirement. */ @@ -306,7 +306,7 @@ public int getReferredByCount() */ public List getReferredByRequirement() { - return (referredBy == null ? new ArrayList(0) : new ArrayList(referredBy)); + return (referredBy == null ? new ArrayList<>(0) : new ArrayList<>(referredBy)); } /** @@ -316,7 +316,7 @@ public List getReferredByRequirement() */ public List getReferredByRequirementFor(final InputRequirementSource aSource) { - return (referredBy == null ? new ArrayList(0) + return (referredBy == null ? new ArrayList<>(0) : referredBy.stream().filter((r) -> r.getSource() == aSource).distinct().collect(Collectors.toList())); } @@ -325,9 +325,12 @@ public List getReferredByRequirementFor(final InputRequirementS */ public List getReferredBySource() { - return (referredBy == null ? new ArrayList(0) - : referredBy.stream().map(RequirementImpl::getSource).filter(Objects::nonNull).distinct().collect( - Collectors.toList())); + return (referredBy == null ? new ArrayList<>(0) + : referredBy.stream() + .map(RequirementImpl::getSource) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList())); } /* diff --git a/RETA-core/src/main/java/com/ben12/reta/util/DOMUtils.java b/RETA-core/src/main/java/com/ben12/reta/util/DOMUtils.java new file mode 100644 index 0000000..b66da59 --- /dev/null +++ b/RETA-core/src/main/java/com/ben12/reta/util/DOMUtils.java @@ -0,0 +1,65 @@ +// Package : com.ben12.reta.util +// File : DOMUtils.java +// +// Copyright (C) 2025 Benoît Moreau (ben.12) +// +// This file is part of RETA (Requirement Engineering Traceability Analysis). +// +// RETA 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. +// +// RETA 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 RETA. If not, see . +package com.ben12.reta.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * @author Benoît Moreau (ben.12) + */ +public class DOMUtils +{ + public static List getElementsByTagNameAndAttribute(final Document doc, final String tag, + final String attr) + { + final var elements = getElementsByTagName(doc, tag); + elements.removeIf(e -> !e.hasAttribute(attr)); + return elements; + } + + public static List getElementsByTagName(final Document doc, final String name) + { + final var nodeList = doc.getElementsByTagName(name); + return getElements(nodeList); + } + + public static List getElementsByTagName(final Element el, final String name) + { + final var nodeList = el.getElementsByTagName(name); + return getElements(nodeList); + } + + public static List getElements(final NodeList nodeList) + { + return IntStream.range(0, nodeList.getLength()) + .mapToObj(nodeList::item) + .filter(n -> n instanceof Element) + .map(Element.class::cast) + .collect(Collectors.toCollection(ArrayList::new)); + } + +} diff --git a/RETA-core/src/main/java/com/ben12/reta/util/RETAAnalysis.java b/RETA-core/src/main/java/com/ben12/reta/util/RETAAnalysis.java index 87c187e..653a358 100644 --- a/RETA-core/src/main/java/com/ben12/reta/util/RETAAnalysis.java +++ b/RETA-core/src/main/java/com/ben12/reta/util/RETAAnalysis.java @@ -25,8 +25,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -44,8 +42,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -56,16 +52,10 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; -import com.google.common.io.Files; - -import jakarta.validation.constraints.NotEmpty; import com.ben12.reta.api.RETAParseException; import com.ben12.reta.api.RETAParser; import com.ben12.reta.api.SourceConfiguration; -import com.ben12.reta.beans.constraints.IsPath; -import com.ben12.reta.beans.constraints.PathExists; -import com.ben12.reta.beans.constraints.PathExists.KindOfPath; import com.ben12.reta.export.ExcelExporter; import com.ben12.reta.model.InputRequirementSource; import com.ben12.reta.model.RequirementImpl; @@ -84,9 +74,6 @@ public final class RETAAnalysis /** {@link #requirementSources} property name. */ public static final String REQUIREMENT_SOURCES = "requirementSources"; - /** {@link #output} property name. */ - public static final String OUTPUT = "output"; - /** Singleton instance. */ private static RETAAnalysis instance = null; @@ -99,13 +86,6 @@ public final class RETAAnalysis /** Configuration file opened. */ private File config = null; - /** Output Excel file path. */ - @NotEmpty - @IsPath - @PathExists(kind = KindOfPath.DIRECTORY, parent = true) - private final StringProperty output = new SimpleStringProperty(this, - OUTPUT); - // TODO maybe useful to see unknown references and mismatch versions // private final Comparator reqCompId = (req1, req2) -> req1.getId().compareTo(req2.getId()); @@ -176,14 +156,6 @@ public File getConfig() return config; } - /** - * @return the output - */ - public StringProperty outputProperty() - { - return output; - } - /** * @param iniFile * RETA INI file @@ -209,35 +181,6 @@ public void configure(final File iniFile) ini.getConfig().setFileEncoding(Charset.forName("CP1252")); ini.load(iniFile); - final String outputFile = ini.get("GENERAL", "output"); - if (outputFile != null) - { - final Path outPath = Paths.get(outputFile); - final String fileName = outPath.getFileName().toString(); - if (!"xlsx".equals(Files.getFileExtension(fileName))) - { - LOGGER.warning("output extension changed for xlsx"); - final Path parent = outPath.getParent(); - if (parent == null) - { - output.set(Files.getNameWithoutExtension(fileName) + ".xlsx"); - } - else - { - output.set(parent.resolve(Files.getNameWithoutExtension(fileName) + ".xlsx").toString()); - } - } - else - { - output.set(outPath.normalize().toString()); - } - } - else - { - output.set(Paths.get(iniFile.getParent(), Files.getNameWithoutExtension(iniFile.getName()) + ".xlsx") - .toString()); - } - final Map> coversMap = new LinkedHashMap<>(); final String documentsStr = ini.get("GENERAL", "inputs"); @@ -340,8 +283,6 @@ public boolean saveConfig(final File iniFile) { final Section generalSection = ini.add("GENERAL"); - generalSection.add("output", output.get()); - final List inputs = new ArrayList<>(); for (final InputRequirementSource requirementSource : requirementSources) @@ -446,9 +387,13 @@ public void parse(final InputRequirementSource requirementSource, final StringBu /** * Analyze the parsing result. * Search requirement references in requirements. + * + * @param progress + * progression in percent */ - public void analyse() + public void analyse(final Consumer progress) { + final var counter = new AtomicInteger(0); requirementSources.parallelStream().forEach(source -> { LOGGER.info("Start analyse " + source.getName()); @@ -490,6 +435,8 @@ public void analyse() } } + progress.accept((double) counter.incrementAndGet() / requirementSources.size()); + LOGGER.info("End analyse " + source.getName()); }); } @@ -497,22 +444,17 @@ public void analyse() /** * Write Excel file result of requirement traceability analysis. * + * @param outputFile + * output file * @throws IOException * I/O exception * @throws InvalidFormatException * Invalid Excel format exception */ - public void writeExcel() throws IOException, InvalidFormatException + public void writeExcel(final File outputFile) throws IOException, InvalidFormatException { LOGGER.info("Start write excel output"); - Path outputFile = Paths.get(output.get()); - if (!outputFile.isAbsolute()) - { - final Path root = config.getAbsoluteFile().getParentFile().toPath(); - outputFile = root.resolve(outputFile); - } - final var exporter = new ExcelExporter(); final var wb = exporter.export(requirementSources); writeExcel(wb, outputFile); @@ -528,9 +470,9 @@ public void writeExcel() throws IOException, InvalidFormatException * @throws IOException * I/O exception */ - public void writeExcel(final Workbook workbook, final Path outputFile) throws IOException + public void writeExcel(final Workbook workbook, final File outputFile) throws IOException { - try (FileOutputStream fos = new FileOutputStream(outputFile.toFile())) + try (FileOutputStream fos = new FileOutputStream(outputFile)) { workbook.write(fos); } diff --git a/RETA-core/src/main/java/com/ben12/reta/view/MainConfigurationController.java b/RETA-core/src/main/java/com/ben12/reta/view/MainConfigurationController.java index 2fca0a1..9790dea 100644 --- a/RETA-core/src/main/java/com/ben12/reta/view/MainConfigurationController.java +++ b/RETA-core/src/main/java/com/ben12/reta/view/MainConfigurationController.java @@ -20,15 +20,19 @@ package com.ben12.reta.view; import java.awt.Desktop; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -36,28 +40,49 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.IntegerBinding; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; +import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Accordion; import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; +import javafx.scene.layout.GridPane; +import javafx.scene.web.WebView; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; import javafx.util.Callback; import javafx.util.Pair; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.w3c.dom.Element; + +import com.google.common.base.Objects; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; + import com.ben12.reta.beans.property.buffering.BufferingManager; import com.ben12.reta.beans.property.buffering.ObservableListBuffering; -import com.ben12.reta.beans.property.buffering.SimpleObjectPropertyBuffering; +import com.ben12.reta.model.GraphData; +import com.ben12.reta.model.GraphData.Link; import com.ben12.reta.model.InputRequirementSource; +import com.ben12.reta.model.RequirementImpl; import com.ben12.reta.plugin.SourceProviderPlugin; +import com.ben12.reta.util.DOMUtils; import com.ben12.reta.util.RETAAnalysis; import com.ben12.reta.view.control.MessageDialog; import com.ben12.reta.view.validation.ValidationDecorator; @@ -76,9 +101,6 @@ public class MainConfigurationController implements Initializable /** The {@link BufferingManager} instance. */ private final BufferingManager bufferingManager = new BufferingManager(); - /** Output file buffered property. */ - private final SimpleObjectPropertyBuffering bufferedOutput; - /** Requirement source list. */ private final ObservableList sources = FXCollections .observableArrayList(); @@ -106,6 +128,12 @@ public class MainConfigurationController implements Initializable /** Requirement source {@link TitledPane} list. */ private List panes = new ArrayList<>(); + /** Last export file. */ + private File lastExport = null; + + /** Graph data. */ + private GraphData graph; + /** Root pane. */ @FXML private Parent root; @@ -126,6 +154,10 @@ public class MainConfigurationController implements Initializable @FXML private Button run; + /** Run analysis button. */ + @FXML + private Button export; + /** Delete selected requirement source button. */ @FXML private Button delete; @@ -142,6 +174,14 @@ public class MainConfigurationController implements Initializable @FXML private ValidationDecorator outputFile; + /** TabPane result. */ + @FXML + private TabPane resultTabs; + + /** Graph result. */ + @FXML + private WebView webview; + /** * Constructor. */ @@ -156,7 +196,6 @@ public MainConfigurationController() bufferingManager.add((ObservableListBuffering) bufferedSources); bufferedSourcesName = bufferingManager.buffering(sourcesName); - bufferedOutput = bufferingManager.buffering(RETAAnalysis.getInstance().outputProperty()); } /** @@ -206,9 +245,6 @@ public void initialize(final URL location, final ResourceBundle resources) try { - outputFile.getChild().textProperty().bindBidirectional(bufferedOutput); - outputFile.bindValidation(bufferedOutput); - delete.disableProperty() .bind(sourceConfigurations.expandedPaneProperty() .isNull() @@ -250,6 +286,14 @@ public ObservableList getDependencies() save.disableProperty().bind(Bindings.not(bufferingManager.validProperty())); cancel.disableProperty().bind(Bindings.not(bufferingManager.bufferingProperty())); run.disableProperty().bind(Bindings.not(bufferingManager.validProperty())); + export.setDisable(true); + + webview.getEngine().getLoadWorker().stateProperty().subscribe(state -> { + if (state == Worker.State.SUCCEEDED) + { + this.customizePlantuml(); + } + }); } catch (final Exception e) { @@ -257,6 +301,50 @@ public ObservableList getDependencies() } } + private void customizePlantuml() + { + final var document = webview.getEngine().getDocument(); + final var groups = DOMUtils.getElementsByTagNameAndAttribute(document, "g", "data-entity"); + for (final var g : groups) + { + final var sourceAlias = g.getAttribute("data-entity"); + final var source = graph.getSourceEntities().get(sourceAlias); + final var texts = DOMUtils.getElementsByTagName(g, "text"); + + if (!texts.isEmpty() && Objects.equal(source.getName(), texts.getFirst().getTextContent())) + { + final var textEl = texts.get(0); + final var title = document.createElementNS(textEl.getNamespaceURI(), "title"); + title.setTextContent(source.getConfiguration().getDescription()); + textEl.appendChild(title); + } + + for (final Element textEl : texts.subList(1, texts.size())) + { + final var reqId = textEl.getTextContent(); + final var req = source.getRequirements() + .stream() + .filter(r -> Objects.equal(r.getId(), reqId)) + .findFirst(); + if (req.isPresent()) + { + final var title = document.createElementNS(textEl.getNamespaceURI(), "title"); + title.setTextContent(req.get().getText()); + textEl.appendChild(title); + if (req.get().getReferredBySource().isEmpty() && !source.getCoversBy().isEmpty()) + { + textEl.setAttribute("style", "fill: red;"); + } + else if (!req.get().getReferredBySource().isEmpty() + && req.get().getReferredBySource().size() < source.getCoversBy().size()) + { + textEl.setAttribute("style", "fill: coral;"); + } + } + } + } + } + /** * Add the requirement source in the view. * @@ -470,24 +558,30 @@ protected void run(final ActionEvent event) { final var task = new Task() { + private void updateProgress(final double p) + { + updateProgress(p, 1.0); + } + @Override protected Void call() throws Exception { try { - updateProgress(0.00, 1.0); + updateProgress(0.00); + updateMessage(labels.getString("progress.reading")); + RETAAnalysis.getInstance().parse(p -> updateProgress(p * 0.50)); + updateProgress(0.50); - RETAAnalysis.getInstance().parse(p -> updateProgress(p * 0.60, 1.0)); - updateProgress(0.60, 1.0); updateMessage(labels.getString("progress.analysing")); + RETAAnalysis.getInstance().analyse(p -> updateProgress(0.5 + (p * 0.10))); + updateProgress(0.60); - RETAAnalysis.getInstance().analyse(); - updateProgress(0.70, 1.0); - updateMessage(labels.getString("progress.writing")); + updateMessage(labels.getString("progress.graph")); + graph = buildGraph(); + updateProgress(1.0); - RETAAnalysis.getInstance().writeExcel(); - updateProgress(1.0, 1.0); updateMessage(labels.getString("progress.complete")); } catch (final Exception e) @@ -497,42 +591,325 @@ protected Void call() throws Exception } finally { - updateProgress(1.0, 1.0); + updateProgress(1.0); } return null; } + + @Override + protected void succeeded() + { + webview.getEngine().loadContent(graph.getHtml()); + buildTabs(); + export.setDisable(false); + } + + @Override + protected void failed() + { + if (graph != null) + { + webview.getEngine().loadContent(graph.getHtml()); + buildTabs(); + } + } }; MessageDialog.showProgressBar(root.getScene().getWindow(), labels.getString("progress.title"), task); + graph = null; + webview.getEngine().loadContent(""); + resultTabs.getTabs().remove(1, resultTabs.getTabs().size()); + export.setDisable(true); + new Thread(task).start(); } } - /** - * Action event to select the output file. - * - * @param event - * the {@link ActionEvent} - */ + private GraphData buildGraph() + { + final Map sourceEntities = new HashMap<>(); + final Map links = new HashMap<>(); + final var graphLines = new ArrayList(); + + graphLines.add("@startuml"); + graphLines.add("skinparam classAttributeIconSize 0"); + + final var analysis = RETAAnalysis.getInstance(); + + final var index = new AtomicInteger(0); + final var allSources = analysis.requirementSourcesProperty(); + final var isources = allSources.stream() + .collect(Collectors.toMap(s -> s, s -> index.getAndIncrement(), (a, b) -> a)); + + for (final var source : allSources) + { + final var i = isources.get(source); + graphLines.add("object \"**" + source.getName() + "**\" as S" + i + " {"); + final var reqs = source.getRequirements(); + for (final var req : reqs) + { + graphLines.add("{field} " + req.getId().replace("\\", "\\")); + } + graphLines.add("}"); + + sourceEntities.put("S" + i, source); + } + + for (final var source : allSources) + { + final var i = isources.get(source); + final var reqs = source.getRequirements(); + for (final var req : reqs) + { + final var reqId = req.getId().replace("\\", "\\"); + final var refs = req.getReferences(); + for (final var ref : refs) + { + final var refSource = ref.getSource(); + if (refSource != null) + { + final var refSo = isources.get(refSource); + final var refId = ref.getId().replace("\\", "\\"); + graphLines.add("\"S" + refSo + "::" + refId + "\" <--- \"S" + i + "::" + reqId + "\""); + } + + final String dataSourceLine = "" + graphLines.size(); + links.put(dataSourceLine, new Link(dataSourceLine, source, req, refSource, ref)); + } + } + } + + graphLines.add("@enduml"); + + final var puGraph = String.join("\n", graphLines); + final var reader = new SourceStringReader(puGraph); + final var output = new ByteArrayOutputStream(); + try + { + reader.outputImage(output, new FileFormatOption(FileFormat.SVG)); + } + catch (final IOException e) + { + LOGGER.log(Level.SEVERE, "Error during Graph generation", e); + } + + final var svg = new String(output.toByteArray(), StandardCharsets.UTF_8); + final var html = """ + + + + + """ + svg + """ + + """; + return new GraphData(html, sourceEntities, links); + } + + private void buildTabs() + { + final var analysis = RETAAnalysis.getInstance(); + final var allSources = analysis.requirementSourcesProperty(); + for (final var source : allSources) + { + final var tab = createSourceTab(source); + resultTabs.getTabs().add(tab); + } + + final var tab = createErrorsTab(allSources); + resultTabs.getTabs().add(tab); + } + + private Tab createErrorsTab(final List allSources) + { + final var tab = new Tab(labels.getString("errors")); + tab.setClosable(false); + + final var table = new GridPane(); + table.getStyleClass().add("result-table"); + table.setPadding(new Insets(8)); + + final var sourceHeader = new Label(labels.getString("unknownfromsrc")); + sourceHeader.getStyleClass().addAll("header", "first-row", "first-col"); + final var reqHeader = new Label(labels.getString("unknownfromreq")); + reqHeader.getStyleClass().addAll("header", "first-row"); + final var refHeader = new Label(labels.getString("unknownreference")); + refHeader.getStyleClass().addAll("header", "first-row", "last-col"); + table.addRow(0, sourceHeader, reqHeader, refHeader); + + final var total = allSources.stream().mapToInt(s -> s.getAllUknownReferences().size()).sum(); + + for (final var source : allSources) + { + final var reqs = source.getRequirements(); + final var count = source.getAllUknownReferences().size(); + if (count > 0) + { + var row = table.getRowCount(); + final var sourceCell = new Label(source.getName()); + sourceCell.getStyleClass().addAll("first-col"); + table.add(sourceCell, 0, row, 1, count); + for (final var req : reqs) + { + final var refs = req.getReferencesFor(null); + if (!refs.isEmpty()) + { + final var reqCell = new Label(req.getText()); + table.add(reqCell, 1, row, 1, refs.size()); + for (final var ref : refs) + { + final var refCell = new Label(ref.getText()); + refCell.getStyleClass().addAll("last-col"); + table.add(refCell, 2, row); + if (row == total) + { + sourceCell.getStyleClass().addAll("last-row"); + reqCell.getStyleClass().addAll("last-row"); + refCell.getStyleClass().addAll("last-row"); + } + row++; + } + } + } + } + } + + tab.setContent(new ScrollPane(table)); + return tab; + } + @FXML - protected void selectOutputFile(final ActionEvent event) + protected void export(final ActionEvent event) { - final Path currentFile = bufferedOutput.get() == null ? null : Paths.get(bufferedOutput.get()); - final FileChooser fileChooser = new FileChooser(); - fileChooser.getExtensionFilters().add(new ExtensionFilter(labels.getString("excel.file.desc"), "*.xlsx")); - fileChooser.setTitle(labels.getString("output.title")); - if (currentFile != null) + try + { + final FileChooser fileChooser = new FileChooser(); + fileChooser.getExtensionFilters().add(new ExtensionFilter(labels.getString("excel.file.desc"), "*.xlsx")); + fileChooser.setTitle(labels.getString("output.title")); + if (lastExport != null) + { + fileChooser.setInitialDirectory(lastExport.getParentFile()); + fileChooser.setInitialFileName(lastExport.getName()); + } + else + { + fileChooser.setInitialDirectory(RETAAnalysis.getInstance().getConfig().getParentFile()); + } + + final File file = fileChooser.showSaveDialog(root.getScene().getWindow()); + if (file != null) + { + RETAAnalysis.getInstance().writeExcel(file); + lastExport = file; + } + } + catch (InvalidFormatException | IOException e) { - fileChooser.setInitialDirectory(currentFile.toFile().getParentFile()); - fileChooser.setInitialFileName(currentFile.getFileName().toString()); + LOGGER.log(Level.SEVERE, "Exporting excel", e); } - final File file = fileChooser.showSaveDialog(root.getScene().getWindow()); + } - if (file != null) + private Tab createSourceTab(final InputRequirementSource source) + { + final var tab = new Tab(source.getName()); + tab.setClosable(false); + + final var table = new GridPane(); + table.getStyleClass().add("result-table"); + table.setPadding(new Insets(8)); + + final var reqHeader = new Label(labels.getString("requirement")); + reqHeader.getStyleClass().addAll("header", "first-row", "first-col"); + final var sourceHeader = new Label(labels.getString("source")); + sourceHeader.getStyleClass().addAll("header", "first-row"); + final var refHeader = new Label(labels.getString("reference")); + refHeader.getStyleClass().addAll("header", "first-row", "last-col"); + + table.addRow(0, reqHeader, sourceHeader, refHeader); + + final var requirements = source.getRequirements(); + final var refSources = source.getCoversBy().keySet(); + var index = 0; + for (final var requirement : requirements) + { + final var reqCell = new Label(requirement.getText()); + reqCell.getStyleClass().add("first-col"); + setReqStyle(source, requirement, reqCell); + + final List
+ + GitHub + https://github.com/ben12/reta/issues + + + https://github.com/ben12/reta/tree/master/RETA-packaging + scm:git:git://github.com/ben12/reta.git/RETA-packaging + scm:git:git@github.com:ben12/reta.git/RETA-packaging + + + + GNU General Public License (GPL) + ../LICENSE + + \ No newline at end of file diff --git a/RETA-tika-plugin/pom.xml b/RETA-tika-plugin/pom.xml index fb5f4f7..e18f3ee 100644 --- a/RETA-tika-plugin/pom.xml +++ b/RETA-tika-plugin/pom.xml @@ -10,10 +10,6 @@ reta-parent 1.1.0 - - 3.1.0 - 5.4.0 - ben.12 @@ -78,9 +74,9 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 RETA TIKA plugin JavaDocs +
RETA ${project.version} ]]>
@@ -97,7 +93,7 @@ GNU General Public License (GPL) - http://www.gnu.org/licenses/gpl-3.0.txt + ../LICENSE \ No newline at end of file diff --git a/data/analysis.reta b/data/analysis.reta index c0e45f3..d13c224 100644 --- a/data/analysis.reta +++ b/data/analysis.reta @@ -1,45 +1,40 @@ -[GENERAL] -output = analysis.xlsx -inputs = DOC1,DOC2,SOURCES - -[DOC1] -plugin = com.ben12.reta.plugin.tika.TikaSourceProviderPlugin -path = DocTest1.doc -filter = -requirement.start.regex = ^[ \t]*REQ_(RETA_(\w+_\d+)(?:_(\w+))?[\s-]+(.*))$ -requirement.start.Version.index = 3 -requirement.start.Label.index = 4 -requirement.start.Text.index = 1 -requirement.start.Id.index = 2 -requirement.end.regex = ^[ \t]*END_REQ[ \t]*$ -requirement.ref.regex = ^[ \t]*CF_REQ_RETA_(\w+_\d+)(?:_(\w+))?(?:[ \t-]+(.*))?$ -requirement.ref.Comment.index = 3 -requirement.ref.Version.index = 2 -requirement.ref.Id.index = 1 -covers = DOC2 - -[DOC2] -plugin = com.ben12.reta.plugin.tika.TikaSourceProviderPlugin -path = DocTest2.doc -filter = -requirement.start.regex = ^[ \t]*REQ_RETA_(\w+_\d+)(?:_(\w+))?[\s-]+(.*)$ -requirement.start.Version.index = 2 -requirement.start.Label.index = 3 -requirement.start.Text.index = 0 -requirement.start.Id.index = 1 -requirement.end.regex = ^END_REQ -requirement.ref.regex = ^[ \t]*CF_REQ_RETA_(\w+_\d+)(?:_(\w+))?(?:[ \t-]+(.*))?$ -requirement.ref.Comment.index = 3 -requirement.ref.Version.index = 2 -requirement.ref.Id.index = 1 -covers = DOC1 - -[SOURCES] -plugin = com.ben12.reta.plugin.tika.TikaDirectorySourceProviderPlugin -path = sourcesTests -filter = ^(include\\.*\.(h|hpp)|java\\.*\.java|[^\\]*\.(c|cpp))$ -requirement.ref.regex = CF_(?:REQ_)?RETA_(\w+_\d+)(?:_(\w+))? -requirement.ref.Version.index = 2 -requirement.ref.Id.index = 1 -covers = DOC1,DOC2 - +[GENERAL] +inputs = DOC1,DOC2,SOURCES + +[DOC1] +plugin = com.ben12.reta.plugin.tika.TikaSourceProviderPlugin +path = DocTest1.doc +filter = +requirement.start.regex = ^[ \t]*REQ_(RETA_(\w+_\d+)(?:_(\w+))?[\s-]+(.*))$ +requirement.start.Version.index = 3 +requirement.start.Label.index = 4 +requirement.start.Text.index = 1 +requirement.start.Id.index = 2 +requirement.end.regex = ^[ \t]*END_REQ[ \t]*$ +covers = + +[DOC2] +plugin = com.ben12.reta.plugin.tika.TikaSourceProviderPlugin +path = DocTest2.doc +filter = +requirement.start.regex = ^[ \t]*REQ_RETA_(\w+_\d+)(?:_(\w+))?[\s-]+(.*)$ +requirement.start.Version.index = 2 +requirement.start.Label.index = 3 +requirement.start.Text.index = 0 +requirement.start.Id.index = 1 +requirement.end.regex = ^END_REQ +requirement.ref.regex = ^[ \t]*CF_REQ_RETA_(\w+_\d+)(?:_(\w+))?(?:[ \t-]+(.*))?$ +requirement.ref.Comment.index = 3 +requirement.ref.Version.index = 2 +requirement.ref.Id.index = 1 +covers = DOC1 + +[SOURCES] +plugin = com.ben12.reta.plugin.tika.TikaDirectorySourceProviderPlugin +path = sourcesTests +filter = ^(include\\.*\.(h|hpp)|java\\.*\.java|[^\\]*\.(c|cpp))$ +requirement.ref.regex = CF_(?:REQ_)?RETA_(\w+_\d+)(?:_(\w+))? +requirement.ref.Version.index = 2 +requirement.ref.Id.index = 1 +covers = DOC1,DOC2 + diff --git a/eclipse/launchers/RETA Main.launch b/eclipse/launchers/RETA Main.launch index 96fca47..fd4d510 100644 --- a/eclipse/launchers/RETA Main.launch +++ b/eclipse/launchers/RETA Main.launch @@ -13,7 +13,7 @@ - + @@ -22,6 +22,6 @@ - + diff --git a/pom.xml b/pom.xml index 2962289..0e860d3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,15 @@ pom 1.1.0 Requirement Engineering Traceability Analysis - https://github.com/ben12/reta + https://reta.ben12.eu Requirement Engineering Traceability Analysis Project cp1252 21 21 21 + 3.1.0 + 5.4.0 @@ -45,6 +47,11 @@ javafx-fxml ${javafx.version} + + org.openjfx + javafx-web + ${javafx.version} + com.google.guava guava @@ -103,7 +110,7 @@ RETA - http://ben12.github.io/reta + https://reta.ben12.eu @@ -114,6 +121,11 @@ maven-compiler-plugin 3.14.0 + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + org.apache.maven.plugins maven-site-plugin @@ -137,7 +149,7 @@ GNU General Public License (GPL) - http://www.gnu.org/licenses/gpl-3.0.txt + ./LICENSE \ No newline at end of file diff --git a/site/markdown/index.md b/site/markdown/index.md new file mode 100644 index 0000000..d423495 --- /dev/null +++ b/site/markdown/index.md @@ -0,0 +1,50 @@ +## About + +RETA (Requirement Engineering Traceability Analysis) can read any source of documents or code, extracts the requirements and requirement references, and analyses them for generate the traceability matrix. + +## Installation + +Download then install [the last release](https://github.com/ben12/reta/releases/latest). + +## Plugins + +### TIKA plugin + +TIKA plugin uses [Apache Tika](https://tika.apache.org/) to read any document (doc, xls, pdf, ...) and extracts requirements and requirement references from it. + +#### Configuration + +Requirements and references are extracted from documents using regular expression. You can find documentation on the web and for example here : [Pattern JAVADOC](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html#construct) + +So, if your requirements are formatted like this in your document : + +> REQ_RETA_SRS_*NNNN*_*v* - *summary* +> +> *content* +> +> REQ_END + +Where "NNNN" is the requirement number, "v" the version, "summary" the summary description and "content" the detailed description. + +Then, regular expression to match requirement start may be : + `^[ \t]*REQ_((RETA_SRS_\d+)_(\w+)[\s-]+(.*?))$` +And regular expression to match requirement end may be : + `^[ \t]*REQ_END[ \t]*$` + +Bracket will capture part of text which will be used, for example, for identify the requirement reference using "Id" attribute ([Pattern JAVADOC](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html#cg)). +For done this, you must identify each capture group using "Index of regular expression groups in requirement starts" table and "Index of regular expression groups in references" table. +In this sample: + - "Text" attribute must be set to *1* for capture "RETA_SRS_*NNNN*_*v* - *summary*", + - "Id" attribute must be set to *2* for capture "RETA_SRS_*NNNN*", + - "Version" attribute must be set to *3* for capture "*v*", + - "Summary" attribute must be set to *4* for capture "*summary*", + +Text and Id attributes are required to build the matrix. + +## Export + +RETA allows you to export the analysis result to an Excel file. + +## Alternatives to RETA + +- Reqtify (https://www.3ds.com/products/catia/reqtify) \ No newline at end of file diff --git a/site/site.xml b/site/site.xml index c07eeb1..091d3dc 100644 --- a/site/site.xml +++ b/site/site.xml @@ -1,17 +1,21 @@ - org.apache.maven.skins maven-fluido-skin - 1.4 + 2.1.0 - + true + true + + + ben12/reta right