diff --git a/pom.xml b/pom.xml index 96dee20e5..1574f9809 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 11 0.0.0-MASTER-SNAPSHOT - 0.0.0-MASTER-SNAPSHOT + 0.0.0-SED-4497-SNAPSHOT 5.0.4 diff --git a/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageManagerOSTest.java b/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageManagerOSTest.java index b189900fc..a3b6e45c9 100644 --- a/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageManagerOSTest.java +++ b/step-automation-packages/step-automation-packages-controller/src/test/java/step/automation/packages/AutomationPackageManagerOSTest.java @@ -1521,6 +1521,10 @@ private SampleUploadingResult uploadSample1WithAsserts(ObjectId explicitOldId, A assertNotEquals(generalScriptFunction.getScriptFile().get(), r.storedPackage.getAutomationPackageLibraryResource()); } else if (automationPackageFileSource == null) { assertEquals("", generalScriptFunction.getLibrariesFile().get()); + } else if ("KeywordInLib".equals(kwName)) { + //this is a KW declared in a library, its script file should be the AP library + assertEquals(r.storedPackage.getAutomationPackageLibraryResourceRevision(), generalScriptFunction.getScriptFile().get()); + assertTrue(generalScriptFunction.getLibrariesFile().get().isEmpty()); } else { assertEquals(r.storedPackage.getAutomationPackageResourceRevision(), generalScriptFunction.getScriptFile().get()); assertEquals(r.storedPackage.getAutomationPackageLibraryResourceRevision(), generalScriptFunction.getLibrariesFile().get()); diff --git a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java index f8898bd7b..f9d9cf6e3 100644 --- a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java +++ b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java @@ -8,7 +8,6 @@ import step.core.dynamicbeans.DynamicValue; import step.core.plans.Plan; import step.core.scanner.AnnotationScanner; -import step.engine.plugins.LocalFunctionPlugin; import step.functions.Function; import step.functions.manager.FunctionManagerImpl; import step.handlers.javahandler.Keyword; @@ -24,6 +23,8 @@ import java.io.*; import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; @@ -31,6 +32,8 @@ import java.util.List; import java.util.Set; +import static step.plugins.java.GeneralScriptFunction.$_MARK_AS_KEYWORD_FROM_AUTOMATION_PACKAGE_LIBRARY; + public class JavaAutomationPackageReader extends AutomationPackageReader { protected final StepClassParser stepClassParser; @@ -58,9 +61,7 @@ public List getSupportedFileTypes() { @Override protected void fillAutomationPackageWithAnnotatedKeywordsAndPlans(JavaAutomationPackageArchive archive, AutomationPackageContent res) throws AutomationPackageReadingException { try (AnnotationScanner annotationScanner = archive.createAnnotationScanner()) { - // this code duplicates the StepJarParser, but here we don't set the scriptFile and librariesFile to GeneralScriptFunctions - // instead of this we keep the scriptFile blank and fill it further in AutomationPackageKeywordsAttributesApplier (after we upload the jar file as resource) - List scannedKeywords = extractAnnotatedKeywords(annotationScanner, null, null); + List scannedKeywords = extractAnnotatedKeywords(annotationScanner, archive); if (!scannedKeywords.isEmpty()) { log.info("{} annotated keywords found in automation package {}", scannedKeywords.size(), StringUtils.defaultString(archive.getAutomationPackageName())); } @@ -165,7 +166,20 @@ private static List getPlanFromPlansAnnotation(Annotation return result; } - private static List 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;