Skip to content

feat: add swagger-postman-importer plugin with JDK 8 support#812

Closed
bakthava wants to merge 3 commits intoundera:masterfrom
bakthava:feature/swagger-postman-importer-jdk8
Closed

feat: add swagger-postman-importer plugin with JDK 8 support#812
bakthava wants to merge 3 commits intoundera:masterfrom
bakthava:feature/swagger-postman-importer-jdk8

Conversation

@bakthava
Copy link
Copy Markdown
Contributor

@bakthava bakthava commented Apr 4, 2026

  • New JMeter plugin for importing Swagger/OpenAPI and Postman collections
  • Version 1.1 with full JDK 8 compatibility
  • Targets Java 8 with JMeter 5.1.1 dependencies
  • Includes JSON parsing and test plan generation
  • Registers as custom JMeter GUI action and menu creator

Fixes: Integration with undera/jmeter-plugins ecosystem

- New JMeter plugin for importing Swagger/OpenAPI and Postman collections
- Version 1.1 with full JDK 8 compatibility
- Targets Java 8 with JMeter 5.1.1 dependencies
- Includes JSON parsing and test plan generation
- Registers as custom JMeter GUI action and menu creator

Fixes: Integration with undera/jmeter-plugins ecosystem
Copilot AI review requested due to automatic review settings April 4, 2026 19:16
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.07%. Comparing base (09d23ae) to head (9c62c8f).
⚠️ Report is 64 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #812      +/-   ##
============================================
+ Coverage     68.90%   69.07%   +0.17%     
- Complexity     2629     2654      +25     
============================================
  Files           230      233       +3     
  Lines         15965    16061      +96     
  Branches       1638     1650      +12     
============================================
+ Hits          11000    11094      +94     
+ Misses         4146     4138       -8     
- Partials        819      829      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new JMeter GUI plugin module intended to import Swagger/OpenAPI specs and Postman collections, preview extracted HTTP requests, and generate a runnable JMeter .jmx test plan.

Changes:

  • Introduces a new Maven module (swagger-postman-importer) with JMeter GUI action + Tools menu integration.
  • Implements Swagger/OpenAPI + Postman parsing into a unified RequestModel.
  • Generates JMX test plans from parsed requests (including headers, query params, and bodies).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
plugins/swagger-postman-importer/pom.xml New module POM with JMeter deps and shading for JSON library.
plugins/swagger-postman-importer/main/resources/META-INF/services/org.apache.jmeter.gui.plugin.MenuCreator Registers the menu creator via ServiceLoader.
plugins/swagger-postman-importer/main/resources/META-INF/services/org.apache.jmeter.gui.action.Command Registers the GUI action via ServiceLoader.
plugins/swagger-postman-importer/main/resources/extra_menus.xml Adds a Tools menu entry definition.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/YamlToJsonConverter.java Custom YAML→JSON conversion helper for OpenAPI YAML input.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/SwaggerPostmanImporterMenuCreator.java Adds Tools menu item that triggers the importer action.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/SwaggerPostmanImporterAction.java GUI dialog for selecting inputs/outputs, previewing parsed requests, and generating JMX.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/SwaggerImporter.java Parses Swagger/OpenAPI v2/v3 into RequestModel list.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/PostmanImporter.java Parses Postman v2/v2.1 collections into RequestModel list.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/model/RequestModel.java Shared request representation used by both importers and JMX generation.
plugins/swagger-postman-importer/main/java/com/example/jmeter/importer/JMeterTestPlanBuilder.java Emits JMX XML for a Thread Group + HTTP samplers + per-sampler headers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +45 to +54
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>8</release>
</configuration>
</plugin>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module uses a non-standard Maven layout (main/java and main/resources), but the POM doesn’t configure sourceDirectory / resources accordingly. With the current POM, Maven will look in src/main/java and src/main/resources, so this module won’t compile and the META-INF/services files won’t be packaged. Either move sources/resources under src/main/... or configure the build to point at the existing directories.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
<groupId>com.example</groupId>
<artifactId>swagger-postman-importer</artifactId>
<version>1.1</version>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

groupId/artifactId/packages are currently using placeholder com.example naming, which is inconsistent with the rest of this repository’s published coordinates (e.g., kg.apc:jmeter-plugins-*). This will make the artifact look unofficial and also leaks into the shaded relocation prefix (com.example.shaded...). Please align the Maven coordinates (and corresponding Java package / relocation prefix) with the project’s conventions.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +11
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>swagger-postman-importer</artifactId>
<version>1.1</version>
<packaging>jar</packaging>
<name>Swagger / Postman Importer for JMeter</name>
<description>JMeter plugin that imports Swagger/OpenAPI and Postman collections
and generates a JMeter Test Plan (.jmx).</description>

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module’s POM is missing the standard metadata that other plugin modules include (parent oss-parent, license, SCM, developers, encoding/source/target properties). If this is intended to be released alongside the other plugins, please mirror the structure used in existing plugin POMs so publishing/build tooling behaves consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +233
if (inputPath.isEmpty()) throw new IllegalArgumentException("Please select an input file.");
return swaggerRadio.isSelected()
? new SwaggerImporter().parse(inputPath)
: new PostmanImporter().parse(inputPath);
}

private void doPreview() {
statusLabel.setText("Parsing…");
statusLabel.setForeground(Color.DARK_GRAY);
SwingUtilities.invokeLater(() -> {
try {
List<RequestModel> requests = parseInput();
StringBuilder sb = new StringBuilder();
sb.append(String.format("Found %d request(s):%n%n", requests.size()));
for (int i = 0; i < requests.size(); i++) {
RequestModel r = requests.get(i);
sb.append(String.format("#%d %s%n", i + 1, r.getName()));
sb.append(String.format(" %-8s %s://%s%s%s%n",
r.getMethod(), r.getProtocol(), r.getHost(),
r.getPort() > 0 ? ":" + r.getPort() : "", r.getPath()));
if (!r.getHeaders().isEmpty()) {
sb.append(" Headers:");
for (RequestModel.HeaderEntry h : r.getHeaders())
sb.append(String.format("%n %s: %s", h.getName(), h.getValue()));
sb.append("\n");
}
if (!r.getQueryParams().isEmpty()) {
sb.append(" Query params:");
for (RequestModel.ParamEntry qp : r.getQueryParams())
sb.append(String.format("%n %s=%s", qp.getName(), qp.getValue()));
sb.append("\n");
}
if (r.getBodyData() != null && !r.getBodyData().isEmpty()) {
String body = r.getBodyData().length() > 200
? r.getBodyData().substring(0, 197) + "…"
: r.getBodyData();
sb.append(String.format(" Body: %s%n", body));
}
sb.append("\n");
}
previewArea.setText(sb.toString());
previewArea.setCaretPosition(0);
statusLabel.setText("Parsed " + requests.size() + " request(s).");
statusLabel.setForeground(new Color(0, 128, 0));
} catch (Exception ex) {
previewArea.setText("Error: " + ex.getMessage());
statusLabel.setText("Parse failed.");
statusLabel.setForeground(Color.RED);
}
});
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both parseInput() paths (Swagger/Postman) do file IO + JSON/YAML parsing, but doPreview() schedules that work with SwingUtilities.invokeLater, which still runs on the Swing EDT (and the action listener is already on the EDT). This will freeze the JMeter UI for larger specs/collections. Run parsing/generation on a background thread (e.g., SwingWorker) and update the UI on the EDT when done.

Suggested change
if (inputPath.isEmpty()) throw new IllegalArgumentException("Please select an input file.");
return swaggerRadio.isSelected()
? new SwaggerImporter().parse(inputPath)
: new PostmanImporter().parse(inputPath);
}
private void doPreview() {
statusLabel.setText("Parsing…");
statusLabel.setForeground(Color.DARK_GRAY);
SwingUtilities.invokeLater(() -> {
try {
List<RequestModel> requests = parseInput();
StringBuilder sb = new StringBuilder();
sb.append(String.format("Found %d request(s):%n%n", requests.size()));
for (int i = 0; i < requests.size(); i++) {
RequestModel r = requests.get(i);
sb.append(String.format("#%d %s%n", i + 1, r.getName()));
sb.append(String.format(" %-8s %s://%s%s%s%n",
r.getMethod(), r.getProtocol(), r.getHost(),
r.getPort() > 0 ? ":" + r.getPort() : "", r.getPath()));
if (!r.getHeaders().isEmpty()) {
sb.append(" Headers:");
for (RequestModel.HeaderEntry h : r.getHeaders())
sb.append(String.format("%n %s: %s", h.getName(), h.getValue()));
sb.append("\n");
}
if (!r.getQueryParams().isEmpty()) {
sb.append(" Query params:");
for (RequestModel.ParamEntry qp : r.getQueryParams())
sb.append(String.format("%n %s=%s", qp.getName(), qp.getValue()));
sb.append("\n");
}
if (r.getBodyData() != null && !r.getBodyData().isEmpty()) {
String body = r.getBodyData().length() > 200
? r.getBodyData().substring(0, 197) + "…"
: r.getBodyData();
sb.append(String.format(" Body: %s%n", body));
}
sb.append("\n");
}
previewArea.setText(sb.toString());
previewArea.setCaretPosition(0);
statusLabel.setText("Parsed " + requests.size() + " request(s).");
statusLabel.setForeground(new Color(0, 128, 0));
} catch (Exception ex) {
previewArea.setText("Error: " + ex.getMessage());
statusLabel.setText("Parse failed.");
statusLabel.setForeground(Color.RED);
}
});
return parseInput(inputPath, swaggerRadio.isSelected());
}
private List<RequestModel> parseInput(String inputPath, boolean swaggerSelected) throws Exception {
if (inputPath.isEmpty()) throw new IllegalArgumentException("Please select an input file.");
return swaggerSelected
? new SwaggerImporter().parse(inputPath)
: new PostmanImporter().parse(inputPath);
}
private String buildPreviewText(List<RequestModel> requests) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Found %d request(s):%n%n", requests.size()));
for (int i = 0; i < requests.size(); i++) {
RequestModel r = requests.get(i);
sb.append(String.format("#%d %s%n", i + 1, r.getName()));
sb.append(String.format(" %-8s %s://%s%s%s%n",
r.getMethod(), r.getProtocol(), r.getHost(),
r.getPort() > 0 ? ":" + r.getPort() : "", r.getPath()));
if (!r.getHeaders().isEmpty()) {
sb.append(" Headers:");
for (RequestModel.HeaderEntry h : r.getHeaders())
sb.append(String.format("%n %s: %s", h.getName(), h.getValue()));
sb.append("\n");
}
if (!r.getQueryParams().isEmpty()) {
sb.append(" Query params:");
for (RequestModel.ParamEntry qp : r.getQueryParams())
sb.append(String.format("%n %s=%s", qp.getName(), qp.getValue()));
sb.append("\n");
}
if (r.getBodyData() != null && !r.getBodyData().isEmpty()) {
String body = r.getBodyData().length() > 200
? r.getBodyData().substring(0, 197) + "…"
: r.getBodyData();
sb.append(String.format(" Body: %s%n", body));
}
sb.append("\n");
}
return sb.toString();
}
private void doPreview() {
final String inputPath = inputFileField.getText().trim();
final boolean swaggerSelected = swaggerRadio.isSelected();
statusLabel.setText("Parsing…");
statusLabel.setForeground(Color.DARK_GRAY);
new SwingWorker<String, Void>() {
private int requestCount;
@Override
protected String doInBackground() throws Exception {
List<RequestModel> requests = parseInput(inputPath, swaggerSelected);
requestCount = requests.size();
return buildPreviewText(requests);
}
@Override
protected void done() {
try {
previewArea.setText(get());
previewArea.setCaretPosition(0);
statusLabel.setText("Parsed " + requestCount + " request(s).");
statusLabel.setForeground(new Color(0, 128, 0));
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
previewArea.setText("Error: " + ex.getMessage());
statusLabel.setText("Parse failed.");
statusLabel.setForeground(Color.RED);
} catch (java.util.concurrent.ExecutionException ex) {
Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
previewArea.setText("Error: " + cause.getMessage());
statusLabel.setText("Parse failed.");
statusLabel.setForeground(Color.RED);
}
}
}.execute();

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +254
private void doGenerate() {
statusLabel.setText("Generating…");
statusLabel.setForeground(Color.DARK_GRAY);
SwingUtilities.invokeLater(() -> {
try {
String outputPath = outputFileField.getText().trim();
if (outputPath.isEmpty())
throw new IllegalArgumentException("Please select an output JMX file.");

List<RequestModel> requests = parseInput();
if (requests.isEmpty())
throw new IllegalStateException("No requests found in the selected file.");

String planName = planNameField.getText().trim();
if (planName.isEmpty()) planName = "Imported Test Plan";

int threads = (Integer) threadSpinner.getValue();
new JMeterTestPlanBuilder().writeJmx(requests, planName, threads, outputPath);

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doGenerate() writes the JMX to disk inside SwingUtilities.invokeLater (EDT), so generating a plan from a large spec/collection will block the UI. Please move parsing + file write to a background task and only do UI updates (statusLabel, dialogs) on the EDT.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +28
public class JMeterTestPlanBuilder {

private final AtomicInteger idSeq = new AtomicInteger(100);

/**
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idSeq is declared but never used. Please remove it, or use it to generate deterministic unique IDs if that was the intention (unused fields add noise and can confuse future maintenance).

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +242
private String appendQueryToPath(String path, RequestModel req, boolean hasBody) {
if (!hasBody || req.getQueryParams().isEmpty()) return path;
StringBuilder sb = new StringBuilder(path).append('?');
boolean first = true;
for (RequestModel.ParamEntry qp : req.getQueryParams()) {
if (!first) sb.append('&');
first = false;
sb.append(qp.getName()).append('=').append(qp.getValue());
}
return sb.toString();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When hasBody is true, query params are appended to the path without URL-encoding and without handling an existing query string. This can generate invalid requests when names/values contain reserved characters (space, &, =, %, etc.) or when the path already contains ?. Consider URL-encoding params (and appending with ?/& appropriately) before writing them into HTTPSampler.path.

Copilot uses AI. Check for mistakes.
skipEmptyLines();
if (lineIdx >= lines.length) return null;
String line = lines[lineIdx];
int indent = indentOf(line);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseValue(int parentIndent) never uses parentIndent, so the parameter is effectively dead and makes the parsing logic harder to understand. Either remove the parameter or use it to enforce/validate indentation boundaries when descending (which would also make the parser more robust).

Suggested change
int indent = indentOf(line);
int indent = indentOf(line);
if (parentIndent >= 0 && indent <= parentIndent) return null;

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +19
* Converts the subset of YAML used by standard OpenAPI specs to JSON without
* any external dependencies (uses only the bundled org.json via relocation).
*
* <p>Handles: block/flow mappings, sequences, scalars, quoted strings,
* inline JSON-style objects, multi-line values.</p>
*/
public class YamlToJsonConverter {

private String[] lines;
private int lineIdx;

public String convert(String yaml) {
lines = yaml.replace("\r\n", "\n").replace("\r", "\n").split("\n", -1);
lineIdx = 0;
StringBuilder sb = new StringBuilder();
Object result = parseValue(-1);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a hand-rolled YAML parser/converter for OpenAPI YAML. Even “subset” YAML parsing is easy to get wrong (anchors/aliases, multiline scalars, escaping, indentation edge cases), which can lead to incorrect imports for valid OpenAPI specs. The repo already uses SnakeYAML in other modules (e.g. plugins/json), so it would be safer/less maintenance to reuse a real YAML parser instead of maintaining a custom one.

Suggested change
* Converts the subset of YAML used by standard OpenAPI specs to JSON without
* any external dependencies (uses only the bundled org.json via relocation).
*
* <p>Handles: block/flow mappings, sequences, scalars, quoted strings,
* inline JSON-style objects, multi-line values.</p>
*/
public class YamlToJsonConverter {
private String[] lines;
private int lineIdx;
public String convert(String yaml) {
lines = yaml.replace("\r\n", "\n").replace("\r", "\n").split("\n", -1);
lineIdx = 0;
StringBuilder sb = new StringBuilder();
Object result = parseValue(-1);
* Converts YAML used by standard OpenAPI specs to JSON.
*/
public class YamlToJsonConverter {
public String convert(String yaml) {
Object result = new org.yaml.snakeyaml.Yaml().load(yaml);
StringBuilder sb = new StringBuilder();

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +40
public List<RequestModel> parse(String filePath) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filePath)));

String lower = filePath.toLowerCase();
if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
content = stripYamlComments(content);
content = new YamlToJsonConverter().convert(content);
}

JSONObject root = new JSONObject(content);
List<RequestModel> requests = new ArrayList<>();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing JUnit tests throughout this repo for plugin logic, but this new importer module introduces substantial parsing and JMX-generation behavior without any automated tests. Adding unit tests for representative Swagger/OpenAPI (JSON+YAML) and Postman samples (including auth/header/query/body cases) would help prevent regressions and validate the JMX output.

Copilot uses AI. Check for mistakes.
bakthava and others added 2 commits April 4, 2026 15:30
- Fix Maven layout: move from main/ to src/main/ structure
- Update Maven coordinates: com.example -> kg.apc (kg.apc prefix)
- Update artifactId: swagger-postman-importer -> jmeter-plugin-swagger-postman-importer
- Replace custom YAML parser with SnakeYAML (production-grade)
- Fix unused idSeq field in JMeterTestPlanBuilder
- Add URL encoding for query parameters
- Update service files with new package names
- Fix Maven shading configuration for kg.apc coordinates

All Copilot AI review comments now addressed:
✓ Standard Maven layout with src/main/java
✓ Proper Maven coordinates (kg.apc groupId)
✓ Production YAML parser (SnakeYAML)
✓ Query parameter URL encoding
✓ Code quality improvements
@undera
Copy link
Copy Markdown
Owner

undera commented Apr 6, 2026

We don't accept plugin source code here, we only accept descriptors. Look how other PRs are organized first.

@undera undera closed this Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants