diff --git a/src/main/java/org/apache/nifi/NarMojo.java b/src/main/java/org/apache/nifi/NarMojo.java index ba9965f..6758f6e 100644 --- a/src/main/java/org/apache/nifi/NarMojo.java +++ b/src/main/java/org/apache/nifi/NarMojo.java @@ -115,7 +115,9 @@ @Mojo(name = "nar", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true, requiresDependencyResolution = ResolutionScope.RUNTIME) public class NarMojo extends AbstractMojo { private static final String CONTROLLER_SERVICE_CLASS_NAME = "org.apache.nifi.controller.ControllerService"; + private static final String CONNECTOR_CLASS_NAME = "org.apache.nifi.components.connector.Connector"; private static final String DOCUMENTATION_WRITER_CLASS_NAME = "org.apache.nifi.documentation.xml.XmlDocumentationWriter"; + private static final String CONNECTOR_DOCUMENTATION_WRITER_CLASS_NAME = "org.apache.nifi.documentation.xml.XmlConnectorDocumentationWriter"; private static final String[] DEFAULT_EXCLUDES = new String[]{"**/package.html"}; private static final String[] DEFAULT_INCLUDES = new String[]{"**/**"}; @@ -553,6 +555,9 @@ private void generateDocumentation() throws MojoExecutionException { final File additionalDetailsDir = new File(docsFile.getParentFile(), "additional-details"); createDirectory(additionalDetailsDir); + final File stepDocumentationDir = new File(docsFile.getParentFile(), "steps"); + createDirectory(stepDocumentationDir); + try (final OutputStream out = new FileOutputStream(docsFile)) { final XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(out, "UTF-8"); @@ -629,6 +634,20 @@ private void generateDocumentation() throws MojoExecutionException { final Set flowRegistryClientDefinitions = extensionDefinitionFactory.discoverExtensions(ExtensionType.FLOW_REGISTRY_CLIENT); writeDocumentation(flowRegistryClientDefinitions, extensionClassLoader, docWriterClass, xmlWriter, additionalDetailsDir); + + // Connectors use a separate documentation writer since they are not ConfigurableComponents + final Set connectorDefinitions = extensionDefinitionFactory.discoverExtensions(ExtensionType.CONNECTOR); + if (!connectorDefinitions.isEmpty()) { + Class connectorDocWriterClass = null; + try { + connectorDocWriterClass = Class.forName(CONNECTOR_DOCUMENTATION_WRITER_CLASS_NAME, false, extensionClassLoader); + } catch (ClassNotFoundException e) { + getLog().warn("Cannot locate class " + CONNECTOR_DOCUMENTATION_WRITER_CLASS_NAME + ", so no documentation will be generated for Connectors in this NAR"); + } + if (connectorDocWriterClass != null) { + writeConnectorDocumentation(connectorDefinitions, extensionClassLoader, connectorDocWriterClass, xmlWriter, stepDocumentationDir); + } + } } finally { if (currentContextClassLoader != null) { Thread.currentThread().setContextClassLoader(currentContextClassLoader); @@ -705,6 +724,46 @@ private void writeDocumentation(final ExtensionDefinition extensionDefinition, f } } + private void writeConnectorDocumentation(final Set extensionDefinitions, final ExtensionClassLoader classLoader, + final Class connectorDocWriterClass, final XMLStreamWriter xmlWriter, final File stepDocumentationDir) + throws InvocationTargetException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { + + final Set sorted = new TreeSet<>(Comparator.comparing(ExtensionDefinition::getExtensionName)); + sorted.addAll(extensionDefinitions); + + for (final ExtensionDefinition definition : sorted) { + writeConnectorDocumentation(definition, classLoader, connectorDocWriterClass, xmlWriter); + } + + final Set connectorNames = sorted.stream() + .map(ExtensionDefinition::getExtensionName) + .collect(Collectors.toSet()); + + try { + writeStepDocumentation(classLoader, connectorNames, stepDocumentationDir); + } catch (final Exception e) { + throw new IOException("Unable to extract Step Documentation", e); + } + } + + private void writeConnectorDocumentation(final ExtensionDefinition extensionDefinition, final ExtensionClassLoader classLoader, + final Class connectorDocWriterClass, final XMLStreamWriter xmlWriter) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException { + + getLog().debug("Generating Connector documentation for " + extensionDefinition.getExtensionName() + " using ClassLoader:" + System.lineSeparator() + classLoader.toTree()); + final Object connectorDocWriter = connectorDocWriterClass.getConstructor(XMLStreamWriter.class).newInstance(xmlWriter); + final Class connectorClass = Class.forName(CONNECTOR_CLASS_NAME, false, classLoader); + + final Class extensionClass = Class.forName(extensionDefinition.getExtensionName(), false, classLoader); + final Object connectorInstance = extensionClass.getDeclaredConstructor().newInstance(); + + final Method initMethod = connectorDocWriterClass.getMethod("initialize", connectorClass); + initMethod.invoke(connectorDocWriter, connectorInstance); + + final Method writeMethod = connectorDocWriterClass.getMethod("write", connectorClass); + writeMethod.invoke(connectorDocWriter, connectorInstance); + } + private List getDocumentationServiceAPIs(Class serviceApiClass, Set serviceDefinitions) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { final Constructor ctr = serviceApiClass.getConstructor(String.class, String.class, String.class, String.class); @@ -848,6 +907,70 @@ private void copy(final InputStream in, final OutputStream out) throws IOExcepti } } + private void writeStepDocumentation(final ExtensionClassLoader classLoader, final Set connectorNames, final File stepDocumentationDir) + throws URISyntaxException, IOException, MojoExecutionException { + + for (final URL url : classLoader.getURLs()) { + final File file = new File(url.toURI()); + final String filename = file.getName(); + if (!filename.endsWith(".jar")) { + continue; + } + + writeStepDocumentation(file, connectorNames, stepDocumentationDir); + } + } + + private void writeStepDocumentation(final File file, final Set connectorNames, final File stepDocumentationDir) throws IOException, MojoExecutionException { + final JarFile jarFile = new JarFile(file); + + for (final Enumeration jarEnumeration = jarFile.entries(); jarEnumeration.hasMoreElements();) { + final JarEntry jarEntry = jarEnumeration.nextElement(); + + final String entryName = jarEntry.getName(); + // Look for step documentation under docs//steps/ + if (!entryName.startsWith("docs/")) { + continue; + } + + final int nextSlashIndex = entryName.indexOf("/", 5); + if (nextSlashIndex < 0) { + continue; + } + + final String connectorName = entryName.substring(5, nextSlashIndex); + if (!connectorNames.contains(connectorName)) { + continue; + } + + // Check if this is under the steps/ subdirectory + final String afterConnector = entryName.substring(nextSlashIndex + 1); + if (!afterConnector.startsWith("steps/")) { + continue; + } + + if (jarEntry.isDirectory()) { + continue; + } + + // Get the step documentation filename (e.g., "Configure_Connection.md") + final String stepFileName = afterConnector.substring(6); // Remove "steps/" + if (stepFileName.isEmpty()) { + continue; + } + + getLog().debug("Found step documentation file " + entryName + " in " + file + " for connector " + connectorName); + final File connectorDirectory = new File(stepDocumentationDir, connectorName); + final File destinationFile = new File(connectorDirectory, stepFileName); + + createDirectory(destinationFile.getParentFile()); + + try (final InputStream in = jarFile.getInputStream(jarEntry); + final OutputStream out = new FileOutputStream(destinationFile)) { + copy(in, out); + } + } + } private ExtensionClassLoaderFactory createClassLoaderFactory() { return new ExtensionClassLoaderFactory.Builder() @@ -1089,6 +1212,11 @@ private NarResult createArchive() throws MojoExecutionException { archiver.getArchiver().addDirectory(additionalDetailsDirectory, "META-INF/docs/additional-details/"); } + File stepDocumentationDirectory = new File(extensionDocsFile.getParentFile(), "steps"); + if (stepDocumentationDirectory.exists()) { + archiver.getArchiver().addDirectory(stepDocumentationDirectory, "META-INF/docs/steps/"); + } + File existingManifest = defaultManifestFile; if (useDefaultManifestFile && existingManifest.exists() && archive.getManifestFile() == null) { getLog().info("Adding existing MANIFEST to archive. Found under: " + existingManifest.getPath()); diff --git a/src/main/java/org/apache/nifi/extension/definition/ExtensionType.java b/src/main/java/org/apache/nifi/extension/definition/ExtensionType.java index faa739c..d4daa0c 100644 --- a/src/main/java/org/apache/nifi/extension/definition/ExtensionType.java +++ b/src/main/java/org/apache/nifi/extension/definition/ExtensionType.java @@ -28,6 +28,8 @@ public enum ExtensionType { PARAMETER_PROVIDER, - FLOW_REGISTRY_CLIENT; + FLOW_REGISTRY_CLIENT, + + CONNECTOR; } diff --git a/src/main/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactory.java b/src/main/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactory.java index b27a03a..8ae3a4f 100644 --- a/src/main/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactory.java +++ b/src/main/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactory.java @@ -45,6 +45,7 @@ public class ExtensionDefinitionFactory { INTERFACE_NAMES.put(ExtensionType.FLOW_ANALYSIS_RULE, "org.apache.nifi.flowanalysis.FlowAnalysisRule"); INTERFACE_NAMES.put(ExtensionType.PARAMETER_PROVIDER, "org.apache.nifi.parameter.ParameterProvider"); INTERFACE_NAMES.put(ExtensionType.FLOW_REGISTRY_CLIENT, "org.apache.nifi.registry.flow.FlowRegistryClient"); + INTERFACE_NAMES.put(ExtensionType.CONNECTOR, "org.apache.nifi.components.connector.Connector"); } private final ClassLoader extensionClassLoader; diff --git a/src/test/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactoryTest.java b/src/test/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactoryTest.java new file mode 100644 index 0000000..0c21c30 --- /dev/null +++ b/src/test/java/org/apache/nifi/extension/definition/extraction/ExtensionDefinitionFactoryTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.extension.definition.extraction; + +import org.apache.nifi.extension.definition.ExtensionDefinition; +import org.apache.nifi.extension.definition.ExtensionType; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ExtensionDefinitionFactoryTest { + + @Test + void testDiscoverExtensionsWithNoExtensions() throws IOException { + // Use the current class loader which won't have any NiFi extensions + final ExtensionDefinitionFactory factory = new ExtensionDefinitionFactory(getClass().getClassLoader()); + + // Verify that each extension type can be queried without error (even if empty) + for (final ExtensionType extensionType : ExtensionType.values()) { + final Set definitions = factory.discoverExtensions(extensionType); + assertNotNull(definitions, "Definitions should not be null for " + extensionType); + } + } + + @Test + void testAllExtensionTypesHaveInterfaceMappings() throws IOException { + final ExtensionDefinitionFactory factory = new ExtensionDefinitionFactory(getClass().getClassLoader()); + + // Verify that all extension types are supported (no exceptions thrown) + for (final ExtensionType extensionType : ExtensionType.values()) { + // This will throw an exception if the extension type is not mapped in INTERFACE_NAMES + final Set definitions = factory.discoverExtensions(extensionType); + assertNotNull(definitions); + } + } + + @Test + void testConnectorExtensionTypeSupported() throws IOException { + final ExtensionDefinitionFactory factory = new ExtensionDefinitionFactory(getClass().getClassLoader()); + + // Verify CONNECTOR type specifically works + final Set definitions = factory.discoverExtensions(ExtensionType.CONNECTOR); + assertNotNull(definitions); + // With a standard classloader, there should be no Connector implementations + assertTrue(definitions.isEmpty()); + } +} +