diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ec95720
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,140 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/intellij,java
+# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,java
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+/target/
+.idea/
+*.iml
+.DS_Store
+# End of https://www.toptal.com/developers/gitignore/api/intellij,java
\ No newline at end of file
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..43aac6c
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b3fcc68
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,286 @@
+
+
+ 4.0.0
+
+ com.blazemeter
+ jmeter-bzm-commons
+ jar
+ 0.1
+ BlazeMeter JMeter commons
+ This project holds common utilities used by BlazeMeter plugin projects.
+ https://github.com/Blazemeter/jmeter-bzm-commons
+
+
+
+ The Apache License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+ Blazemeter
+ ops@blazemeter.com
+ Blazemeter
+ https://blazemeter.com/
+
+
+
+
+ UTF-8
+ UTF-8
+
+
+
+ scm:git:git://github.com/Blazemeter/jmeter-bzm-commons.git
+ scm:git:ssh://github.com:Blazemeter/jmeter-bzm-commons.git
+ https://github.com/Blazemeter/jmeter-bzm-commons/tree/master
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+
+ org.apache.jmeter
+ ApacheJMeter_core
+ 4.0
+ provided
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.1.0
+ maven-plugin
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+ true
+
+
+
+ agent-for-ut
+
+ prepare-agent
+
+
+ jacoco.agent.ut.arg
+
+
+
+ agent-for-it
+
+ prepare-agent-integration
+
+
+ jacoco.agent.it.arg
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ ${jacoco.agent.ut.arg}
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ ${jacoco.agent.it.arg}
+ ${project.build.directory}/surefire-reports
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ 8
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.1.0
+
+
+ validate
+ validate
+
+ checkstyle.xml
+ true
+ false
+ true
+
+
+ check
+
+
+
+
+
+
+
+
+
+ sonar
+
+
+
+
+ org.sonarsource.scanner.maven
+ sonar-maven-plugin
+ 3.6.0.1398
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+ true
+
+
+
+ agent-for-ut
+
+ prepare-agent
+
+
+ jacoco.agent.ut.arg
+
+
+
+ agent-for-it
+
+ prepare-agent-integration
+
+
+ jacoco.agent.it.arg
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ ${jacoco.agent.ut.arg}
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ ${jacoco.agent.it.arg}
+ ${project.build.directory}/surefire-reports
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
+ release
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.8
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..e8e8b2d
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,26 @@
+# BlazeMeter JMeter commons
+
+This repository holds common utilities used by BlazeMeter plugin projects, avoiding duplication between them.
+
+This is a list of the features and projects that use it:
+
+
+| | CR | Cx | RTE | HLS | HTTP2 |
+|-------------------------|----|----|-----|----- |-------|
+| BlazeMeter Labs Logo | ☑ | ✅ | ☑ | ☑ | ☑ |
+| Collapsible Panels | ☑ | ☑ | | ||
+| Fields with validation | ☑ | ✅ | | ||
+| Fields with placeholder | ☑ | ✅ | | ||
+| Themed icons | ☑ | ✅ | ☑ | ☑ | ☑ |
+| ButtonBuilder | ☑ | ✅ | | ||
+
+Legend:
+* CR: Correlation Recorder
+* Cx: Citrix Plugin
+* RTE: RTE Plugin
+* HLS: HLS Plugin
+* HTTP2: HTTP2 Plugin
+
+☑ = Functionality used
+
+✅ = Functionality migrated (using this project)
diff --git a/src/main/java/com/blazemeter/jmeter/commons/BlazemeterLabsLogo.java b/src/main/java/com/blazemeter/jmeter/commons/BlazemeterLabsLogo.java
new file mode 100644
index 0000000..4910ac8
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/BlazemeterLabsLogo.java
@@ -0,0 +1,48 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Cursor;
+import java.awt.Desktop;
+import java.awt.Graphics;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BlazemeterLabsLogo extends JLabel {
+
+ private static final Logger LOG = LoggerFactory.getLogger(BlazemeterLabsLogo.class);
+ private static final ImageIcon BLAZEMETER_LOGO = ThemedIcon
+ .fromResourceName("blazemeter-labs-logo.png");
+
+ public BlazemeterLabsLogo(String pageURL) {
+ super(BLAZEMETER_LOGO);
+ setBrowseOnClick(pageURL);
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ setIcon(BLAZEMETER_LOGO);
+ super.paint(g);
+ }
+
+ private void setBrowseOnClick(String url) {
+ setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent mouseEvent) {
+ if (Desktop.isDesktopSupported()) {
+ try {
+ Desktop.getDesktop().browse(new URI(url));
+ } catch (IOException | URISyntaxException exception) {
+ LOG.error("Problem when accessing repository", exception);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/CollapsiblePanel.java b/src/main/java/com/blazemeter/jmeter/commons/CollapsiblePanel.java
new file mode 100644
index 0000000..dd16352
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/CollapsiblePanel.java
@@ -0,0 +1,308 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+public class CollapsiblePanel extends JPanel {
+
+ public static final int PREFERED_HEIGHT = 40;
+ private final Header header;
+ private final JComponent contentComponent;
+ private boolean collapsed;
+
+ private CollapsiblePanel(Builder builder) {
+ setBackground(Color.darkGray);
+ this.header = new Header(builder);
+ this.contentComponent = builder.content;
+
+ setName(builder.namePrefix + "-collapsiblePanel");
+ setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+
+ add(this.header);
+ add(this.contentComponent);
+ if (builder.isCollapsed) {
+ toggleCollapsed();
+ }
+ }
+
+ public void toggleCollapsed() {
+ collapsed = !collapsed;
+ header.toggleCollapsed();
+ contentComponent.setVisible(!collapsed);
+ }
+
+ public boolean isCollapsed() {
+ return collapsed;
+ }
+
+ public JPanel getHeaderPanel() {
+ return this.header;
+ }
+
+ public void setEnabled(boolean enabled) {
+ header.setEnabled(enabled);
+ }
+
+ public String getId() {
+ return header.getPanelTitle();
+ }
+
+ private class Header extends JPanel {
+
+ private final JTextField name;
+ private final JPanel buttonsPanel;
+ private final ImageIcon collapsedIcon = ThemedIcon.fromResourceName("collapsed.png");
+ private final ImageIcon expandedIcon = ThemedIcon.fromResourceName("expanded.png");
+ private final JButton collapseButton;
+ private boolean enable;
+
+ private Header(Builder builder) {
+ //Used to avoid issues while testing
+ setName(builder.namePrefix + "-collapsiblePanel-header");
+ setBorder(BorderFactory.createTitledBorder(""));
+ setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+ this.enable = builder.isEnabled;
+ if (builder.enablingListener != null) {
+ /*
+ we pass each field of builder instead of builder as parameter to avoid tying this instance
+ listeners to builder, which might change after creating this object.
+ */
+ add(buildEnableCheckBox(builder.enablingListener));
+ addGap();
+ }
+ name = buildNameField(builder.title, builder.editableTitle);
+ addFocusListener(buildDisableNameOnFocusLostListener(name));
+ add(name);
+ if (builder.editableTitle) {
+ addGap();
+ add(buildEditTitleIcon(name));
+ addGap();
+ }
+ add(Box.createHorizontalGlue());
+ buttonsPanel = buildButtonsPanel(builder.buttons);
+ add(buttonsPanel);
+ add(Box.createHorizontalGlue());
+ collapseButton = buildCollapseButton(builder.collapsingListener);
+ add(collapseButton);
+ setMaximumSize(new Dimension(Integer.MAX_VALUE, calcHeight(builder.buttons)));
+ }
+
+ private JCheckBox buildEnableCheckBox(Consumer enablingListener) {
+ JCheckBox ret = SwingUtils
+ .createComponent(getName() + "-disableCheck", new JCheckBox());
+ ret.setSelected(this.enable);
+ ret.addItemListener(e -> {
+ this.enable = !this.enable;
+ ret.setSelected(this.enable);
+ enablingListener.accept(this.enable);
+ });
+ return ret;
+ }
+
+ private void addGap() {
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ }
+
+ private JTextField buildNameField(String title, boolean editable) {
+ JTextField ret = new JTextField(title, 10);
+ ret.setName(getName() + "-title");
+ ret.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 10));
+ ret.setEditable(false);
+ if (editable) {
+ ret.addFocusListener(buildDisableNameOnFocusLostListener(ret));
+ ret.addActionListener(e -> ret.setEditable(false));
+ }
+ return ret;
+ }
+
+ private FocusAdapter buildDisableNameOnFocusLostListener(JTextField name) {
+ return new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ name.setEditable(false);
+ }
+ };
+ }
+
+ private JLabel buildEditTitleIcon(JTextField nameField) {
+ JLabel ret = new JLabel();
+ ret.setIcon(ThemedIcon.fromResourceName("pencil-edit.png"));
+ ret.setName(getName() + "-editTitleIcon");
+ ret.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ nameField.setEditable(true);
+ nameField.requestFocus();
+ }
+
+ public void mouseEntered(MouseEvent e) {
+ setCursor(Cursor.HAND_CURSOR);
+ }
+
+ public void mouseExited(MouseEvent e) {
+ setCursor(Cursor.DEFAULT_CURSOR);
+ }
+ });
+ return ret;
+ }
+
+ private JPanel buildButtonsPanel(List buttons) {
+ JPanel ret = new JPanel();
+ ret.setLayout(new BoxLayout(ret, BoxLayout.LINE_AXIS));
+ ret.setName(getName() + "-buttonsPanel");
+ for (JButton button : buttons) {
+ ret.add(button);
+ ret.add(Box.createRigidArea(new Dimension(10, 0)));
+ }
+ return ret;
+ }
+
+ private JButton buildCollapseButton(Runnable collapsingListeners) {
+ JButton ret = new JButton(expandedIcon);
+ ret.setName(getName() + "-collapseButton");
+ //Making the button looks like a label
+ ret.setFocusPainted(false);
+ ret.setMargin(new Insets(0, 0, 0, 0));
+ ret.setContentAreaFilled(false);
+ ret.setBorderPainted(false);
+ ret.setOpaque(false);
+ ret.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ CollapsiblePanel.this.toggleCollapsed();
+ if (collapsingListeners != null) {
+ collapsingListeners.run();
+ }
+ }
+
+ public void mouseEntered(MouseEvent e) {
+ setCursor(Cursor.HAND_CURSOR);
+ }
+
+ public void mouseExited(MouseEvent e) {
+ setCursor(Cursor.DEFAULT_CURSOR);
+ }
+ });
+ return ret;
+ }
+
+ private int calcHeight(List buttons) {
+ return buttons.stream()
+ .mapToInt(button -> (int) button.getMinimumSize().getHeight())
+ .max()
+ .orElse(PREFERED_HEIGHT);
+ }
+
+ public void setEnabled(boolean enabled) {
+ name.setEditable(true);
+ name.setForeground(
+ enabled ? new JTextField().getForeground() : new JTextField().getDisabledTextColor());
+ name.setEditable(false);
+ }
+
+ private String getPanelTitle() {
+ return name.getText();
+ }
+
+ private void setCursor(int cursor) {
+ this.setCursor(Cursor.getPredefinedCursor(cursor));
+ }
+
+ private void toggleCollapsed() {
+ boolean isCollapsed = collapseButton.getIcon().equals(expandedIcon);
+ buttonsPanel.setVisible(!isCollapsed);
+ collapseButton.setIcon(isCollapsed ? collapsedIcon : expandedIcon);
+ }
+
+ @Override
+ public int getHeight() {
+ return PREFERED_HEIGHT;
+ }
+
+ }
+
+ public static final class Builder {
+
+ private String namePrefix = "";
+ private String title = "";
+ private boolean editableTitle = false;
+ private List buttons = Collections.emptyList();
+ private Runnable collapsingListener;
+ private boolean isEnabled = true;
+ private boolean isCollapsed = false;
+ private Consumer enablingListener;
+ private JComponent content;
+
+ public Builder() {
+ }
+
+ public Builder withNamePrefix(String prefix) {
+ this.namePrefix = prefix;
+ return this;
+ }
+
+ public Builder withTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Builder withEditableTitle() {
+ this.editableTitle = true;
+ return this;
+ }
+
+ public Builder withButtons(List buttons) {
+ this.buttons = buttons;
+ return this;
+ }
+
+ public Builder withCollapsingListener(Runnable collapsingListener) {
+ this.collapsingListener = collapsingListener;
+ return this;
+ }
+
+ public Builder withEnablingListener(Consumer enablingListener) {
+ this.enablingListener = enablingListener;
+ return this;
+ }
+
+ public Builder withEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ return this;
+ }
+
+ public Builder withCollapsed(boolean isCollapsed) {
+ this.isCollapsed = isCollapsed;
+ return this;
+ }
+
+ public Builder withContent(JComponent content) {
+ this.content = content;
+ return this;
+ }
+
+ public CollapsiblePanel build() {
+ return new CollapsiblePanel(this);
+ }
+
+ }
+}
+
diff --git a/src/main/java/com/blazemeter/jmeter/commons/FieldValidations.java b/src/main/java/com/blazemeter/jmeter/commons/FieldValidations.java
new file mode 100644
index 0000000..f381271
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/FieldValidations.java
@@ -0,0 +1,60 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+import javax.swing.border.Border;
+
+public class FieldValidations {
+ private final Border defaultBorder = new JTextField().getBorder();
+ private final Border invalidBorder = BorderFactory.createLineBorder(Color.red);
+
+ private JTextField field;
+ private final JLabel error;
+ private List validations = new ArrayList<>();
+
+ public FieldValidations(JTextField field, JLabel error) {
+ this.field = field;
+ this.error = error;
+ }
+
+ public void setField(JTextField field) {
+ this.field = field;
+ }
+
+ public JTextField getField() {
+ return field;
+ }
+
+ public void addValidations(Validation... validation) {
+ validations = Arrays.asList(validation);
+ }
+
+ public List getValidations() {
+ return validations;
+ }
+
+ public void applyFormat() {
+ updateValidationStates();
+ boolean valid = isValid();
+ field.setBorder(valid ? defaultBorder : invalidBorder);
+ error.setVisible(!valid);
+ error.setText(valid ? "" : validations.stream()
+ .filter(validation -> !validation.isValid())
+ .map(Validation::getErrorMessage)
+ .collect(Collectors.joining(". ")));
+ }
+
+ public void updateValidationStates() {
+ validations.forEach(validation -> validation.updateState(field.getText()));
+ }
+
+ public boolean isValid() {
+ return validations.stream().allMatch(Validation::isValid);
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/FormValidation.java b/src/main/java/com/blazemeter/jmeter/commons/FormValidation.java
new file mode 100644
index 0000000..ed18e49
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/FormValidation.java
@@ -0,0 +1,76 @@
+package com.blazemeter.jmeter.commons;
+
+import java.util.List;
+import javax.swing.JTextField;
+
+public class FormValidation {
+ private final List fieldValidations;
+ private Runnable onValid;
+ private Runnable onInvalid;
+ private boolean active = true;
+
+ public FormValidation(List fieldValidations) {
+ this.fieldValidations = fieldValidations;
+ }
+
+ public void onSuccess(Runnable runnable) {
+ this.onValid = runnable;
+ }
+
+ public void onFailure(Runnable runnable) {
+ this.onInvalid = runnable;
+ }
+
+ public boolean isValid() {
+ fieldValidations.forEach(FieldValidations::updateValidationStates);
+ return fieldValidations.stream().allMatch(FieldValidations::isValid);
+ }
+
+ public void updateFormats(JTextField source) {
+ boolean allFieldsValid = true;
+ for (FieldValidations fieldValidations : fieldValidations) {
+ fieldValidations.updateValidationStates();
+
+ //If at least 1 field is invalid, the form is invalid
+ if (!fieldValidations.isValid()) {
+ allFieldsValid = false;
+ }
+
+ //Apply the format to the field if the triggering field is
+ // the same as the field being validated
+ if (fieldValidations.getField().equals(source)) {
+ fieldValidations.applyFormat();
+ }
+ }
+
+ if (allFieldsValid) {
+ onValid.run();
+ } else {
+ onInvalid.run();
+ }
+ }
+
+ public void applyFormats() {
+ fieldValidations.forEach(FieldValidations::applyFormat);
+ }
+
+ public void validate(JTextField field) {
+ updateFormats(field);
+ }
+
+ public void validate() {
+ if (!active) {
+ return;
+ }
+
+ if (isValid()) {
+ onValid.run();
+ } else {
+ onInvalid.run();
+ }
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderPassword.java b/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderPassword.java
new file mode 100644
index 0000000..06c5d6e
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderPassword.java
@@ -0,0 +1,41 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import javax.swing.JPasswordField;
+
+public class PlaceHolderPassword extends JPasswordField {
+ private String placeHolder = "";
+
+ public PlaceHolderPassword() {
+ this(null);
+ }
+
+ public PlaceHolderPassword(String text) {
+ super(text);
+ }
+
+ @Override
+ protected void paintComponent(Graphics pG) {
+ super.paintComponent(pG);
+
+ if (placeHolder == null || placeHolder.length() == 0 || getPassword().length > 0) {
+ return;
+ }
+
+ Graphics2D g = (Graphics2D) pG;
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setColor(getDisabledTextColor());
+ g.drawString(placeHolder, getInsets().left, pG.getFontMetrics()
+ .getMaxAscent() + getInsets().top);
+ }
+
+ public void setPlaceHolder(String placeHolder) {
+ this.placeHolder = placeHolder;
+ }
+
+ public String getPlaceHolder() {
+ return placeHolder;
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderTextField.java b/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderTextField.java
new file mode 100644
index 0000000..21ae583
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/PlaceHolderTextField.java
@@ -0,0 +1,42 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import javax.swing.JTextField;
+
+public class PlaceHolderTextField extends JTextField {
+
+ private String placeHolder = "";
+
+ public PlaceHolderTextField() {
+ this(null);
+ }
+
+ public PlaceHolderTextField(String text) {
+ super(text);
+ }
+
+ @Override
+ protected void paintComponent(Graphics pG) {
+ super.paintComponent(pG);
+
+ if (placeHolder == null || placeHolder.length() == 0 || getText().length() > 0) {
+ return;
+ }
+
+ Graphics2D g = (Graphics2D) pG;
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setColor(getDisabledTextColor());
+ g.drawString(placeHolder, getInsets().left, pG.getFontMetrics()
+ .getMaxAscent() + getInsets().top);
+ }
+
+ public void setPlaceHolder(String placeHolder) {
+ this.placeHolder = placeHolder;
+ }
+
+ public String getPlaceHolder() {
+ return placeHolder;
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/StringUtils.java b/src/main/java/com/blazemeter/jmeter/commons/StringUtils.java
new file mode 100644
index 0000000..31af7fa
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/StringUtils.java
@@ -0,0 +1,10 @@
+package com.blazemeter.jmeter.commons;
+
+public class StringUtils {
+
+ public static String capitalize(String text) {
+ return text != null && !text.isEmpty() ? text.substring(0, 1).toUpperCase() + text.substring(1)
+ : text;
+ }
+
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/SwingUtils.java b/src/main/java/com/blazemeter/jmeter/commons/SwingUtils.java
new file mode 100644
index 0000000..a386f46
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/SwingUtils.java
@@ -0,0 +1,79 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.event.ActionListener;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import org.apache.jmeter.util.JMeterUtils;
+
+public class SwingUtils {
+ public static T createComponent(String name, T component) {
+ component.setName(name);
+ return component;
+ }
+
+ public static final class ButtonBuilder {
+
+ private ActionListener actionListener;
+ private String action;
+ private String name;
+ private String iconName = "";
+ private ImageIcon icon;
+ private boolean enabled = true;
+ private boolean hasText = true;
+
+ public ButtonBuilder() {
+
+ }
+
+ public ButtonBuilder withActionListener(ActionListener actionListener) {
+ this.actionListener = actionListener;
+ return this;
+ }
+
+ public ButtonBuilder withAction(String action) {
+ this.action = action;
+ return this;
+ }
+
+ public ButtonBuilder withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ButtonBuilder withIcon(String iconName) {
+ this.iconName = iconName;
+ return this;
+ }
+
+ public ButtonBuilder withIcon(ImageIcon icon) {
+ this.icon = icon;
+ return this;
+ }
+
+ public ButtonBuilder isEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ public ButtonBuilder hasText(boolean hasText) {
+ this.hasText = hasText;
+ return this;
+ }
+
+ public JButton build() {
+ String parsedName = JMeterUtils.getResString(name);
+
+ JButton button = createComponent(name + "Button", new JButton());
+
+ button.setActionCommand(action);
+ button.addActionListener(actionListener);
+ button.setEnabled(enabled);
+ button.setIcon(iconName.isEmpty() ? icon : ThemedIcon.fromResourceName(iconName));
+ button.setText(!hasText ? ""
+ : parsedName.contains("res_key") ? StringUtils.capitalize(name) : parsedName);
+ return button;
+ }
+ }
+
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/ThemeUtils.java b/src/main/java/com/blazemeter/jmeter/commons/ThemeUtils.java
new file mode 100644
index 0000000..cc96740
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/ThemeUtils.java
@@ -0,0 +1,14 @@
+package com.blazemeter.jmeter.commons;
+
+import java.util.regex.Pattern;
+import org.apache.jmeter.gui.action.LookAndFeelCommand;
+
+public class ThemeUtils {
+
+ private static final Pattern DARK_THEME_PATTERN = Pattern
+ .compile("Intellij|HighContrastLight|HighContrastDark|Darcula|Motif|OneDark|SolarizedDark");
+
+ public static boolean isDark() {
+ return DARK_THEME_PATTERN.matcher(LookAndFeelCommand.getJMeterLaf()).find();
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/ThemedIcon.java b/src/main/java/com/blazemeter/jmeter/commons/ThemedIcon.java
new file mode 100644
index 0000000..75970b7
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/ThemedIcon.java
@@ -0,0 +1,22 @@
+package com.blazemeter.jmeter.commons;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+import javax.swing.ImageIcon;
+
+public class ThemedIcon {
+
+ private static final Map CACHED_ICONS = new WeakHashMap<>();
+ private static final String RESOURCE_SEPARATOR = "/";
+
+ public static ImageIcon fromResourceName(String resourceName) {
+ String resourcePath = getThemePath() + RESOURCE_SEPARATOR + resourceName;
+ return CACHED_ICONS
+ .computeIfAbsent(resourcePath, p -> new ImageIcon(ThemedIcon.class.getResource(p)));
+ }
+
+ private static String getThemePath() {
+ return ThemeUtils.isDark() ? "/dark-theme" : "/light-theme";
+ }
+
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/ThemedIconLabel.java b/src/main/java/com/blazemeter/jmeter/commons/ThemedIconLabel.java
new file mode 100644
index 0000000..49f40bd
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/ThemedIconLabel.java
@@ -0,0 +1,20 @@
+package com.blazemeter.jmeter.commons;
+
+import java.awt.Graphics;
+import javax.swing.JLabel;
+
+public class ThemedIconLabel extends JLabel {
+
+ private final String iconResourceName;
+
+ public ThemedIconLabel(String iconResourceName) {
+ super(ThemedIcon.fromResourceName(iconResourceName));
+ this.iconResourceName = iconResourceName;
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ setIcon(ThemedIcon.fromResourceName(iconResourceName));
+ super.paint(g);
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/commons/Validation.java b/src/main/java/com/blazemeter/jmeter/commons/Validation.java
new file mode 100644
index 0000000..317e054
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/commons/Validation.java
@@ -0,0 +1,27 @@
+package com.blazemeter.jmeter.commons;
+
+import java.util.function.Predicate;
+
+public class Validation {
+
+ private final Predicate condition;
+ private final String errorMessage;
+ private boolean valid;
+
+ public Validation(Predicate condition, String errorMessage) {
+ this.condition = condition;
+ this.errorMessage = errorMessage;
+ }
+
+ public void updateState(String value) {
+ this.valid = condition.test(value);
+ }
+
+ public boolean isValid() {
+ return valid;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+}
diff --git a/src/main/resources/dark-theme/blazemeter-labs-logo.png b/src/main/resources/dark-theme/blazemeter-labs-logo.png
new file mode 100644
index 0000000..917ecb1
Binary files /dev/null and b/src/main/resources/dark-theme/blazemeter-labs-logo.png differ
diff --git a/src/main/resources/dark-theme/collapsed.png b/src/main/resources/dark-theme/collapsed.png
new file mode 100644
index 0000000..c8b0ddb
Binary files /dev/null and b/src/main/resources/dark-theme/collapsed.png differ
diff --git a/src/main/resources/dark-theme/expanded.png b/src/main/resources/dark-theme/expanded.png
new file mode 100644
index 0000000..e029ca4
Binary files /dev/null and b/src/main/resources/dark-theme/expanded.png differ
diff --git a/src/main/resources/dark-theme/not-visible-credentials.png b/src/main/resources/dark-theme/not-visible-credentials.png
new file mode 100644
index 0000000..0cbfde3
Binary files /dev/null and b/src/main/resources/dark-theme/not-visible-credentials.png differ
diff --git a/src/main/resources/dark-theme/visible-credentials.png b/src/main/resources/dark-theme/visible-credentials.png
new file mode 100644
index 0000000..4b78520
Binary files /dev/null and b/src/main/resources/dark-theme/visible-credentials.png differ
diff --git a/src/main/resources/light-theme/blazemeter-labs-logo.png b/src/main/resources/light-theme/blazemeter-labs-logo.png
new file mode 100644
index 0000000..beb7b9b
Binary files /dev/null and b/src/main/resources/light-theme/blazemeter-labs-logo.png differ
diff --git a/src/main/resources/light-theme/collapsed.png b/src/main/resources/light-theme/collapsed.png
new file mode 100644
index 0000000..87d0d6c
Binary files /dev/null and b/src/main/resources/light-theme/collapsed.png differ
diff --git a/src/main/resources/light-theme/expanded.png b/src/main/resources/light-theme/expanded.png
new file mode 100644
index 0000000..9e79a39
Binary files /dev/null and b/src/main/resources/light-theme/expanded.png differ
diff --git a/src/main/resources/light-theme/not-visible-credentials.png b/src/main/resources/light-theme/not-visible-credentials.png
new file mode 100644
index 0000000..948bfaa
Binary files /dev/null and b/src/main/resources/light-theme/not-visible-credentials.png differ
diff --git a/src/main/resources/light-theme/visible-credentials.png b/src/main/resources/light-theme/visible-credentials.png
new file mode 100644
index 0000000..de8b55b
Binary files /dev/null and b/src/main/resources/light-theme/visible-credentials.png differ