extractAnnotatedKeywords(AnnotationScanner annotationScanner, String scriptFile, String librariesFile) throws JsonSchemaPreparationException {
+ /**
+ * Extracts annotated Keywords from the provided annotation scanner and AP archive.
+ *
+ * Note: script and library resources are assigned to the Keywords at a later stage,
+ * after the related Step resources have been created. Keywords declared in a library
+ * are marked with the custom field {@code $_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY}
+ * and will use that library as their script JAR.
+ *
+ * @param annotationScanner the scanner backed by a classloader containing both the main AP JAR
+ * and the (optional) library JAR
+ * @param archive the Automation Package (AP archive and its library) from which
+ * Keywords are being extracted
+ */
+ private static List extractAnnotatedKeywords(AnnotationScanner annotationScanner, AutomationPackageArchive archive) throws JsonSchemaPreparationException {
List scannedKeywords = new ArrayList<>();
Set methods = annotationScanner.getMethodsWithAnnotation(Keyword.class);
if (!methods.isEmpty()) {
@@ -187,13 +201,22 @@ private static List extractAnnotatedKeywords(Ann
function.setAttributes(new HashMap<>());
function.getAttributes().put(AbstractOrganizableObject.NAME, functionName);
- // to be filled by AutomationPackageKeywordsAttributesApplier
- if (scriptFile != null) {
- function.setScriptFile(new DynamicValue<>(scriptFile));
- }
-
- if (librariesFile != null) {
- function.setLibrariesFile(new DynamicValue<>(librariesFile));
+ // Determine whether this keyword originates from the library JAR.
+ // If so, the library JAR is its own "main" script file and it has no additional libraries.
+ // Because the actual script and libraries are set later while preparing the staging in applyAutomationPackageContext,
+ // we mark the function with a flag here.
+ // We use ClassGraph's scan metadata (via annotationScanner) to determine the source
+ File libraryFile = archive.getLibraryFile();
+ if (libraryFile != null) {
+ try {
+ URI libraryJarUri = libraryFile.toURI();
+ URL classpathElementUrl = annotationScanner.getClasspathElementUrl(m.getDeclaringClass().getName());
+ if (classpathElementUrl != null && libraryJarUri.equals(classpathElementUrl.toURI())) {
+ function.addCustomField($_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY, true);
+ }
+ } catch (URISyntaxException e) {
+ log.warn("Could not determine classpath element URI for method {}, skipping library JAR check", m.getName(), e);
+ }
}
function.getCallTimeout().setValue(annotation.timeout());
diff --git a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
index c21da6031..f295fc5cd 100644
--- a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java
@@ -18,9 +18,6 @@
******************************************************************************/
package step.automation.packages;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.io.*;
import java.net.URL;
import java.util.List;
@@ -29,16 +26,15 @@
public abstract class AutomationPackageArchive implements Closeable {
- private static final Logger log = LoggerFactory.getLogger(AutomationPackageArchive.class);
public static final List METADATA_FILES = List.of("automation-package.yml", "automation-package.yaml");
public static final String NULL_TYPE_ERROR_MSG = "The type of the AutomationPackageArchive must not be null";
private final File originalFile;
- private final File keywordLibFile;
+ private final File libraryFile;
private final String type;
private final String archiveName;
- public AutomationPackageArchive(File automationPackageFile, File keywordLibFile, String type, String archiveName) throws AutomationPackageReadingException {
+ public AutomationPackageArchive(File automationPackageFile, File libraryFile, String type, String archiveName) throws AutomationPackageReadingException {
Objects.requireNonNull(automationPackageFile, "The automationPackageFile must not be null");
Objects.requireNonNull(automationPackageFile, NULL_TYPE_ERROR_MSG);
this.archiveName = archiveName;
@@ -46,7 +42,7 @@ public AutomationPackageArchive(File automationPackageFile, File keywordLibFile,
throw new AutomationPackageReadingException("Automation package " + automationPackageFile.getName() + " doesn't exist");
}
this.originalFile = automationPackageFile;
- this.keywordLibFile = keywordLibFile;
+ this.libraryFile = libraryFile;
this.type = type;
}
@@ -69,8 +65,8 @@ public String getAutomationPackageName() {
abstract public List getResourcesByPattern(String resourcePathPattern);
- public File getKeywordLibFile() {
- return keywordLibFile;
+ public File getLibraryFile() {
+ return libraryFile;
}
public File getOriginalFile() {
diff --git a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
index 41eb5f009..21256d92d 100644
--- a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
+++ b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java
@@ -40,7 +40,7 @@ public class JavaAutomationPackageArchive extends AutomationPackageArchive {
public static final List METADATA_FILES = List.of("automation-package.yml", "automation-package.yaml");
private final ClassLoader classLoaderForMainApFile;
- private final ClassLoader classLoaderForApAndLibraries;
+ private final URLClassLoader classLoaderForApAndLibraries;
private boolean internalClassLoader = false;
private final ResourcePathMatchingResolver pathMatchingResourceResolver;
@@ -81,7 +81,9 @@ public JavaAutomationPackageArchive(File automationPackageFile, File keywordLibF
this.pathMatchingResourceResolver = new ResourcePathMatchingResolver(classLoaderForMainApFile);
// IMPORTANT!!! The class loader used to scan plans and keywords by annotations should contain all the classes from AP file and keyword lib
- // (inclusive the parent classloader)
+ // 1. because Keywords declared in the AP file but using classes from the library will throw java.lang.NoClassDefFoundError otherwise
+ // 2. because we also want to include Keywords and Plans declared as code in the library.
+ // However, Keyword from the library will be created using the library as main Jar file
this.classLoaderForApAndLibraries = createClassloaderForApWithKeywordLib(automationPackageFile, keywordLibFile);
} catch (MalformedURLException ex) {
throw new AutomationPackageReadingException("Unable to read automation package", ex);
@@ -162,7 +164,7 @@ public ClassLoader getClassLoaderForApAndLibraries() {
}
public AnnotationScanner createAnnotationScanner() {
- return AnnotationScanner.forSpecificJarFromURLClassLoader((URLClassLoader) getClassLoaderForApAndLibraries());
+ return AnnotationScanner.forSpecificJarFromURLClassLoader(classLoaderForApAndLibraries);
}
@Override
diff --git a/step-functions-plugins/step-functions-plugins-java/step-functions-plugins-java-def/src/main/java/step/plugins/java/GeneralScriptFunction.java b/step-functions-plugins/step-functions-plugins-java/step-functions-plugins-java-def/src/main/java/step/plugins/java/GeneralScriptFunction.java
index 6fd0d215b..c4547c798 100644
--- a/step-functions-plugins/step-functions-plugins-java/step-functions-plugins-java-def/src/main/java/step/plugins/java/GeneralScriptFunction.java
+++ b/step-functions-plugins/step-functions-plugins-java/step-functions-plugins-java-def/src/main/java/step/plugins/java/GeneralScriptFunction.java
@@ -33,6 +33,8 @@
*/
public class GeneralScriptFunction extends Function implements AutomationPackageContextual {
+ public static final String $_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY = "$markAsKeywordFromAutomationPackageLibrary";
+
DynamicValue scriptFile = new DynamicValue<>("");
DynamicValue scriptLanguage = new DynamicValue<>("");
@@ -91,15 +93,33 @@ public void setErrorHandlerFile(DynamicValue errorHandlerFile) {
@Override
public GeneralScriptFunction applyAutomationPackageContext(StagingAutomationPackageContext context) {
+ //Only process function without script file set (i.e. Keywords from scanned annotations)
if (getScriptFile().get() == null || getScriptFile().get().isEmpty()) {
AutomationPackage ap = context.getAutomationPackage();
- if (ap != null && ap.getAutomationPackageResourceRevision() != null && !ap.getAutomationPackageResourceRevision().isEmpty()) {
- setScriptFile(new DynamicValue<>(ap.getAutomationPackageResourceRevision()));
- } else {
- throw new RuntimeException("General script functions can only be used within automation package archive");
+ if (ap == null) {
+ throw new RuntimeException("General script functions defined in Automation Packages must either be declared in the descriptor providing an explicit script file or with Keyword annotation.");
}
- if (ap != null && ap.getAutomationPackageLibraryResourceRevision() != null && !ap.getAutomationPackageLibraryResourceRevision().isEmpty()) {
- setLibrariesFile(new DynamicValue<>(ap.getAutomationPackageLibraryResourceRevision()));
+ String automationPackageLibraryResourceRevision = ap.getAutomationPackageLibraryResourceRevision();
+ boolean hasLibrary = automationPackageLibraryResourceRevision != null && !automationPackageLibraryResourceRevision.isEmpty();
+ //Handle Keywords declared in AP library, the library is used as script file for them
+ if (getCustomField($_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY) != null) {
+ getCustomFields().remove($_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY);
+ if (hasLibrary) {
+ setScriptFile(new DynamicValue<>(automationPackageLibraryResourceRevision));
+ } else {
+ throw new RuntimeException("Inconsistent state: the annotated Keyword '" + this.getAttribute(NAME) + "' was detected in an Automation Package Library, but the library resource does not exist.");
+ }
+ } else {
+ //Keyword annotated in main AP file
+ String automationPackageResourceRevision = ap.getAutomationPackageResourceRevision();
+ if (automationPackageResourceRevision != null && !automationPackageResourceRevision.isEmpty()) {
+ setScriptFile(new DynamicValue<>(automationPackageResourceRevision));
+ } else {
+ throw new RuntimeException("Inconsistent state: the annotated Keyword '" + this.getAttribute(NAME) + "' was detected in an Automation Package, but the package resource does not exist.");
+ }
+ if (hasLibrary) {
+ setLibrariesFile(new DynamicValue<>(automationPackageLibraryResourceRevision));
+ }
}
}
return this;