From f25690af2d590766f52c8f9ce33f6a3b6281859f Mon Sep 17 00:00:00 2001 From: aglotero Date: Mon, 8 Oct 2018 18:22:19 -0300 Subject: [PATCH 1/6] NIFI-5642: QueryCassandra processor : output FlowFiles as soon fetch_size is reached NIFI-5642: QueryCassandra processor : output FlowFiles as soon fetch_size is reached Fixed checkstyle error Delete build.sh Delete local build file NIFI-5642 : letting fetch_size to control the Cassandra data flow creating a new MAX_ROWS_PER_FLOW_FILE parameter Fixed checkstyle error: no more import java.util.* Fixed missing imports NIFI-5642: added REL_ORIGINAL relationship in order to allow incremental commit --- .../cassandra/AbstractCassandraProcessor.java | 16 +- .../processors/cassandra/QueryCassandra.java | 361 +++++++++++------- .../cassandra/CassandraQueryTestUtil.java | 34 +- .../cassandra/QueryCassandraTest.java | 163 ++++++-- 4 files changed, 397 insertions(+), 177 deletions(-) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java index d1e38748dda3..a9883b2b2b45 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java @@ -132,17 +132,25 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .addValidator(StandardValidators.CHARACTER_SET_VALIDATOR) .build(); + // Relationships public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") - .description("A FlowFile is transferred to this relationship if the operation completed successfully.") + .description("Successfully created FlowFile from CQL query result set.") .build(); + + static final Relationship REL_ORIGINAL = new Relationship.Builder() + .name("original") + .description("All input FlowFiles that are part of a successful query execution go here.") + .build(); + public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") - .description("A FlowFile is transferred to this relationship if the operation failed.") + .description("CQL query execution failed.") .build(); + public static final Relationship REL_RETRY = new Relationship.Builder().name("retry") - .description("A FlowFile is transferred to this relationship if the operation cannot be completed but attempting " - + "it again may succeed.") + .description("A FlowFile is transferred to this relationship if the query cannot be completed but attempting " + + "the operation again may succeed.") .build(); static List descriptors = new ArrayList<>(); diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index 75a66f0a5a4e..beaf4a7e72ab 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -19,7 +19,6 @@ import com.datastax.driver.core.ColumnDefinitions; import com.datastax.driver.core.DataType; import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.AuthenticationException; @@ -33,7 +32,7 @@ import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.DatumWriter; -import org.apache.commons.text.StringEscapeUtils; +import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; @@ -61,15 +60,16 @@ import java.io.OutputStream; import java.nio.charset.Charset; import java.text.SimpleDateFormat; -import java.util.ArrayList; +import java.util.Map; import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Collections; import java.util.TimeZone; +import java.util.Date; +import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -120,6 +120,16 @@ public class QueryCassandra extends AbstractCassandraProcessor { .addValidator(StandardValidators.INTEGER_VALIDATOR) .build(); + public static final PropertyDescriptor MAX_ROWS_PER_FLOW_FILE = new PropertyDescriptor.Builder() + .name("Max Rows Per Flow File") + .description("The maximum number of result rows that will be included in a single FlowFile. This will allow you to break up very large " + + "result sets into multiple FlowFiles. If the value specified is zero, then all rows are returned in a single FlowFile.") + .defaultValue("0") + .required(true) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .build(); + public static final PropertyDescriptor OUTPUT_FORMAT = new PropertyDescriptor.Builder() .name("Output Format") .description("The format to which the result rows will be converted. If JSON is selected, the output will " @@ -144,11 +154,13 @@ public class QueryCassandra extends AbstractCassandraProcessor { _propertyDescriptors.add(CQL_SELECT_QUERY); _propertyDescriptors.add(QUERY_TIMEOUT); _propertyDescriptors.add(FETCH_SIZE); + _propertyDescriptors.add(MAX_ROWS_PER_FLOW_FILE); _propertyDescriptors.add(OUTPUT_FORMAT); propertyDescriptors = Collections.unmodifiableList(_propertyDescriptors); Set _relationships = new HashSet<>(); _relationships.add(REL_SUCCESS); + _relationships.add(REL_ORIGINAL); _relationships.add(REL_FAILURE); _relationships.add(REL_RETRY); relationships = Collections.unmodifiableSet(_relationships); @@ -191,76 +203,110 @@ public void onScheduled(final ProcessContext context) { @Override public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + FlowFile inputFlowFile = null; FlowFile fileToProcess = null; + + Map attributes = null; + if (context.hasIncomingConnection()) { - fileToProcess = session.get(); + inputFlowFile = session.get(); // If we have no FlowFile, and all incoming connections are self-loops then we can continue on. // However, if we have no FlowFile and we have connections coming from other Processors, then // we know that we should run only if we have a FlowFile. - if (fileToProcess == null && context.hasNonLoopConnection()) { + if (inputFlowFile == null && context.hasNonLoopConnection()) { return; } + + attributes = inputFlowFile.getAttributes(); } final ComponentLog logger = getLogger(); - final String selectQuery = context.getProperty(CQL_SELECT_QUERY).evaluateAttributeExpressions(fileToProcess).getValue(); - final long queryTimeout = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions(fileToProcess).asTimePeriod(TimeUnit.MILLISECONDS); + final String selectQuery = context.getProperty(CQL_SELECT_QUERY).evaluateAttributeExpressions(inputFlowFile).getValue(); + final long queryTimeout = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions(inputFlowFile).asTimePeriod(TimeUnit.MILLISECONDS); final String outputFormat = context.getProperty(OUTPUT_FORMAT).getValue(); - final Charset charset = Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(fileToProcess).getValue()); + final long maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions().asInteger(); + final Charset charset = Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(inputFlowFile).getValue()); final StopWatch stopWatch = new StopWatch(true); - if (fileToProcess == null) { - fileToProcess = session.create(); + if(inputFlowFile != null){ + session.transfer(inputFlowFile, REL_ORIGINAL); } try { // The documentation for the driver recommends the session remain open the entire time the processor is running // and states that it is thread-safe. This is why connectionSession is not in a try-with-resources. final Session connectionSession = cassandraSession.get(); - final ResultSetFuture queryFuture = connectionSession.executeAsync(selectQuery); + final ResultSet resultSet; + + if (queryTimeout > 0) { + resultSet = connectionSession.execute(selectQuery, queryTimeout, TimeUnit.MILLISECONDS); + }else{ + resultSet = connectionSession.execute(selectQuery); + } + final AtomicLong nrOfRows = new AtomicLong(0L); - fileToProcess = session.write(fileToProcess, new OutputStreamCallback() { - @Override - public void process(final OutputStream out) throws IOException { - try { - logger.debug("Executing CQL query {}", new Object[]{selectQuery}); - final ResultSet resultSet; - if (queryTimeout > 0) { - resultSet = queryFuture.getUninterruptibly(queryTimeout, TimeUnit.MILLISECONDS); - if (AVRO_FORMAT.equals(outputFormat)) { - nrOfRows.set(convertToAvroStream(resultSet, out, queryTimeout, TimeUnit.MILLISECONDS)); - } else if (JSON_FORMAT.equals(outputFormat)) { - nrOfRows.set(convertToJsonStream(resultSet, out, charset, queryTimeout, TimeUnit.MILLISECONDS)); - } - } else { - resultSet = queryFuture.getUninterruptibly(); - if (AVRO_FORMAT.equals(outputFormat)) { - nrOfRows.set(convertToAvroStream(resultSet, out, 0, null)); - } else if (JSON_FORMAT.equals(outputFormat)) { - nrOfRows.set(convertToJsonStream(resultSet, out, charset, 0, null)); + do { + fileToProcess = session.create(); + + // Assuring that if we have an input FlowFile + // the generated output inherit the attributes + if(attributes != null){ + fileToProcess = session.putAllAttributes(fileToProcess, attributes); + } + + fileToProcess = session.write(fileToProcess, new OutputStreamCallback() { + @Override + public void process(final OutputStream out) throws IOException { + try { + logger.debug("Executing CQL query {}", new Object[]{selectQuery}); + if (queryTimeout > 0) { + if (AVRO_FORMAT.equals(outputFormat)) { + nrOfRows.set(convertToAvroStream(resultSet, maxRowsPerFlowFile, + out, queryTimeout, TimeUnit.MILLISECONDS)); + } else if (JSON_FORMAT.equals(outputFormat)) { + nrOfRows.set(convertToJsonStream(resultSet, maxRowsPerFlowFile, + out, charset, queryTimeout, TimeUnit.MILLISECONDS)); + } + } else { + if (AVRO_FORMAT.equals(outputFormat)) { + nrOfRows.set(convertToAvroStream(resultSet, maxRowsPerFlowFile, + out, 0, null)); + } else if (JSON_FORMAT.equals(outputFormat)) { + nrOfRows.set(convertToJsonStream(resultSet, maxRowsPerFlowFile, + out, charset, 0, null)); + } } - } - } catch (final TimeoutException | InterruptedException | ExecutionException e) { - throw new ProcessException(e); + } catch (final TimeoutException | InterruptedException | ExecutionException e) { + throw new ProcessException(e); + } } - } - }); + }); + - // set attribute how many rows were selected - fileToProcess = session.putAttribute(fileToProcess, RESULT_ROW_COUNT, String.valueOf(nrOfRows.get())); + // set attribute how many rows were selected + fileToProcess = session.putAttribute(fileToProcess, RESULT_ROW_COUNT, String.valueOf(nrOfRows.get())); - // set mime.type based on output format - fileToProcess = session.putAttribute(fileToProcess, CoreAttributes.MIME_TYPE.key(), - JSON_FORMAT.equals(outputFormat) ? "application/json" : "application/avro-binary"); + // set mime.type based on output format + fileToProcess = session.putAttribute(fileToProcess, CoreAttributes.MIME_TYPE.key(), + JSON_FORMAT.equals(outputFormat) ? "application/json" : "application/avro-binary"); - logger.info("{} contains {} Avro records; transferring to 'success'", - new Object[]{fileToProcess, nrOfRows.get()}); - session.getProvenanceReporter().modifyContent(fileToProcess, "Retrieved " + nrOfRows.get() + " rows", - stopWatch.getElapsed(TimeUnit.MILLISECONDS)); - session.transfer(fileToProcess, REL_SUCCESS); + logger.info("{} contains {} records; transferring to 'success'", + new Object[]{fileToProcess, nrOfRows.get()}); + session.getProvenanceReporter().modifyContent(fileToProcess, "Retrieved " + nrOfRows.get() + " rows", + stopWatch.getElapsed(TimeUnit.MILLISECONDS)); + session.transfer(fileToProcess, REL_SUCCESS); + session.commit(); + + try { + resultSet.fetchMoreResults().get(); + } catch (Exception e) { + logger.error("ExecutionException : query {} for {} due to {}; routing to failure", + new Object[]{selectQuery, fileToProcess, e}); + } + } while (!resultSet.isExhausted()); } catch (final NoHostAvailableException nhae) { getLogger().error("No host in the Cassandra cluster can be contacted successfully to execute this query", nhae); @@ -269,11 +315,17 @@ public void process(final OutputStream out) throws IOException { // cap the error limit at 10, format the messages, and don't include the stack trace (it is displayed by the // logger message above). getLogger().error(nhae.getCustomMessage(10, true, false)); + if (fileToProcess == null) { + fileToProcess = session.create(); + } fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_RETRY); } catch (final QueryExecutionException qee) { logger.error("Cannot execute the query with the requested consistency level successfully", qee); + if (fileToProcess == null) { + fileToProcess = session.create(); + } fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_RETRY); @@ -282,28 +334,41 @@ public void process(final OutputStream out) throws IOException { logger.error("The CQL query {} is invalid due to syntax error, authorization issue, or another " + "validation problem; routing {} to failure", new Object[]{selectQuery, fileToProcess}, qve); + + if (fileToProcess == null) { + fileToProcess = session.create(); + } fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_FAILURE); } else { // This can happen if any exceptions occur while setting up the connection, statement, etc. logger.error("The CQL query {} is invalid due to syntax error, authorization issue, or another " + "validation problem", new Object[]{selectQuery}, qve); - session.remove(fileToProcess); + if (fileToProcess != null) { + session.remove(fileToProcess); + } context.yield(); } } catch (final ProcessException e) { if (context.hasIncomingConnection()) { logger.error("Unable to execute CQL select query {} for {} due to {}; routing to failure", new Object[]{selectQuery, fileToProcess, e}); + if (fileToProcess == null) { + fileToProcess = session.create(); + } fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_FAILURE); + } else { logger.error("Unable to execute CQL select query {} due to {}", new Object[]{selectQuery, e}); - session.remove(fileToProcess); + if (fileToProcess != null) { + session.remove(fileToProcess); + } context.yield(); } } + session.commit(); } @@ -330,7 +395,8 @@ public void shutdown() { * @throws TimeoutException If a result set fetch has taken longer than the specified timeout * @throws ExecutionException If any error occurs during the result set fetch */ - public static long convertToAvroStream(final ResultSet rs, final OutputStream outStream, + public static long convertToAvroStream(final ResultSet rs, long maxRowsPerFlowFile, + final OutputStream outStream, long timeout, TimeUnit timeUnit) throws IOException, InterruptedException, TimeoutException, ExecutionException { @@ -343,36 +409,47 @@ public static long convertToAvroStream(final ResultSet rs, final OutputStream ou final ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); long nrOfRows = 0; + long rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); + if (columnDefinitions != null) { - do { - - // Grab the ones we have - int rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); - if (rowsAvailableWithoutFetching == 0) { - // Get more - if (timeout <= 0 || timeUnit == null) { - rs.fetchMoreResults().get(); - } else { - rs.fetchMoreResults().get(timeout, timeUnit); - } + + // Grab the ones we have + if (rowsAvailableWithoutFetching == 0) { + // Get more + if (timeout <= 0 || timeUnit == null) { + rs.fetchMoreResults().get(); + } else { + rs.fetchMoreResults().get(timeout, timeUnit); } + rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); + } - for (Row row : rs) { + if(maxRowsPerFlowFile == 0){ + maxRowsPerFlowFile = rowsAvailableWithoutFetching; + } + + Row row; + while(nrOfRows < maxRowsPerFlowFile){ + try { + row = rs.iterator().next(); + }catch (NoSuchElementException nsee){ + nrOfRows -= 1; + break; + } - for (int i = 0; i < columnDefinitions.size(); i++) { - final DataType dataType = columnDefinitions.getType(i); + for (int i = 0; i < columnDefinitions.size(); i++) { + final DataType dataType = columnDefinitions.getType(i); - if (row.isNull(i)) { - rec.put(i, null); - } else { - rec.put(i, getCassandraObject(row, i, dataType)); - } + if (row.isNull(i)) { + rec.put(i, null); + } else { + rec.put(i, getCassandraObject(row, i, dataType)); } - dataFileWriter.append(rec); - nrOfRows += 1; - } - } while (!rs.isFullyFetched()); + dataFileWriter.append(rec); + nrOfRows += 1; + + } } return nrOfRows; } @@ -391,7 +468,8 @@ public static long convertToAvroStream(final ResultSet rs, final OutputStream ou * @throws TimeoutException If a result set fetch has taken longer than the specified timeout * @throws ExecutionException If any error occurs during the result set fetch */ - public static long convertToJsonStream(final ResultSet rs, final OutputStream outStream, + public static long convertToJsonStream(final ResultSet rs, long maxRowsPerFlowFile, + final OutputStream outStream, Charset charset, long timeout, TimeUnit timeUnit) throws IOException, InterruptedException, TimeoutException, ExecutionException { @@ -400,77 +478,87 @@ public static long convertToJsonStream(final ResultSet rs, final OutputStream ou outStream.write("{\"results\":[".getBytes(charset)); final ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); long nrOfRows = 0; + long rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); + if (columnDefinitions != null) { - do { - - // Grab the ones we have - int rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); - if (rowsAvailableWithoutFetching == 0) { - // Get more - if (timeout <= 0 || timeUnit == null) { - rs.fetchMoreResults().get(); - } else { - rs.fetchMoreResults().get(timeout, timeUnit); - } + + // Grab the ones we have + if (rowsAvailableWithoutFetching == 0) { + // Get more + if (timeout <= 0 || timeUnit == null) { + rs.fetchMoreResults().get(); + } else { + rs.fetchMoreResults().get(timeout, timeUnit); } + rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); + } - for (Row row : rs) { - if (nrOfRows != 0) { + if(maxRowsPerFlowFile == 0){ + maxRowsPerFlowFile = rowsAvailableWithoutFetching; + } + Row row; + while(nrOfRows < maxRowsPerFlowFile){ + try { + row = rs.iterator().next(); + }catch (NoSuchElementException nsee){ + //nrOfRows -= 1; + break; + } + if (nrOfRows != 0) { + outStream.write(",".getBytes(charset)); + } + outStream.write("{".getBytes(charset)); + for (int i = 0; i < columnDefinitions.size(); i++) { + final DataType dataType = columnDefinitions.getType(i); + final String colName = columnDefinitions.getName(i); + if (i != 0) { outStream.write(",".getBytes(charset)); } - outStream.write("{".getBytes(charset)); - for (int i = 0; i < columnDefinitions.size(); i++) { - final DataType dataType = columnDefinitions.getType(i); - final String colName = columnDefinitions.getName(i); - if (i != 0) { - outStream.write(",".getBytes(charset)); - } - if (row.isNull(i)) { - outStream.write(("\"" + colName + "\"" + ":null").getBytes(charset)); - } else { - Object value = getCassandraObject(row, i, dataType); - String valueString; - if (value instanceof List || value instanceof Set) { - boolean first = true; - StringBuilder sb = new StringBuilder("["); - for (Object element : ((Collection) value)) { - if (!first) { - sb.append(","); - } - sb.append(getJsonElement(element)); - first = false; + if (row.isNull(i)) { + outStream.write(("\"" + colName + "\"" + ":null").getBytes(charset)); + } else { + Object value = getCassandraObject(row, i, dataType); + String valueString; + if (value instanceof List || value instanceof Set) { + boolean first = true; + StringBuilder sb = new StringBuilder("["); + for (Object element : ((Collection) value)) { + if (!first) { + sb.append(","); } - sb.append("]"); - valueString = sb.toString(); - } else if (value instanceof Map) { - boolean first = true; - StringBuilder sb = new StringBuilder("{"); - for (Object element : ((Map) value).entrySet()) { - Map.Entry entry = (Map.Entry) element; - Object mapKey = entry.getKey(); - Object mapValue = entry.getValue(); - - if (!first) { - sb.append(","); - } - sb.append(getJsonElement(mapKey)); - sb.append(":"); - sb.append(getJsonElement(mapValue)); - first = false; + sb.append(getJsonElement(element)); + first = false; + } + sb.append("]"); + valueString = sb.toString(); + } else if (value instanceof Map) { + boolean first = true; + StringBuilder sb = new StringBuilder("{"); + for (Object element : ((Map) value).entrySet()) { + Map.Entry entry = (Map.Entry) element; + Object mapKey = entry.getKey(); + Object mapValue = entry.getValue(); + + if (!first) { + sb.append(","); } - sb.append("}"); - valueString = sb.toString(); - } else { - valueString = getJsonElement(value); + sb.append(getJsonElement(mapKey)); + sb.append(":"); + sb.append(getJsonElement(mapValue)); + first = false; } - outStream.write(("\"" + colName + "\":" - + valueString + "").getBytes(charset)); + sb.append("}"); + valueString = sb.toString(); + } else { + valueString = getJsonElement(value); } + outStream.write(("\"" + colName + "\":" + + valueString + "").getBytes(charset)); } - nrOfRows += 1; - outStream.write("}".getBytes(charset)); } - } while (!rs.isFullyFetched()); + nrOfRows += 1; + outStream.write("}".getBytes(charset)); + } } return nrOfRows; } finally { @@ -550,3 +638,4 @@ public static Schema createSchema(final ResultSet rs) throws IOException { return builder.endRecord(); } } + diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/CassandraQueryTestUtil.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/CassandraQueryTestUtil.java index 49a676069814..52bcbc096aab 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/CassandraQueryTestUtil.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/CassandraQueryTestUtil.java @@ -22,19 +22,19 @@ import com.datastax.driver.core.Row; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.ListenableFuture; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.text.SimpleDateFormat; +import java.util.Set; +import java.util.Map; +import java.util.List; +import java.util.HashMap; import java.util.Arrays; import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.TimeZone; - +import java.util.Date; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; @@ -45,7 +45,7 @@ * Utility methods for Cassandra processors' unit tests */ public class CassandraQueryTestUtil { - public static ResultSet createMockResultSet() throws Exception { + public static ResultSet createMockResultSet(boolean falseThenTrue) throws Exception { ResultSet resultSet = mock(ResultSet.class); ColumnDefinitions columnDefinitions = mock(ColumnDefinitions.class); when(columnDefinitions.size()).thenReturn(9); @@ -95,14 +95,28 @@ public DataType answer(InvocationOnMock invocationOnMock) throws Throwable { }}, true, 3.0f, 4.0) ); + ListenableFuture future = mock(ListenableFuture.class); + when(future.get()).thenReturn(rows); + when(resultSet.fetchMoreResults()).thenReturn(future); + when(resultSet.iterator()).thenReturn(rows.iterator()); when(resultSet.all()).thenReturn(rows); when(resultSet.getAvailableWithoutFetching()).thenReturn(rows.size()); when(resultSet.isFullyFetched()).thenReturn(false).thenReturn(true); + if(falseThenTrue) { + when(resultSet.isExhausted()).thenReturn(false, true); + }else{ + when(resultSet.isExhausted()).thenReturn(true); + } when(resultSet.getColumnDefinitions()).thenReturn(columnDefinitions); + return resultSet; } + public static ResultSet createMockResultSet() throws Exception { + return createMockResultSet(true); + } + public static ResultSet createMockResultSetOneColumn() throws Exception { ResultSet resultSet = mock(ResultSet.class); ColumnDefinitions columnDefinitions = mock(ColumnDefinitions.class); @@ -132,10 +146,15 @@ public DataType answer(InvocationOnMock invocationOnMock) throws Throwable { createRow("user2") ); + ListenableFuture future = mock(ListenableFuture.class); + when(future.get()).thenReturn(rows); + when(resultSet.fetchMoreResults()).thenReturn(future); + when(resultSet.iterator()).thenReturn(rows.iterator()); when(resultSet.all()).thenReturn(rows); when(resultSet.getAvailableWithoutFetching()).thenReturn(rows.size()); when(resultSet.isFullyFetched()).thenReturn(false).thenReturn(true); + when(resultSet.isExhausted()).thenReturn(false).thenReturn(true); when(resultSet.getColumnDefinitions()).thenReturn(columnDefinitions); return resultSet; } @@ -163,3 +182,4 @@ public static Row createRow(String user_id) { return row; } } + diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/QueryCassandraTest.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/QueryCassandraTest.java index dfec386e7497..f2c551b67970 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/QueryCassandraTest.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/QueryCassandraTest.java @@ -16,16 +16,6 @@ */ package org.apache.nifi.processors.cassandra; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Configuration; import com.datastax.driver.core.ConsistencyLevel; @@ -36,22 +26,33 @@ import com.datastax.driver.core.exceptions.InvalidQueryException; import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.core.exceptions.ReadTimeoutException; -import java.io.ByteArrayOutputStream; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.net.ssl.SSLContext; import org.apache.avro.Schema; import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.stream.io.ByteArrayOutputStream; import org.apache.nifi.util.MockFlowFile; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; import org.junit.Before; import org.junit.Test; +import javax.net.ssl.SSLContext; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class QueryCassandraTest { @@ -150,6 +151,7 @@ public void testProcessorJsonOutput() { @Test public void testProcessorELConfigJsonOutput() { + setUpStandardProcessorConfig(); testRunner.setProperty(AbstractCassandraProcessor.CONTACT_POINTS, "${hosts}"); testRunner.setProperty(QueryCassandra.CQL_SELECT_QUERY, "${query}"); testRunner.setProperty(AbstractCassandraProcessor.PASSWORD, "${pass}"); @@ -157,6 +159,7 @@ public void testProcessorELConfigJsonOutput() { testRunner.setProperty(AbstractCassandraProcessor.CHARSET, "${charset}"); testRunner.setProperty(QueryCassandra.QUERY_TIMEOUT, "${timeout}"); testRunner.setProperty(QueryCassandra.FETCH_SIZE, "${fetch}"); + testRunner.setProperty(QueryCassandra.MAX_ROWS_PER_FLOW_FILE, "${max-rows-per-flow}"); testRunner.setIncomingConnection(false); testRunner.assertValid(); @@ -166,6 +169,7 @@ public void testProcessorELConfigJsonOutput() { testRunner.setVariable("charset", "UTF-8"); testRunner.setVariable("timeout", "30 sec"); testRunner.setVariable("fetch", "0"); + testRunner.setVariable("max-rows-per-flow", "0"); // Test JSON output testRunner.setProperty(QueryCassandra.OUTPUT_FORMAT, QueryCassandra.JSON_FORMAT); @@ -201,7 +205,7 @@ public void testProcessorJsonOutputWithQueryTimeout() { } @Test - public void testProcessorEmptyFlowFileAndExceptions() { + public void testProcessorEmptyFlowFile() { setUpStandardProcessorConfig(); // Run with empty flowfile @@ -209,36 +213,82 @@ public void testProcessorEmptyFlowFileAndExceptions() { processor.setExceptionToThrow(null); testRunner.enqueue("".getBytes()); testRunner.run(1, true, true); - testRunner.assertAllFlowFilesTransferred(QueryCassandra.REL_SUCCESS, 1); + testRunner.assertTransferCount(QueryCassandra.REL_SUCCESS, 1); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); + testRunner.clearTransferState(); + } + + @Test + public void testProcessorEmptyFlowFileMaxRowsPerFlowFileEqOne() { + + processor = new MockQueryCassandraTwoRounds(); + testRunner = TestRunners.newTestRunner(processor); + + setUpStandardProcessorConfig(); + + testRunner.setIncomingConnection(true); + testRunner.setProperty(QueryCassandra.MAX_ROWS_PER_FLOW_FILE, "1"); + processor.setExceptionToThrow(null); + testRunner.enqueue("".getBytes()); + testRunner.run(1, true, true); + testRunner.assertTransferCount(QueryCassandra.REL_SUCCESS, 2); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); testRunner.clearTransferState(); + } + + + @Test + public void testProcessorEmptyFlowFileAndNoHostAvailableException() { + setUpStandardProcessorConfig(); // Test exceptions processor.setExceptionToThrow(new NoHostAvailableException(new HashMap())); testRunner.enqueue("".getBytes()); testRunner.run(1, true, true); - testRunner.assertAllFlowFilesTransferred(QueryCassandra.REL_RETRY, 1); + testRunner.assertTransferCount(QueryCassandra.REL_RETRY, 1); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); testRunner.clearTransferState(); + } + + @Test + public void testProcessorEmptyFlowFileAndInetSocketAddressConsistencyLevelANY() { + setUpStandardProcessorConfig(); processor.setExceptionToThrow( new ReadTimeoutException(new InetSocketAddress("localhost", 9042), ConsistencyLevel.ANY, 0, 1, false)); testRunner.enqueue("".getBytes()); testRunner.run(1, true, true); - testRunner.assertAllFlowFilesTransferred(QueryCassandra.REL_RETRY, 1); + testRunner.assertTransferCount(QueryCassandra.REL_RETRY, 1); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); testRunner.clearTransferState(); + } + + @Test + public void testProcessorEmptyFlowFileAndInetSocketAddressDefault() { + setUpStandardProcessorConfig(); processor.setExceptionToThrow( new InvalidQueryException(new InetSocketAddress("localhost", 9042), "invalid query")); testRunner.enqueue("".getBytes()); testRunner.run(1, true, true); - testRunner.assertAllFlowFilesTransferred(QueryCassandra.REL_FAILURE, 1); + testRunner.assertTransferCount(QueryCassandra.REL_FAILURE, 1); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); testRunner.clearTransferState(); + } + + @Test + public void testProcessorEmptyFlowFileAndExceptionsProcessException() { + setUpStandardProcessorConfig(); processor.setExceptionToThrow(new ProcessException()); testRunner.enqueue("".getBytes()); testRunner.run(1, true, true); - testRunner.assertAllFlowFilesTransferred(QueryCassandra.REL_FAILURE, 1); + testRunner.assertTransferCount(QueryCassandra.REL_FAILURE, 1); + testRunner.assertTransferCount(QueryCassandra.REL_ORIGINAL, 1); } + // -- + @Test public void testCreateSchemaOneColumn() throws Exception { ResultSet rs = CassandraQueryTestUtil.createMockResultSetOneColumn(); @@ -249,7 +299,7 @@ public void testCreateSchemaOneColumn() throws Exception { @Test public void testCreateSchema() throws Exception { - ResultSet rs = CassandraQueryTestUtil.createMockResultSet(); + ResultSet rs = CassandraQueryTestUtil.createMockResultSet(true); Schema schema = QueryCassandra.createSchema(rs); assertNotNull(schema); assertEquals(Schema.Type.RECORD, schema.getType()); @@ -354,17 +404,20 @@ public void testCreateSchema() throws Exception { @Test public void testConvertToAvroStream() throws Exception { + setUpStandardProcessorConfig(); ResultSet rs = CassandraQueryTestUtil.createMockResultSet(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - long numberOfRows = QueryCassandra.convertToAvroStream(rs, baos, 0, null); + long numberOfRows = QueryCassandra.convertToAvroStream(rs, 0, baos, 0, null); assertEquals(2, numberOfRows); } @Test public void testConvertToJSONStream() throws Exception { + setUpStandardProcessorConfig(); ResultSet rs = CassandraQueryTestUtil.createMockResultSet(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - long numberOfRows = QueryCassandra.convertToJsonStream(rs, baos, StandardCharsets.UTF_8, 0, null); + long numberOfRows = QueryCassandra.convertToJsonStream(rs, 0, baos, StandardCharsets.UTF_8, + 0, null); assertEquals(2, numberOfRows); } @@ -374,6 +427,7 @@ private void setUpStandardProcessorConfig() { testRunner.setProperty(QueryCassandra.CQL_SELECT_QUERY, "select * from test"); testRunner.setProperty(AbstractCassandraProcessor.PASSWORD, "password"); testRunner.setProperty(AbstractCassandraProcessor.USERNAME, "username"); + testRunner.setProperty(QueryCassandra.MAX_ROWS_PER_FLOW_FILE, "0"); } /** @@ -397,17 +451,21 @@ protected Cluster createCluster(List contactPoints, SSLContex Configuration config = Configuration.builder().build(); when(mockCluster.getConfiguration()).thenReturn(config); ResultSetFuture future = mock(ResultSetFuture.class); - ResultSet rs = CassandraQueryTestUtil.createMockResultSet(); + ResultSet rs = CassandraQueryTestUtil.createMockResultSet(false); when(future.getUninterruptibly()).thenReturn(rs); + try { doReturn(rs).when(future).getUninterruptibly(anyLong(), any(TimeUnit.class)); } catch (TimeoutException te) { throw new IllegalArgumentException("Mocked cluster doesn't time out"); } + if (exceptionToThrow != null) { - when(mockSession.executeAsync(anyString())).thenThrow(exceptionToThrow); + when(mockSession.execute(anyString(), any(), any())).thenThrow(exceptionToThrow); + when(mockSession.execute(anyString())).thenThrow(exceptionToThrow); } else { - when(mockSession.executeAsync(anyString())).thenReturn(future); + when(mockSession.execute(anyString(),any(), any())).thenReturn(rs); + when(mockSession.execute(anyString())).thenReturn(rs); } } catch (Exception e) { fail(e.getMessage()); @@ -418,7 +476,52 @@ protected Cluster createCluster(List contactPoints, SSLContex public void setExceptionToThrow(Exception e) { this.exceptionToThrow = e; } + } + + private static class MockQueryCassandraTwoRounds extends MockQueryCassandra { + + private Exception exceptionToThrow = null; + @Override + protected Cluster createCluster(List contactPoints, SSLContext sslContext, + String username, String password) { + Cluster mockCluster = mock(Cluster.class); + try { + Metadata mockMetadata = mock(Metadata.class); + when(mockMetadata.getClusterName()).thenReturn("cluster1"); + when(mockCluster.getMetadata()).thenReturn(mockMetadata); + Session mockSession = mock(Session.class); + when(mockCluster.connect()).thenReturn(mockSession); + when(mockCluster.connect(anyString())).thenReturn(mockSession); + Configuration config = Configuration.builder().build(); + when(mockCluster.getConfiguration()).thenReturn(config); + ResultSetFuture future = mock(ResultSetFuture.class); + ResultSet rs = CassandraQueryTestUtil.createMockResultSet(true); + when(future.getUninterruptibly()).thenReturn(rs); + + try { + doReturn(rs).when(future).getUninterruptibly(anyLong(), any(TimeUnit.class)); + } catch (TimeoutException te) { + throw new IllegalArgumentException("Mocked cluster doesn't time out"); + } + + if (exceptionToThrow != null) { + when(mockSession.execute(anyString(), any(), any())).thenThrow(exceptionToThrow); + when(mockSession.execute(anyString())).thenThrow(exceptionToThrow); + } else { + when(mockSession.execute(anyString(),any(), any())).thenReturn(rs); + when(mockSession.execute(anyString())).thenReturn(rs); + } + } catch (Exception e) { + fail(e.getMessage()); + } + return mockCluster; + } + + public void setExceptionToThrow(Exception e) { + this.exceptionToThrow = e; + } } } + From 500ab00896ccae73e2f6c5f7c8b8245f816f672c Mon Sep 17 00:00:00 2001 From: aglotero Date: Fri, 26 Oct 2018 10:19:20 -0300 Subject: [PATCH 2/6] Addressing comments from code review --- .../nifi/processors/cassandra/AbstractCassandraProcessor.java | 2 +- .../org/apache/nifi/processors/cassandra/QueryCassandra.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java index a9883b2b2b45..d0753698a15b 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java @@ -135,7 +135,7 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { // Relationships public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") - .description("Successfully created FlowFile from CQL query result set.") + .description("A FlowFile is transferred to this relationship if the operation completed successfully.") .build(); static final Relationship REL_ORIGINAL = new Relationship.Builder() diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index beaf4a7e72ab..f9e26ada4c0b 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -32,7 +32,7 @@ import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.DatumWriter; -import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; @@ -501,7 +501,7 @@ public static long convertToJsonStream(final ResultSet rs, long maxRowsPerFlowFi try { row = rs.iterator().next(); }catch (NoSuchElementException nsee){ - //nrOfRows -= 1; + nrOfRows -= 1; break; } if (nrOfRows != 0) { From d84419a344c09530de969077d1773496fe94cda8 Mon Sep 17 00:00:00 2001 From: aglotero Date: Tue, 6 Nov 2018 22:49:50 -0200 Subject: [PATCH 3/6] Adjustments on timestamp datatype formatting --- .../cassandra/AbstractCassandraProcessor.java | 8 +++-- .../processors/cassandra/QueryCassandra.java | 35 +++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java index d0753698a15b..76eae2b06806 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java @@ -50,7 +50,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; +import java.text.SimpleDateFormat; /** * AbstractCassandraProcessor is a base class for Cassandra processors and contains logic and variables common to most @@ -312,8 +314,10 @@ protected static Object getCassandraObject(Row row, int i, DataType dataType) { return row.getDouble(i); } else if (dataType.equals(DataType.timestamp())) { - return row.getTimestamp(i); - + // Timestamp type returned with ISO 8601 format + SimpleDateFormat formatter = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.getDefault()); + return formatter.format(row.getTimestamp(i)); } else if (dataType.equals(DataType.date())) { return row.getDate(i); diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index f9e26ada4c0b..e6a6b12dea21 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -61,14 +61,14 @@ import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.Map; -import java.util.Collection; import java.util.List; import java.util.Set; import java.util.ArrayList; import java.util.HashSet; +import java.util.Collection; import java.util.Collections; -import java.util.TimeZone; import java.util.Date; +import java.util.TimeZone; import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -278,14 +278,12 @@ public void process(final OutputStream out) throws IOException { out, charset, 0, null)); } } - } catch (final TimeoutException | InterruptedException | ExecutionException e) { throw new ProcessException(e); } } }); - // set attribute how many rows were selected fileToProcess = session.putAttribute(fileToProcess, RESULT_ROW_COUNT, String.valueOf(nrOfRows.get())); @@ -320,8 +318,8 @@ public void process(final OutputStream out) throws IOException { } fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_RETRY); - } catch (final QueryExecutionException qee) { + //session.rollback(); logger.error("Cannot execute the query with the requested consistency level successfully", qee); if (fileToProcess == null) { fileToProcess = session.create(); @@ -402,19 +400,20 @@ public static long convertToAvroStream(final ResultSet rs, long maxRowsPerFlowFi final Schema schema = createSchema(rs); final GenericRecord rec = new GenericData.Record(schema); - final DatumWriter datumWriter = new GenericDatumWriter<>(schema); + try (final DataFileWriter dataFileWriter = new DataFileWriter<>(datumWriter)) { dataFileWriter.create(schema, outStream); - final ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); + ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); long nrOfRows = 0; long rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); if (columnDefinitions != null) { // Grab the ones we have - if (rowsAvailableWithoutFetching == 0) { + if (rowsAvailableWithoutFetching == 0 + || rowsAvailableWithoutFetching < maxRowsPerFlowFile) { // Get more if (timeout <= 0 || timeUnit == null) { rs.fetchMoreResults().get(); @@ -429,6 +428,7 @@ public static long convertToAvroStream(final ResultSet rs, long maxRowsPerFlowFi } Row row; + //Iterator it = rs.iterator(); while(nrOfRows < maxRowsPerFlowFile){ try { row = rs.iterator().next(); @@ -437,6 +437,12 @@ public static long convertToAvroStream(final ResultSet rs, long maxRowsPerFlowFi break; } + // iterator().next() is like iterator().one() => return null on end + // https://docs.datastax.com/en/drivers/java/2.0/com/datastax/driver/core/ResultSet.html#one-- + if(row == null){ + break; + } + for (int i = 0; i < columnDefinitions.size(); i++) { final DataType dataType = columnDefinitions.getType(i); @@ -446,9 +452,9 @@ public static long convertToAvroStream(final ResultSet rs, long maxRowsPerFlowFi rec.put(i, getCassandraObject(row, i, dataType)); } } + dataFileWriter.append(rec); nrOfRows += 1; - } } return nrOfRows; @@ -476,7 +482,7 @@ public static long convertToJsonStream(final ResultSet rs, long maxRowsPerFlowFi try { // Write the initial object brace outStream.write("{\"results\":[".getBytes(charset)); - final ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); + ColumnDefinitions columnDefinitions = rs.getColumnDefinitions(); long nrOfRows = 0; long rowsAvailableWithoutFetching = rs.getAvailableWithoutFetching(); @@ -496,6 +502,7 @@ public static long convertToJsonStream(final ResultSet rs, long maxRowsPerFlowFi if(maxRowsPerFlowFile == 0){ maxRowsPerFlowFile = rowsAvailableWithoutFetching; } + Row row; while(nrOfRows < maxRowsPerFlowFile){ try { @@ -504,9 +511,17 @@ public static long convertToJsonStream(final ResultSet rs, long maxRowsPerFlowFi nrOfRows -= 1; break; } + + // iterator().next() is like iterator().one() => return null on end + // https://docs.datastax.com/en/drivers/java/2.0/com/datastax/driver/core/ResultSet.html#one-- + if(row == null){ + break; + } + if (nrOfRows != 0) { outStream.write(",".getBytes(charset)); } + outStream.write("{".getBytes(charset)); for (int i = 0; i < columnDefinitions.size(); i++) { final DataType dataType = columnDefinitions.getType(i); From 6055f142d057d7107ecefb247d542c2f2cf9038d Mon Sep 17 00:00:00 2001 From: aglotero Date: Fri, 30 Nov 2018 16:44:15 -0200 Subject: [PATCH 4/6] Created the OUTPUT_BATCH_SIZE property --- .../cassandra/AbstractCassandraProcessor.java | 6 +-- .../processors/cassandra/QueryCassandra.java | 51 ++++++++++++++++--- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java index 76eae2b06806..ad05cbb552a8 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java @@ -142,16 +142,16 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { static final Relationship REL_ORIGINAL = new Relationship.Builder() .name("original") - .description("All input FlowFiles that are part of a successful query execution go here.") + .description("All input FlowFiles that are part of a successful CQL operation execution go here.") .build(); public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") - .description("CQL query execution failed.") + .description("CQL operation execution failed.") .build(); public static final Relationship REL_RETRY = new Relationship.Builder().name("retry") - .description("A FlowFile is transferred to this relationship if the query cannot be completed but attempting " + .description("A FlowFile is transferred to this relationship if the operation cannot be completed but attempting " + "the operation again may succeed.") .build(); diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index e6a6b12dea21..b94a5fa0b75a 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -130,6 +130,20 @@ public class QueryCassandra extends AbstractCassandraProcessor { .addValidator(StandardValidators.INTEGER_VALIDATOR) .build(); + public static final PropertyDescriptor OUTPUT_BATCH_SIZE = new PropertyDescriptor.Builder() + .name("qdbt-output-batch-size") + .displayName("Output Batch Size") + .description("The number of output FlowFiles to queue before committing the process session. When set to zero, the session will be committed when all result set rows " + + "have been processed and the output FlowFiles are ready for transfer to the downstream relationship. For large result sets, this can cause a large burst of FlowFiles " + + "to be transferred at the end of processor execution. If this property is set, then when the specified number of FlowFiles are ready for transfer, then the session will " + + "be committed, thus releasing the FlowFiles to the downstream relationship. NOTE: The maxvalue.* and fragment.count attributes will not be set on FlowFiles when this " + + "property is set.") + .defaultValue("0") + .required(true) + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .build(); + public static final PropertyDescriptor OUTPUT_FORMAT = new PropertyDescriptor.Builder() .name("Output Format") .description("The format to which the result rows will be converted. If JSON is selected, the output will " @@ -155,6 +169,7 @@ public class QueryCassandra extends AbstractCassandraProcessor { _propertyDescriptors.add(QUERY_TIMEOUT); _propertyDescriptors.add(FETCH_SIZE); _propertyDescriptors.add(MAX_ROWS_PER_FLOW_FILE); + _propertyDescriptors.add(OUTPUT_BATCH_SIZE); _propertyDescriptors.add(OUTPUT_FORMAT); propertyDescriptors = Collections.unmodifiableList(_propertyDescriptors); @@ -226,12 +241,13 @@ public void onTrigger(final ProcessContext context, final ProcessSession session final long queryTimeout = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions(inputFlowFile).asTimePeriod(TimeUnit.MILLISECONDS); final String outputFormat = context.getProperty(OUTPUT_FORMAT).getValue(); final long maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions().asInteger(); + final long outputBatchSize = context.getProperty(OUTPUT_BATCH_SIZE).evaluateAttributeExpressions().asInteger(); final Charset charset = Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(inputFlowFile).getValue()); final StopWatch stopWatch = new StopWatch(true); - if(inputFlowFile != null){ + /*if(inputFlowFile != null){ session.transfer(inputFlowFile, REL_ORIGINAL); - } + }*/ try { // The documentation for the driver recommends the session remain open the entire time the processor is running @@ -247,13 +263,21 @@ public void onTrigger(final ProcessContext context, final ProcessSession session final AtomicLong nrOfRows = new AtomicLong(0L); + long flowFileCount = 0; + do { - fileToProcess = session.create(); + //fileToProcess = session.create(); // Assuring that if we have an input FlowFile // the generated output inherit the attributes - if(attributes != null){ - fileToProcess = session.putAllAttributes(fileToProcess, attributes); + //if(attributes != null){ + // fileToProcess = session.putAllAttributes(fileToProcess, attributes); + //} + + if(inputFlowFile != null){ + fileToProcess = session.create(inputFlowFile); + }else{ + fileToProcess = session.create(); } fileToProcess = session.write(fileToProcess, new OutputStreamCallback() { @@ -296,7 +320,16 @@ public void process(final OutputStream out) throws IOException { session.getProvenanceReporter().modifyContent(fileToProcess, "Retrieved " + nrOfRows.get() + " rows", stopWatch.getElapsed(TimeUnit.MILLISECONDS)); session.transfer(fileToProcess, REL_SUCCESS); - session.commit(); + + if (outputBatchSize > 0) { + flowFileCount++; + + if (flowFileCount== outputBatchSize) { + session.commit(); + flowFileCount = 0; + } + } + try { resultSet.fetchMoreResults().get(); @@ -319,7 +352,6 @@ public void process(final OutputStream out) throws IOException { fileToProcess = session.penalize(fileToProcess); session.transfer(fileToProcess, REL_RETRY); } catch (final QueryExecutionException qee) { - //session.rollback(); logger.error("Cannot execute the query with the requested consistency level successfully", qee); if (fileToProcess == null) { fileToProcess = session.create(); @@ -366,6 +398,11 @@ public void process(final OutputStream out) throws IOException { context.yield(); } } + + if(inputFlowFile != null){ + session.transfer(inputFlowFile, REL_ORIGINAL); + } + session.commit(); } From ca49d4049caf2cf8575634b63f21e647f579ce1f Mon Sep 17 00:00:00 2001 From: aglotero Date: Wed, 23 Jan 2019 14:49:18 -0200 Subject: [PATCH 5/6] Code review adjustments --- .../apache/nifi/processors/cassandra/QueryCassandra.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index b94a5fa0b75a..c57c53208697 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -266,14 +266,6 @@ public void onTrigger(final ProcessContext context, final ProcessSession session long flowFileCount = 0; do { - //fileToProcess = session.create(); - - // Assuring that if we have an input FlowFile - // the generated output inherit the attributes - //if(attributes != null){ - // fileToProcess = session.putAllAttributes(fileToProcess, attributes); - //} - if(inputFlowFile != null){ fileToProcess = session.create(inputFlowFile); }else{ From cd8f565870c2dee6c7f51eac33a95b423a152e28 Mon Sep 17 00:00:00 2001 From: aglotero Date: Thu, 24 Jan 2019 11:48:31 -0200 Subject: [PATCH 6/6] Merge with master (1.9-SNAPSHOT) --- NOTICE | 3 + README.md | 9 +- SECURITY.md | 54 + nifi-api/pom.xml | 2 +- .../AbstractDocumentationWriter.java | 285 ++ .../ExtensionDocumentationWriter.java | 42 + .../nifi/documentation/ExtensionType.java | 25 + .../documentation/ProvidedServiceAPI.java | 51 + .../StandardProvidedServiceAPI.java | 51 + ...ontrollerServiceInitializationContext.java | 66 + ...ntationProcessorInitializationContext.java | 65 + ...ntationReportingInitializationContext.java | 89 + .../init/EmptyControllerServiceLookup.java | 54 + .../documentation/init/NopComponentLog.java | 172 + .../documentation/init/NopStateManager.java | 43 + .../init/StandaloneNodeTypeProvider.java | 31 + .../xml/XmlDocumentationWriter.java | 407 +++ nifi-assembly/NOTICE | 7 +- nifi-assembly/README.md | 2 +- nifi-assembly/pom.xml | 236 +- nifi-assembly/src/main/assembly/common.xml | 9 + nifi-bootstrap/pom.xml | 12 +- .../nifi-data-provenance-utils/pom.xml | 14 +- nifi-commons/nifi-expression-language/pom.xml | 6 +- .../language/antlr/AttributeExpressionLexer.g | 2 +- .../language/StandardPreparedQuery.java | 21 +- .../evaluation/functions/AndEvaluator.java | 12 +- .../evaluation/functions/OrEvaluator.java | 14 +- .../expression/language/TestQuery.java | 36 + nifi-commons/nifi-flowfile-packager/pom.xml | 2 +- nifi-commons/nifi-hl7-query-language/pom.xml | 2 +- .../nifi/hl7/query/antlr/HL7QueryLexer.g | 2 +- nifi-commons/nifi-json-utils/pom.xml | 6 +- nifi-commons/nifi-logging-utils/pom.xml | 2 +- nifi-commons/nifi-properties/pom.xml | 2 +- .../org/apache/nifi/util/NiFiProperties.java | 24 +- .../NiFiProperties/conf/nifi.blank.properties | 1 - .../conf/nifi.missing.properties | 1 - .../NiFiProperties/conf/nifi.properties | 1 - nifi-commons/nifi-record-path/pom.xml | 11 +- .../apache/nifi/record/path/RecordPathLexer.g | 2 +- .../record/path/util/RecordPathCache.java | 36 +- .../nifi/record/path/TestRecordPath.java | 127 + nifi-commons/nifi-record/pom.xml | 2 +- .../serialization/SimpleRecordSchema.java | 36 +- .../serialization/record/RecordSchema.java | 11 + .../record/type/RecordDataType.java | 2 +- .../record/util/DataTypeUtils.java | 10 + .../serialization/TestSimpleRecordSchema.java | 30 +- .../record/TestDataTypeUtils.java | 28 + nifi-commons/nifi-schema-utils/pom.xml | 2 +- .../repository/schema/RecordIterator.java | 28 + .../repository/schema/SchemaRecordReader.java | 68 +- .../repository/schema/SchemaRecordWriter.java | 9 +- .../schema/SingleRecordIterator.java | 45 + nifi-commons/nifi-security-utils/pom.xml | 8 +- ...tabUser.java => AbstractKerberosUser.java} | 41 +- .../nifi/security/krb/ConfigurationUtil.java | 25 + ...{KeytabAction.java => KerberosAction.java} | 34 +- .../nifi/security/krb/KerberosKeytabUser.java | 59 + .../security/krb/KerberosPasswordUser.java | 110 + .../{KeytabUser.java => KerberosUser.java} | 7 +- .../security/krb/KeytabConfiguration.java | 9 +- .../apache/nifi/security/krb/KDCServer.java | 5 +- ...{KeytabUserIT.java => KerberosUserIT.java} | 63 +- .../security/krb/TestKeytabConfiguration.java | 2 +- nifi-commons/nifi-site-to-site-client/pom.xml | 12 +- nifi-commons/nifi-socket-utils/pom.xml | 8 +- nifi-commons/nifi-utils/pom.xml | 6 +- .../nifi/stream/io/RepeatingInputStream.java | 103 + .../io/util/AbstractTextDemarcator.java | 147 + .../nifi/stream/io/util/LineDemarcator.java | 116 + .../org/apache/nifi/util/FormatUtils.java | 227 +- .../processor/TestFormatUtilsGroovy.groovy | 130 - .../nifi/util/TestFormatUtilsGroovy.groovy | 488 +++ .../stream/io/util/TestLineDemarcator.java | 148 + nifi-commons/nifi-web-utils/pom.xml | 4 +- .../org/apache/nifi/web/util/WebUtils.java | 19 +- .../apache/nifi/web/util/WebUtilsTest.groovy | 43 +- nifi-commons/nifi-write-ahead-log/pom.xml | 4 +- .../nifi/wali/LengthDelimitedJournal.java | 119 +- .../wali/SequentialAccessWriteAheadLog.java | 6 +- .../apache/nifi/wali/WriteAheadJournal.java | 5 + .../org/wali/MinimalLockingWriteAheadLog.java | 3 + .../src/main/java/org/wali/SerDe.java | 34 + .../java/org/wali/WriteAheadRepository.java | 2 +- .../TestSequentialAccessWriteAheadLog.java | 89 +- .../test/java/org/wali/DummyRecordSerde.java | 83 + nifi-commons/pom.xml | 2 +- nifi-docker/dockerhub/DockerImage.txt | 2 +- nifi-docker/dockerhub/Dockerfile | 2 +- nifi-docker/dockerhub/README.md | 7 +- nifi-docker/dockerhub/pom.xml | 2 +- nifi-docker/dockerhub/sh/start.sh | 2 +- nifi-docker/dockermaven/pom.xml | 2 +- nifi-docker/dockermaven/sh/start.sh | 2 +- nifi-docker/pom.xml | 4 +- nifi-docs/pom.xml | 2 +- .../main/asciidoc/administration-guide.adoc | 1088 +----- .../src/main/asciidoc/developer-guide.adoc | 2 +- .../images/cluster_connection_summary.png | Bin 0 -> 106868 bytes .../asciidoc/images/connection-settings.png | Bin 47345 -> 73259 bytes .../images/disconnected-node-cluster-mgt.png | Bin 0 -> 92532 bytes .../src/main/asciidoc/images/iconConnect.png | Bin 0 -> 595 bytes .../src/main/asciidoc/images/iconDetails.png | Bin 362 -> 704 bytes .../main/asciidoc/images/iconDisconnect.png | Bin 0 -> 618 bytes .../asciidoc/images/iconDownloadTemplate.png | Bin 0 -> 929 bytes .../main/asciidoc/images/iconLoadBalance.png | Bin 0 -> 792 bytes .../src/main/asciidoc/images/iconOffload.png | Bin 0 -> 589 bytes .../images/load_balance_active_connection.png | Bin 0 -> 35045 bytes .../load_balance_compression_options.png | Bin 0 -> 83986 bytes .../load_balance_configured_connection.png | Bin 0 -> 30628 bytes .../load_balance_distributed_connection.png | Bin 0 -> 12577 bytes .../images/offloaded-node-cluster-mgt.png | Bin 0 -> 91953 bytes .../images/offloading-node-cluster-mgt.png | Bin 0 -> 91042 bytes .../images/primary-node-cluster-mgt.png | Bin 0 -> 96308 bytes .../main/asciidoc/images/scheduling-tab.png | Bin 49115 -> 54320 bytes .../asciidoc/images/summary_connections.png | Bin 0 -> 113698 bytes .../src/main/asciidoc/toolkit-guide.adoc | 1279 +++++++ nifi-docs/src/main/asciidoc/user-guide.adoc | 202 +- .../nifi-nifi-example-nar/pom.xml | 2 +- .../nifi-nifi-example-processors/pom.xml | 8 +- nifi-external/nifi-example-bundle/pom.xml | 4 +- nifi-external/nifi-spark-receiver/pom.xml | 4 +- nifi-external/nifi-storm-spout/pom.xml | 4 +- nifi-external/pom.xml | 2 +- nifi-framework-api/pom.xml | 4 +- .../org/apache/nifi/authorization/Group.java | 2 +- .../nifi/controller/queue/FlowFileQueue.java | 16 + .../queue/LoadBalancedFlowFileQueue.java | 6 + .../status/history/MetricDescriptor.java | 5 + .../nifi-processor-bundle-archetype/pom.xml | 2 +- .../nifi-service-bundle-archetype/pom.xml | 2 +- nifi-maven-archetypes/pom.xml | 2 +- nifi-mock/pom.xml | 12 +- .../apache/nifi/util/MockProcessSession.java | 9 +- .../nifi-ambari-nar/pom.xml | 4 +- .../nifi-ambari-reporting-task/pom.xml | 8 +- nifi-nar-bundles/nifi-ambari-bundle/pom.xml | 2 +- .../nifi-amqp-bundle/nifi-amqp-nar/pom.xml | 6 +- .../nifi-amqp-processors/pom.xml | 6 +- .../processors/AbstractAMQPProcessor.java | 25 +- .../processors/AbstractAMQPProcessorTest.java | 12 +- nifi-nar-bundles/nifi-amqp-bundle/pom.xml | 6 +- .../nifi-atlas-bundle/nifi-atlas-nar/pom.xml | 4 +- .../nifi-atlas-reporting-task/pom.xml | 21 +- nifi-nar-bundles/nifi-atlas-bundle/pom.xml | 14 +- .../nifi-avro-bundle/nifi-avro-nar/pom.xml | 6 +- .../nifi-avro-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-avro-bundle/pom.xml | 4 +- .../nifi-aws-abstract-processors/pom.xml | 29 +- .../processors/aws/AbstractAWSProcessor.java | 37 +- .../CredentialPropertyDescriptors.java | 4 +- .../processors/aws/regions/AWSRegions.java | 58 - .../nifi-aws-bundle/nifi-aws-nar/pom.xml | 6 +- .../nifi-aws-processors/pom.xml | 12 +- .../apache/nifi/processors/aws/s3/ListS3.java | 43 +- .../nifi/processors/aws/s3/TagS3Object.java | 201 ++ .../org.apache.nifi.processor.Processor | 1 + .../nifi/processors/aws/s3/ITTagS3Object.java | 150 + .../processors/aws/s3/TestTagS3Object.java | 319 ++ .../nifi-aws-service-api-nar/pom.xml | 6 +- .../nifi-aws-service-api/pom.xml | 25 +- nifi-nar-bundles/nifi-aws-bundle/pom.xml | 4 +- .../nifi-azure-bundle/nifi-azure-nar/pom.xml | 6 +- .../nifi-azure-processors/pom.xml | 8 +- .../azure/storage/DeleteAzureBlobStorage.java | 39 +- nifi-nar-bundles/nifi-azure-bundle/pom.xml | 2 +- .../nifi-beats-bundle/nifi-beats-nar/pom.xml | 8 +- .../nifi-beats-processors/pom.xml | 12 +- nifi-nar-bundles/nifi-beats-bundle/pom.xml | 4 +- .../nifi-cassandra-nar/pom.xml | 6 +- .../nifi-cassandra-processors/pom.xml | 18 +- .../cassandra/AbstractCassandraProcessor.java | 140 +- .../processors/cassandra/PutCassandraQL.java | 23 +- .../cassandra/PutCassandraRecord.java | 29 +- .../processors/cassandra/QueryCassandra.java | 43 +- .../AbstractCassandraProcessorTest.java | 48 +- .../nifi-cassandra-services-api-nar/pom.xml | 45 + .../src/main/resources/META-INF/LICENSE | 505 +-- .../src/main/resources/META-INF/NOTICE | 226 ++ .../nifi-cassandra-services-api/pom.xml | 43 + .../CassandraSessionProviderService.java | 35 + .../nifi-cassandra-services-nar/pom.xml | 43 + .../src/main/resources/META-INF/LICENSE | 369 ++ .../src/main/resources/META-INF/NOTICE | 291 ++ .../nifi-cassandra-services/pom.xml | 82 + .../service/CassandraSessionProvider.java | 285 ++ ...g.apache.nifi.controller.ControllerService | 16 + .../nifi/service/MockCassandraProcessor.java | 51 + .../service/TestCassandraSessionProvider.java | 59 + .../nifi-cassandra-bundle/pom.xml | 12 +- .../nifi-ccda-bundle/nifi-ccda-nar/pom.xml | 2 +- .../nifi-ccda-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-ccda-bundle/pom.xml | 4 +- .../nifi-cdc/nifi-cdc-api/pom.xml | 2 +- .../nifi-cdc-mysql-nar/pom.xml | 4 +- .../nifi-cdc-mysql-processors/pom.xml | 10 +- .../mysql/processors/CaptureChangeMySQL.java | 77 +- .../processors/CaptureChangeMySQLTest.groovy | 3 +- .../nifi-cdc/nifi-cdc-mysql-bundle/pom.xml | 4 +- nifi-nar-bundles/nifi-cdc/pom.xml | 2 +- .../nifi-confluent-platform-nar/pom.xml | 6 +- .../pom.xml | 6 +- .../client/CachingSchemaRegistryClient.java | 104 +- .../nifi-confluent-platform-bundle/pom.xml | 2 +- .../nifi-couchbase-nar/pom.xml | 8 +- .../nifi-couchbase-processors/pom.xml | 8 +- .../couchbase/TestPutCouchbaseKey.java | 1 - .../nifi-couchbase-services-api-nar/pom.xml | 8 +- .../nifi-couchbase-services-api/pom.xml | 2 +- .../nifi-couchbase-bundle/pom.xml | 4 +- .../nifi-cybersecurity-nar/pom.xml | 6 +- .../nifi-cybersecurity-processors/pom.xml | 6 +- .../nifi-cybersecurity-bundle/pom.xml | 4 +- .../nifi-datadog-nar/pom.xml | 2 +- .../nifi-datadog-reporting-task/pom.xml | 6 +- nifi-nar-bundles/nifi-datadog-bundle/pom.xml | 4 +- .../pom.xml | 6 +- .../nifi-druid-controller-service-api/pom.xml | 9 +- .../nifi-druid-controller-service/pom.xml | 8 +- .../nifi-druid-bundle/nifi-druid-nar/pom.xml | 8 +- .../nifi-druid-processors/pom.xml | 12 +- nifi-nar-bundles/nifi-druid-bundle/pom.xml | 4 +- .../nifi-elasticsearch-5-nar/pom.xml | 4 +- .../nifi-elasticsearch-5-processors/pom.xml | 6 +- .../pom.xml | 8 +- .../pom.xml | 9 +- .../pom.xml | 8 +- .../nifi-elasticsearch-client-service/pom.xml | 23 +- .../nifi-elasticsearch-nar/pom.xml | 4 +- .../nifi-elasticsearch-processors/pom.xml | 15 +- .../PutElasticsearchHttpRecord.java | 64 +- .../TestPutElasticsearchHttpRecord.java | 117 +- .../nifi-elasticsearch-restapi-nar/pom.xml | 6 +- .../pom.xml | 18 +- .../nifi-elasticsearch-bundle/pom.xml | 8 +- .../nifi-email-bundle/nifi-email-nar/pom.xml | 4 +- .../nifi-email-processors/pom.xml | 8 +- nifi-nar-bundles/nifi-email-bundle/pom.xml | 4 +- .../nifi-enrich-nar/pom.xml | 2 +- .../nifi-enrich-processors/pom.xml | 6 +- .../apache/nifi/processors/GeoEnrichIP.java | 18 +- .../apache/nifi/processors/ISPEnrichIP.java | 3 +- .../processors/maxmind/DatabaseReader.java | 126 +- .../nifi/processors/TestGeoEnrichIP.java | 5 +- .../nifi/processors/TestISPEnrichIP.java | 5 +- nifi-nar-bundles/nifi-enrich-bundle/pom.xml | 4 +- .../nifi-evtx-bundle/nifi-evtx-nar/pom.xml | 4 +- .../nifi-evtx-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-evtx-bundle/pom.xml | 4 +- .../nifi-hadoop-utils/pom.xml | 6 +- .../nifi-processor-utils/pom.xml | 8 +- .../util/list/AbstractListProcessor.java | 8 +- .../nifi-avro-record-utils/pom.xml | 11 +- .../org/apache/nifi/avro/AvroTypeUtil.java | 6 +- .../WriteAvroSchemaAttributeStrategy.java | 28 +- .../apache/nifi/avro/TestAvroTypeUtil.java | 25 + .../nifi-hadoop-record-utils/pom.xml | 8 +- .../nifi-mock-record-utils/pom.xml | 6 +- .../record/MockRecordParser.java | 6 +- .../nifi-standard-record-utils/pom.xml | 8 +- .../validation/StandardSchemaValidator.java | 37 +- .../TestStandardSchemaValidator.java | 8 + .../nifi-record-utils/pom.xml | 12 +- .../nifi-reporting-utils/pom.xml | 6 +- .../nifi-syslog-utils/pom.xml | 6 +- nifi-nar-bundles/nifi-extension-utils/pom.xml | 2 +- .../nifi-flume-bundle/nifi-flume-nar/pom.xml | 6 +- .../nifi-flume-processors/pom.xml | 8 +- nifi-nar-bundles/nifi-flume-bundle/pom.xml | 6 +- .../nifi-framework-nar/pom.xml | 4 +- .../src/main/resources/META-INF/NOTICE | 2 +- .../nifi-administration/pom.xml | 2 +- .../nifi-framework/nifi-authorizer/pom.xml | 4 +- .../authorization/AuthorizerFactoryBean.java | 11 +- .../resources/nifi-authorizer-context.xml | 1 + .../nifi-framework/nifi-client-dto/pom.xml | 2 +- .../web/api/dto/status/PortStatusDTO.java | 4 +- .../api/dto/status/PortStatusSnapshotDTO.java | 4 +- .../api/dto/status/ProcessorStatusDTO.java | 2 +- .../status/ProcessorStatusSnapshotDTO.java | 2 +- .../dto/status/ReportingTaskStatusDTO.java | 2 +- .../nifi-framework/nifi-documentation/pom.xml | 2 +- .../nifi/documentation/DocGenerator.java | 32 +- .../html/HtmlDocumentationWriter.java | 12 +- .../HtmlProcessorDocumentationWriter.java | 5 + .../nifi/documentation/DocGeneratorTest.java | 29 +- .../html/HtmlDocumentationWriterTest.java | 24 +- .../ProcessorDocumentationWriterTest.java | 38 +- .../src/test/resources/conf/nifi.properties | 1 - .../nifi-file-authorizer/pom.xml | 2 +- .../nifi-flowfile-repo-serialization/pom.xml | 8 +- .../SchemaRepositoryRecordSerde.java | 71 +- .../nifi-framework-authorization/pom.xml | 4 +- .../nifi-framework-cluster-protocol/pom.xml | 6 +- .../coordination/ClusterCoordinator.java | 25 + .../ClusterTopologyEventListener.java | 3 + .../node/NodeConnectionState.java | 10 + .../node/NodeConnectionStatus.java | 45 +- .../coordination/node/OffloadCode.java | 40 + .../ClusterCoordinationProtocolSender.java | 9 + .../nifi/cluster/protocol/NodeIdentifier.java | 24 +- ...terCoordinationProtocolSenderListener.java | 6 + .../protocol/impl/SocketProtocolListener.java | 3 + ...dardClusterCoordinationProtocolSender.java | 26 + .../message/AdaptedNodeConnectionStatus.java | 20 +- .../message/NodeConnectionStatusAdapter.java | 6 +- .../protocol/jaxb/message/ObjectFactory.java | 5 + .../protocol/message/OffloadMessage.java | 53 + .../protocol/message/ProtocolMessage.java | 1 + .../ServerSocketConfigurationFactoryBean.java | 2 +- .../nifi-framework-cluster/pom.xml | 2 +- .../PopularVoteFlowElectionFactoryBean.java | 8 +- .../heartbeat/AbstractHeartbeatMonitor.java | 10 +- .../ControllerServiceEndpointMerger.java | 3 + .../http/endpoints/PortEndpointMerger.java | 6 + .../endpoints/ProcessorEndpointMerger.java | 3 + .../RemoteProcessGroupEndpointMerger.java | 3 + .../ReportingTaskEndpointMerger.java | 3 + .../ThreadPoolRequestReplicator.java | 19 + .../node/NodeClusterCoordinator.java | 118 +- .../nifi/cluster/manager/StatusMerger.java | 3 +- .../IllegalNodeOffloadException.java | 38 + .../OffloadedNodeMutableRequestException.java | 39 + .../NodeClusterCoordinatorFactoryBean.java | 9 +- .../nifi-cluster-manager-context.xml | 2 + .../node/NodeClusterCoordinatorSpec.groovy | 99 + .../integration/OffloadNodeITSpec.groovy | 50 + .../flow/TestPopularVoteFlowElection.java | 8 +- .../TestAbstractHeartbeatMonitor.java | 11 + .../node/TestNodeClusterCoordinator.java | 4 +- .../nifi/cluster/integration/Cluster.java | 26 +- .../apache/nifi/cluster/integration/Node.java | 30 +- .../src/test/resources/conf/nifi.properties | 1 - .../nifi-framework-core-api/pom.xml | 6 +- .../controller/AbstractComponentNode.java | 91 +- .../apache/nifi/controller/ComponentNode.java | 16 +- .../apache/nifi/controller/ProcessorNode.java | 46 +- .../nifi/controller/flow/FlowManager.java | 290 ++ .../reporting/ReportingTaskProvider.java | 6 + .../service/ControllerServiceProvider.java | 37 +- .../nifi/logging/LogRepositoryFactory.java | 4 +- .../registry/flow/FlowRegistryClient.java | 12 +- .../nifi/reporting/UserAwareEventAccess.java | 69 + .../controller/TestAbstractComponentNode.java | 3 +- .../registry/flow/TestFlowRegistryClient.java | 109 + .../nifi-framework-core/pom.xml | 24 +- .../validation/TriggerValidationTask.java | 14 +- .../controller/EventDrivenWorkerQueue.java | 12 +- .../nifi/controller/ExtensionBuilder.java | 470 +++ .../nifi/controller/FlowController.java | 3094 ++--------------- .../apache/nifi/controller/FlowSnippet.java | 47 + .../nifi/controller/StandardFlowService.java | 131 +- .../nifi/controller/StandardFlowSnippet.java | 619 ++++ .../controller/StandardFlowSynchronizer.java | 145 +- .../controller/StandardProcessorNode.java | 155 +- .../controller/StandardReloadComponent.java | 209 ++ .../controller/flow/StandardFlowManager.java | 656 ++++ .../controller/kerberos/KerberosConfig.java | 45 + .../queue/AbstractFlowFileQueue.java | 8 +- .../queue/StandardFlowFileQueue.java | 8 + .../queue/SwappablePriorityQueue.java | 26 +- .../SocketLoadBalancedFlowFileQueue.java | 165 +- .../client/async/AsyncLoadBalanceClient.java | 2 + .../async/TransactionCompleteCallback.java | 3 +- .../async/nio/NioAsyncLoadBalanceClient.java | 6 +- .../NioAsyncLoadBalanceClientRegistry.java | 20 +- .../nio/NioAsyncLoadBalanceClientTask.java | 8 +- .../NonLocalPartitionPartitioner.java | 58 + .../partition/RemoteQueuePartition.java | 15 +- .../server/ClusterLoadBalanceAuthorizer.java | 57 +- .../server/LoadBalanceAuthorizer.java | 13 +- .../server/StandardLoadBalanceProtocol.java | 91 +- .../reporting/AbstractReportingTaskNode.java | 26 +- .../reporting/StandardReportingContext.java | 8 +- ...tandardReportingInitializationContext.java | 28 +- .../reporting/StandardReportingTaskNode.java | 14 +- .../repository/FileSystemRepository.java | 13 +- .../repository/StandardProcessSession.java | 214 +- .../repository/StandardQueueProvider.java | 45 + .../repository/io/LimitedInputStream.java | 34 +- .../repository/metrics/EventSumValue.java | 2 +- .../EventDrivenSchedulingAgent.java | 14 +- .../scheduling/QuartzSchedulingAgent.java | 2 +- .../scheduling/StandardProcessScheduler.java | 144 +- .../TimerDrivenSchedulingAgent.java | 2 +- .../serialization/ScheduledStateLookup.java | 2 +- .../serialization/StandardFlowSerializer.java | 4 +- .../service/ControllerServiceLoader.java | 19 +- .../service/GhostControllerService.java | 82 + ...ontrollerServiceInitializationContext.java | 20 +- ...ardControllerServiceInvocationHandler.java | 11 +- .../StandardControllerServiceNode.java | 95 +- .../StandardControllerServiceProvider.java | 262 +- .../state/StandardStateManager.java | 6 +- .../manager/StandardStateManagerProvider.java | 28 +- .../history/AbstractMetricDescriptor.java | 98 + .../history/CounterMetricDescriptor.java | 63 + .../history/StandardMetricDescriptor.java | 74 +- .../history/StandardStatusSnapshot.java | 6 +- .../controller/tasks/ConnectableTask.java | 10 +- .../controller/tasks/ExpireFlowFiles.java | 8 +- .../tasks/ReportingTaskWrapper.java | 9 +- .../nifi/fingerprint/FingerprintFactory.java | 15 +- .../nifi/groups/StandardProcessGroup.java | 197 +- .../repository/StandardLogRepository.java | 6 +- .../StandardXMLFlowConfigurationDAO.java | 8 +- .../processor/StandardProcessContext.java | 36 +- ...tandardProcessorInitializationContext.java | 20 +- .../provenance/ComponentIdentifierLookup.java | 71 + ...StandardProvenanceAuthorizableFactory.java | 119 + .../flow/StandardFlowRegistryClient.java | 16 +- .../flow/mapping/NiFiRegistryFlowMapper.java | 63 +- .../remote/StandardRemoteProcessGroup.java | 81 +- .../nifi/reporting/StandardEventAccess.java | 691 ++++ .../spring/ExtensionManagerFactoryBean.java | 45 + .../spring/FlowControllerFactoryBean.java | 12 +- .../org/apache/nifi/util/BundleUtils.java | 33 +- .../apache/nifi/util/ComponentMetrics.java | 4 +- .../nifi/util/FlowDifferenceFilters.java | 44 + .../src/main/resources/nifi-context.xml | 5 + .../controller/StandardFlowServiceSpec.groovy | 130 + .../NonLocalPartitionPartitionerSpec.groovy | 107 + .../FlowFromDOMFactoryTest.groovy | 82 + .../controller/StandardFlowServiceTest.java | 6 +- .../nifi/controller/TestFlowController.java | 231 +- .../controller/TestStandardProcessorNode.java | 87 +- .../queue/clustered/LoadBalancedQueueIT.java | 16 +- .../TestSocketLoadBalancedFlowFileQueue.java | 20 +- .../async/nio/TestLoadBalanceSession.java | 4 +- .../TestStandardLoadBalanceProtocol.java | 35 +- .../TestStandardReportingContext.java | 29 +- .../repository/TestFileSystemRepository.java | 69 +- .../TestWriteAheadFlowFileRepository.java | 8 + .../repository/io/TestLimitedInputStream.java | 17 +- ...fecycle.java => ProcessorLifecycleIT.java} | 295 +- .../StandardProcessSchedulerIT.java | 96 - .../TestStandardProcessScheduler.java | 259 +- .../StandardFlowSerializerTest.java | 36 +- .../StandardControllerServiceProviderIT.java | 86 +- ...StandardControllerServiceProviderTest.java | 72 +- ...TestStandardControllerServiceProvider.java | 266 +- .../service/mock/MockProcessGroup.java | 8 +- .../fingerprint/FingerprintFactoryTest.java | 8 +- .../nifi/util/TestFlowDifferenceFilters.java | 50 + .../src/test/resources/conf/nifi.properties | 1 - .../flowcontrollertest.nifi.properties | 4 +- .../resources/lifecycletest.nifi.properties | 1 - .../resources/nifi-with-remote.properties | 1 - ...standardflowserializertest.nifi.properties | 1 - ...andardflowsynchronizerspec.nifi.properties | 1 - ...andardprocessschedulertest.nifi.properties | 1 - .../nifi-framework-nar-loading-utils/pom.xml | 46 + .../apache/nifi/nar/ExtensionUiLoader.java | 33 + .../org/apache/nifi/nar/NarAutoLoader.java | 87 + .../apache/nifi/nar/NarAutoLoaderTask.java | 178 + .../java/org/apache/nifi/nar/NarLoader.java | 35 + .../apache/nifi/nar/StandardNarLoader.java | 167 + .../org/apache/nifi/nar/TestNarLoader.java | 189 + .../src/test/resources/README | 9 + .../src/test/resources/conf/nifi.properties | 124 + .../nifi-example-processors-nar-1.0.nar | Bin 0 -> 7221 bytes .../nifi-example-service-api-nar-1.0.nar | Bin 0 -> 4306 bytes .../nifi-example-service-nar-1.1.nar | Bin 0 -> 6409 bytes .../test/resources/lib/nifi-framework-nar.nar | Bin 0 -> 577 bytes .../test/resources/lib/nifi-jetty-bundle.nar | Bin 0 -> 578 bytes .../resources/nifi-example-nars-source.zip | Bin 0 -> 39371 bytes .../nifi-framework-nar-utils/pom.xml | 2 +- ...nfigurableComponentInitializerFactory.java | 10 +- .../init/ControllerServiceInitializer.java | 12 +- .../nifi/init/ProcessorInitializer.java | 12 +- .../init/ReportingTaskingInitializer.java | 12 +- .../nifi/nar/ExtensionDiscoveringManager.java | 47 + .../org/apache/nifi/nar/ExtensionManager.java | 504 +-- .../nifi/nar/ExtensionManagerHolder.java | 61 + .../org/apache/nifi/nar/NarCloseable.java | 6 +- .../nifi/nar/NarThreadContextClassLoader.java | 4 +- .../StandardExtensionDiscoveringManager.java | 527 +++ .../nar/NarThreadContextClassLoaderTest.java | 29 +- .../NarUnpacker/conf/nifi.properties | 1 - .../src/test/resources/nifi.properties | 1 - .../nifi-mock-authorizer/pom.xml | 6 +- .../nifi-framework/nifi-nar-utils/pom.xml | 2 +- .../org/apache/nifi/nar/NarClassLoaders.java | 208 +- .../nifi/nar/NarClassLoadersHolder.java | 41 + .../org/apache/nifi/nar/NarLoadResult.java | 46 + .../java/org/apache/nifi/nar/NarUnpacker.java | 7 +- .../src/test/resources/nifi.properties | 1 - .../nifi-properties-loader/pom.xml | 2 +- ...sitive_properties_protected_aes.properties | 1 - .../test/resources/conf/nifi.blank.properties | 1 - .../resources/conf/nifi.missing.properties | 1 - .../src/test/resources/conf/nifi.properties | 1 - ..._with_additional_sensitive_keys.properties | 1 - ...sitive_properties_protected_aes.properties | 1 - ...rsive_additional_sensitive_keys.properties | 1 - ...sitive_properties_protected_aes.properties | 1 - ...ve_properties_protected_aes_128.properties | 1 - ...ties_protected_aes_128_password.properties | 1 - ...rotected_aes_multiple_malformed.properties | 1 - ..._protected_aes_single_malformed.properties | 1 - ...ve_properties_protected_unknown.properties | 1 - ...ensitive_properties_unprotected.properties | 1 - ...operties_unprotected_extra_line.properties | 1 - .../nifi-repository-models/pom.xml | 6 +- .../repository/StandardRepositoryRecord.java | 71 +- .../TestStandardRepositoryRecord.java | 54 + .../nifi-framework/nifi-resources/pom.xml | 8 +- .../src/main/resources/conf/logback.xml | 3 + .../src/main/resources/conf/nifi.properties | 2 +- .../nifi-framework/nifi-runtime/pom.xml | 2 +- .../src/main/java/org/apache/nifi/NiFi.java | 3 +- .../NiFiProperties/conf/nifi.properties | 1 - ...sitive_properties_protected_aes.properties | 1 - ...ve_properties_protected_aes_128.properties | 1 - ...ies_protected_aes_different_key.properties | 1 - ...protected_aes_different_key_128.properties | 1 - .../nifi-framework/nifi-security/pom.xml | 4 +- .../security/util/SslContextFactory.java | 41 +- .../security/util/SslContextFactoryTest.java | 10 +- .../nifi-framework/nifi-site-to-site/pom.xml | 12 +- .../src/test/resources/nifi.properties | 1 - .../nifi-standard-prioritizers/pom.xml | 2 +- .../nifi-framework/nifi-user-actions/pom.xml | 2 +- .../nifi-web/nifi-custom-ui-utilities/pom.xml | 2 +- .../nifi-web/nifi-jetty/pom.xml | 10 +- .../apache/nifi/web/server/JettyServer.java | 403 ++- .../nifi-web/nifi-ui-extension/pom.xml | 2 +- .../nifi/ui/extension/UiExtensionMapping.java | 14 +- .../nifi-web/nifi-web-api/pom.xml | 2 +- .../StandardAuthorizableLookup.java | 56 +- .../nifi/registry/flow/FlowRegistryUtils.java | 6 +- .../apache/nifi/web/NiFiServiceFacade.java | 31 + .../nifi/web/StandardNiFiServiceFacade.java | 39 +- .../nifi/web/api/ApplicationResource.java | 50 +- .../org/apache/nifi/web/api/FlowResource.java | 4 +- .../nifi/web/api/OutputPortResource.java | 2 +- .../nifi/web/api/ProcessGroupResource.java | 10 +- .../apache/nifi/web/api/TemplateResource.java | 16 +- .../apache/nifi/web/api/VersionsResource.java | 13 +- .../IllegalNodeOffloadExceptionMapper.java | 46 + .../apache/nifi/web/api/dto/DtoFactory.java | 42 +- .../nifi/web/controller/ControllerFacade.java | 134 +- .../controller/ControllerSearchService.java | 4 +- .../nifi/web/dao/impl/ComponentDAO.java | 6 +- .../web/dao/impl/StandardConnectionDAO.java | 8 +- .../impl/StandardControllerServiceDAO.java | 38 +- .../nifi/web/dao/impl/StandardFunnelDAO.java | 8 +- .../web/dao/impl/StandardInputPortDAO.java | 10 +- .../nifi/web/dao/impl/StandardLabelDAO.java | 8 +- .../web/dao/impl/StandardOutputPortDAO.java | 10 +- .../web/dao/impl/StandardProcessGroupDAO.java | 27 +- .../web/dao/impl/StandardProcessorDAO.java | 25 +- .../impl/StandardRemoteProcessGroupDAO.java | 12 +- .../dao/impl/StandardReportingTaskDAO.java | 14 +- .../nifi/web/dao/impl/StandardSnippetDAO.java | 24 +- .../web/dao/impl/StandardTemplateDAO.java | 16 +- .../ControllerServiceProviderFactoryBean.java | 2 +- .../apache/nifi/web/util/SnippetUtils.java | 4 +- .../main/resources/nifi-web-api-context.xml | 16 +- .../web/api/ApplicationResourceTest.groovy | 101 +- .../dao/impl/StandardTemplateDAOSpec.groovy | 12 +- .../StandardAuthorizableLookupTest.java | 8 + .../accesscontrol/AccessControlHelper.java | 28 +- .../accesscontrol/ITAccessTokenEndpoint.java | 28 +- .../TestStandardRemoteProcessGroupDAO.java | 7 +- .../access-control/nifi-flow.properties | 1 - .../resources/access-control/nifi.properties | 1 - .../test/resources/lib/nifi-framework-nar.nar | Bin 0 -> 406 bytes .../test/resources/lib/nifi-jetty-bundle.nar | Bin 0 -> 578 bytes .../resources/site-to-site/nifi.properties | 1 - .../nifi-web/nifi-web-content-access/pom.xml | 2 +- .../nifi-web/nifi-web-content-viewer/pom.xml | 2 +- .../nifi/web/ContentViewerController.java | 3 +- .../nifi-web/nifi-web-docs/pom.xml | 2 +- .../main/webapp/WEB-INF/jsp/documentation.jsp | 1 + .../nifi-web/nifi-web-error/pom.xml | 2 +- .../nifi-web-optimistic-locking/pom.xml | 2 +- .../nifi-web/nifi-web-security/pom.xml | 2 +- .../LoginIdentityProviderFactoryBean.java | 8 +- .../resources/nifi-web-security-context.xml | 1 + .../nifi-web/nifi-web-ui/pom.xml | 2 +- .../src/main/frontend/package-lock.json | 6 +- .../src/main/frontend/package.json | 2 +- .../apache/nifi/web/filter/LogoutFilter.java | 2 +- .../src/main/resources/META-INF/NOTICE | 2 +- .../canvas/connection-configuration.jsp | 59 +- .../WEB-INF/partials/connection-details.jsp | 67 +- .../main/webapp/css/connection-details.css | 16 +- .../nifi-web-ui/src/main/webapp/css/graph.css | 16 +- .../webapp/js/nf/canvas/nf-component-state.js | 74 +- .../nf/canvas/nf-connection-configuration.js | 62 +- .../main/webapp/js/nf/canvas/nf-connection.js | 76 +- .../webapp/js/nf/canvas/nf-queue-listing.js | 272 +- .../webapp/js/nf/cluster/nf-cluster-table.js | 75 +- .../src/main/webapp/js/nf/nf-common.js | 51 + .../webapp/js/nf/nf-connection-details.js | 26 +- .../nifi-framework/nifi-web/pom.xml | 12 +- .../nifi-framework/pom.xml | 5 +- .../nifi-framework-bundle/pom.xml | 79 +- .../nifi-gcp-bundle/nifi-gcp-nar/pom.xml | 6 +- .../nifi-gcp-processors/pom.xml | 20 +- .../processors/gcp/AbstractGCPProcessor.java | 50 +- .../bigquery/AbstractBigQueryProcessor.java | 160 + .../gcp/bigquery/BigQueryAttributes.java | 141 + .../gcp/bigquery/BigQueryUtils.java | 84 + .../gcp/bigquery/PutBigQueryBatch.java | 357 ++ .../gcp/pubsub/AbstractGCPubSubProcessor.java | 21 + .../gcp/storage/AbstractGCSProcessor.java | 55 +- .../processors/gcp/storage/ListGCSBucket.java | 11 +- .../org.apache.nifi.processor.Processor | 3 +- .../gcp/bigquery/AbstractBQTest.java | 96 + .../gcp/bigquery/AbstractBigQueryIT.java | 79 + .../gcp/bigquery/PutBigQueryBatchIT.java | 137 + .../gcp/bigquery/PutBigQueryBatchTest.java | 153 + .../gcp/storage/ListGCSBucketTest.java | 1 - .../nifi-gcp-services-api-nar/pom.xml | 8 +- .../nifi-gcp-services-api/pom.xml | 3 +- nifi-nar-bundles/nifi-gcp-bundle/pom.xml | 9 +- .../nifi-groovyx-nar/pom.xml | 4 +- .../nifi-groovyx-processors/pom.xml | 9 +- nifi-nar-bundles/nifi-groovyx-bundle/pom.xml | 4 +- .../nifi-grpc-bundle/nifi-grpc-nar/pom.xml | 8 +- .../nifi-grpc-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-grpc-bundle/pom.xml | 6 +- .../nifi-hadoop-nar/pom.xml | 4 +- .../nifi-hdfs-processors/pom.xml | 10 +- nifi-nar-bundles/nifi-hadoop-bundle/pom.xml | 4 +- .../nifi-hadoop-libraries-nar/pom.xml | 12 +- .../nifi-hadoop-libraries-bundle/pom.xml | 12 +- .../nifi-hbase-bundle/nifi-hbase-nar/pom.xml | 4 +- .../nifi-hbase-processors/pom.xml | 14 +- nifi-nar-bundles/nifi-hbase-bundle/pom.xml | 4 +- .../nifi-hive-bundle/nifi-hive-nar/pom.xml | 8 +- .../nifi-hive-processors/pom.xml | 14 +- .../nifi/dbcp/hive/HiveConnectionPool.java | 2 + .../nifi/processors/hive/PutHiveQL.java | 10 +- .../processors/hive/PutHiveStreaming.java | 1 + .../nifi/processors/hive/SelectHiveQL.java | 4 +- .../apache/nifi/util/hive/HiveJdbcCommon.java | 13 + .../dbcp/hive/HiveConnectionPoolTest.java | 84 +- .../nifi/processors/hive/TestPutHiveQL.java | 80 +- .../processors/hive/TestSelectHiveQL.java | 5 +- .../src/test/resources/hive-site-security.xml | 4 + .../src/test/resources/krb5.conf | 10 + .../nifi-hive-services-api-nar/pom.xml | 8 +- .../nifi-hive-services-api/pom.xml | 6 +- .../nifi-hive-bundle/nifi-hive3-nar/pom.xml | 8 +- .../src/main/resources/META-INF/NOTICE | 2 +- .../nifi-hive3-processors/pom.xml | 18 +- .../hadoop/hive/ql/io/orc/NiFiOrcUtils.java | 498 ++- .../hive/streaming/NiFiRecordSerDe.java | 28 +- .../nifi/dbcp/hive/Hive3ConnectionPool.java | 2 + .../nifi/processors/hive/PutHive3QL.java | 22 +- .../processors/hive/PutHive3Streaming.java | 11 +- .../apache/nifi/processors/orc/PutORC.java | 18 +- .../orc/record/ORCHDFSRecordWriter.java | 31 +- .../apache/nifi/util/hive/HiveJdbcCommon.java | 14 + .../dbcp/hive/Hive3ConnectionPoolTest.java | 84 +- .../nifi/processors/hive/TestPutHive3QL.java | 79 +- .../hive/TestPutHive3Streaming.java | 197 +- .../processors/hive/TestSelectHive3QL.java | 5 +- .../nifi/processors/orc/PutORCTest.java | 47 +- .../nifi/util/orc/TestNiFiOrcUtils.java | 142 +- .../src/test/resources/hive-site-security.xml | 4 + .../src/test/resources/krb5.conf | 10 + .../src/test/resources/nested_record.avsc | 42 + nifi-nar-bundles/nifi-hive-bundle/pom.xml | 15 +- .../nifi-hl7-bundle/nifi-hl7-nar/pom.xml | 4 +- .../nifi-hl7-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-hl7-bundle/pom.xml | 6 +- .../nifi-html-bundle/nifi-html-nar/pom.xml | 4 +- .../nifi-html-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-html-bundle/pom.xml | 4 +- .../nifi-ignite-nar/pom.xml | 4 +- .../nifi-ignite-processors/pom.xml | 9 +- nifi-nar-bundles/nifi-ignite-bundle/pom.xml | 15 +- .../nifi-influxdb-nar/pom.xml | 4 +- .../nifi-influxdb-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-influxdb-bundle/pom.xml | 4 +- nifi-nar-bundles/nifi-jetty-bundle/pom.xml | 7 +- .../nifi-jms-cf-service-nar/pom.xml | 6 +- .../nifi-jms-cf-service/pom.xml | 2 +- .../nifi-jms-processors-nar/pom.xml | 6 +- .../nifi-jms-processors/pom.xml | 10 +- .../cf/JndiJmsConnectionFactoryProvider.java | 80 +- .../jms/processors/AbstractJMSProcessor.java | 25 +- .../nifi/jms/processors/ConsumeJMS.java | 35 +- .../nifi/jms/processors/JMSPublisher.java | 27 +- .../nifi/jms/processors/PublishJMS.java | 19 +- nifi-nar-bundles/nifi-jms-bundle/pom.xml | 4 +- .../nifi-jolt-record-nar/pom.xml | 8 +- .../nifi-jolt-record-processors/pom.xml | 19 +- .../jolt/record/JoltTransformRecord.java | 73 +- .../nifi-jolt-record-bundle/pom.xml | 13 +- .../nifi-kafka-0-10-nar/pom.xml | 4 +- .../nifi-kafka-0-10-processors/pom.xml | 8 +- .../kafka/pubsub/ConsumeKafka_0_10.java | 3 +- .../nifi-kafka-0-11-nar/pom.xml | 4 +- .../nifi-kafka-0-11-processors/pom.xml | 8 +- .../kafka/pubsub/ConsumeKafka_0_11.java | 3 +- .../nifi-kafka-0-8-nar/pom.xml | 2 +- .../nifi-kafka-0-8-processors/pom.xml | 8 +- .../nifi-kafka-0-9-nar/pom.xml | 4 +- .../nifi-kafka-0-9-processors/pom.xml | 8 +- .../processors/kafka/pubsub/ConsumeKafka.java | 3 +- .../nifi-kafka-1-0-nar/pom.xml | 4 +- .../nifi-kafka-1-0-processors/pom.xml | 8 +- .../kafka/pubsub/ConsumeKafka_1_0.java | 3 +- .../nifi-kafka-2-0-nar/pom.xml | 4 +- .../nifi-kafka-2-0-processors/pom.xml | 8 +- .../kafka/pubsub/ConsumeKafka_2_0.java | 3 +- nifi-nar-bundles/nifi-kafka-bundle/pom.xml | 20 +- .../nifi-kerberos-iaa-providers-nar/pom.xml | 2 +- .../nifi-kerberos-iaa-providers/pom.xml | 12 +- .../pom.xml | 14 +- .../nifi-kite-bundle/nifi-kite-nar/pom.xml | 4 +- .../nifi-kite-processors/pom.xml | 8 +- .../kite/AbstractKiteProcessor.java | 25 +- .../kite/TestCSVToAvroProcessor.java | 18 + nifi-nar-bundles/nifi-kite-bundle/pom.xml | 10 +- .../nifi-kudu-bundle/nifi-kudu-nar/pom.xml | 8 +- .../nifi-kudu-processors/pom.xml | 14 +- nifi-nar-bundles/nifi-kudu-bundle/pom.xml | 4 +- .../nifi-language-translation-nar/pom.xml | 4 +- .../nifi-yandex-processors/pom.xml | 8 +- .../nifi-language-translation-bundle/pom.xml | 2 +- .../nifi-ldap-iaa-providers-nar/pom.xml | 2 +- .../nifi-ldap-iaa-providers/pom.xml | 14 +- .../ldap/tenants/LdapUserGroupProvider.java | 21 +- .../nifi-ldap-iaa-providers-bundle/pom.xml | 14 +- .../nifi-lumberjack-nar/pom.xml | 8 +- .../nifi-lumberjack-processors/pom.xml | 12 +- .../nifi-lumberjack-bundle/pom.xml | 4 +- .../nifi-image-viewer/pom.xml | 2 +- .../nifi-media-bundle/nifi-media-nar/pom.xml | 8 +- .../nifi-media-processors/pom.xml | 8 +- nifi-nar-bundles/nifi-media-bundle/pom.xml | 4 +- .../pom.xml | 6 +- .../nifi-metrics-reporter-service-api/pom.xml | 2 +- .../nifi-metrics-reporting-nar/pom.xml | 6 +- .../nifi-metrics-reporting-task/pom.xml | 8 +- .../nifi-metrics-reporting-bundle/pom.xml | 4 +- .../pom.xml | 6 +- .../nifi-mongodb-client-service-api/pom.xml | 6 +- .../nifi-mongodb-nar/pom.xml | 4 +- .../nifi-mongodb-processors/pom.xml | 17 +- .../mongodb/AbstractMongoProcessor.java | 3 +- .../mongodb/AbstractMongoQueryProcessor.java | 150 + .../nifi/processors/mongodb/GetMongo.java | 113 +- .../processors/mongodb/GetMongoRecord.java | 205 ++ .../org.apache.nifi.processor.Processor | 1 + .../additionalDetails.html | 59 + .../mongodb/GetMongoRecordIT.groovy | 179 + .../mongodb/AbstractMongoProcessorTest.java | 20 +- .../nifi-mongodb-services-nar/pom.xml | 6 +- .../nifi-mongodb-services/pom.xml | 12 +- .../mongodb/MongoDBControllerService.java | 3 +- nifi-nar-bundles/nifi-mongodb-bundle/pom.xml | 4 +- .../nifi-mqtt-bundle/nifi-mqtt-nar/pom.xml | 4 +- .../nifi-mqtt-processors/pom.xml | 6 +- .../nifi/processors/mqtt/ConsumeMQTT.java | 142 +- .../nifi/processors/mqtt/PublishMQTT.java | 126 +- .../mqtt/common/AbstractMQTTProcessor.java | 92 +- .../nifi/processors/mqtt/TestConsumeMQTT.java | 2 +- .../nifi/processors/mqtt/TestPublishMQTT.java | 2 +- .../mqtt/common/TestConsumeMqttCommon.java | 25 +- .../mqtt/integration/TestConsumeMQTT.java | 2 +- .../mqtt/integration/TestConsumeMqttSSL.java | 2 +- ...estPublishAndSubscribeMqttIntegration.java | 2 +- nifi-nar-bundles/nifi-mqtt-bundle/pom.xml | 4 +- .../nifi-network-processors-nar/pom.xml | 7 +- .../nifi-network-processors/pom.xml | 11 +- .../nifi-network-utils/pom.xml | 5 +- nifi-nar-bundles/nifi-network-bundle/pom.xml | 5 +- .../nifi-parquet-nar/pom.xml | 8 +- .../nifi-parquet-processors/pom.xml | 12 +- .../parquet/ConvertAvroToParquet.java | 204 ++ .../nifi/processors/parquet/FetchParquet.java | 1 - .../nifi/processors/parquet/PutParquet.java | 159 +- .../parquet/stream/NifiOutputStream.java | 66 + .../parquet/stream/NifiParquetOutputFile.java | 54 + .../parquet/utils/ParquetUtils.java | 202 ++ .../org.apache.nifi.processor.Processor | 3 +- .../processors/parquet/PutParquetTest.java | 19 +- .../parquet/TestConvertAvroToParquet.java | 263 ++ .../test/resources/avro/all-minus-enum.avsc | 60 + nifi-nar-bundles/nifi-parquet-bundle/pom.xml | 15 +- .../nifi-poi-bundle/nifi-poi-nar/pom.xml | 6 +- .../nifi-poi-processors/pom.xml | 8 +- .../poi/ConvertExcelToCSVProcessorTest.java | 50 +- nifi-nar-bundles/nifi-poi-bundle/pom.xml | 4 +- .../pom.xml | 10 +- .../nifi-provenance-repository-nar/pom.xml | 2 +- .../pom.xml | 6 +- .../nifi-provenance-repository-bundle/pom.xml | 6 +- .../nifi-ranger-nar/pom.xml | 6 +- .../nifi-ranger-plugin/pom.xml | 4 +- .../nifi-ranger-resources/pom.xml | 2 +- nifi-nar-bundles/nifi-ranger-bundle/pom.xml | 4 +- .../nifi-redis-extensions/pom.xml | 8 +- ...RedisDistributedMapCacheClientService.java | 3 +- .../apache/nifi/redis/util/RedisUtils.java | 60 +- .../nifi-redis-bundle/nifi-redis-nar/pom.xml | 8 +- .../nifi-redis-service-api-nar/pom.xml | 8 +- .../src/main/resources/META-INF/NOTICE | 14 +- .../nifi-redis-service-api/pom.xml | 2 +- nifi-nar-bundles/nifi-redis-bundle/pom.xml | 6 +- .../nifi-registry-nar/pom.xml | 6 +- .../nifi-registry-service/pom.xml | 6 +- nifi-nar-bundles/nifi-registry-bundle/pom.xml | 2 +- .../nifi-rethinkdb-nar/pom.xml | 4 +- .../nifi-rethinkdb-processors/pom.xml | 8 +- .../nifi-rethinkdb-bundle/pom.xml | 4 +- .../nifi-riemann-nar/pom.xml | 2 +- .../nifi-riemann-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-riemann-bundle/pom.xml | 10 +- .../nifi-scripting-nar/pom.xml | 4 +- .../nifi-scripting-processors/pom.xml | 8 +- .../nifi/script/ScriptingComponentHelper.java | 4 +- .../impl/JythonScriptEngineConfigurator.java | 16 +- .../nifi-scripting-bundle/pom.xml | 14 +- .../nifi-site-to-site-reporting-nar/pom.xml | 4 +- .../nifi-site-to-site-reporting-task/pom.xml | 20 +- .../AbstractSiteToSiteReportingTask.java | 5 +- .../additionalDetails.html | 32 +- .../src/main/resources/schema-provenance.avsc | 32 +- .../pom.xml | 4 +- .../nifi-slack-bundle/nifi-slack-nar/pom.xml | 6 +- .../nifi-slack-processors/pom.xml | 8 +- nifi-nar-bundles/nifi-slack-bundle/pom.xml | 4 +- .../nifi-snmp-bundle/nifi-snmp-nar/pom.xml | 2 +- .../nifi-snmp-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-snmp-bundle/pom.xml | 4 +- .../nifi-social-media-nar/pom.xml | 4 +- .../nifi-twitter-processors/pom.xml | 6 +- .../nifi/processors/twitter/GetTwitter.java | 37 +- .../nifi-social-media-bundle/pom.xml | 2 +- .../nifi-solr-bundle/nifi-solr-nar/pom.xml | 6 +- .../nifi-solr-processors/pom.xml | 12 +- .../nifi/processors/solr/SolrProcessor.java | 32 +- .../solr/TestPutSolrContentStream.java | 45 +- nifi-nar-bundles/nifi-solr-bundle/pom.xml | 2 +- .../pom.xml | 6 +- .../nifi-livy-controller-service-api/pom.xml | 2 +- .../nifi-livy-controller-service/pom.xml | 10 +- .../nifi-spark-bundle/nifi-livy-nar/pom.xml | 8 +- .../nifi-livy-processors/pom.xml | 18 +- nifi-nar-bundles/nifi-spark-bundle/pom.xml | 13 +- .../nifi-splunk-nar/pom.xml | 8 +- .../nifi-splunk-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-splunk-bundle/pom.xml | 4 +- .../nifi-spring-nar/pom.xml | 2 +- .../nifi-spring-processors/pom.xml | 12 +- .../additionalDetails.html | 14 +- .../src/test/resources/aggregated.xml | 2 +- .../src/test/resources/fromSpringOnly.xml | 4 +- .../src/test/resources/requestReply.xml | 2 +- .../src/test/resources/toSpringOnly.xml | 2 +- nifi-nar-bundles/nifi-spring-bundle/pom.xml | 4 +- .../nifi-jolt-transform-json-ui/pom.xml | 16 +- .../nifi-standard-content-viewer/pom.xml | 4 +- .../nifi-standard-nar/pom.xml | 4 +- .../nifi-standard-processors/pom.xml | 44 +- .../AbstractDatabaseFetchProcessor.java | 4 + .../standard/AbstractExecuteSQL.java | 81 +- .../standard/AbstractQueryDatabaseTable.java | 42 +- .../processors/standard/ConvertJSONToSQL.java | 61 +- .../nifi/processors/standard/ExecuteSQL.java | 16 +- .../processors/standard/ExecuteSQLRecord.java | 4 + .../nifi/processors/standard/FetchFile.java | 12 + .../nifi/processors/standard/FlattenJson.java | 2 +- .../processors/standard/GenerateFlowFile.java | 2 +- .../standard/GenerateTableFetch.java | 125 +- .../nifi/processors/standard/GetFTP.java | 1 + .../nifi/processors/standard/GetSFTP.java | 1 + .../standard/HandleHttpRequest.java | 401 ++- .../nifi/processors/standard/InvokeHTTP.java | 10 +- .../standard/JoltTransformJSON.java | 90 +- .../nifi/processors/standard/ListFTP.java | 1 + .../nifi/processors/standard/ListFile.java | 974 +++++- .../nifi/processors/standard/ListSFTP.java | 1 + .../nifi/processors/standard/ListenHTTP.java | 29 +- .../processors/standard/LogAttribute.java | 2 + .../nifi/processors/standard/LogMessage.java | 20 +- .../standard/PutDatabaseRecord.java | 75 +- .../nifi/processors/standard/PutSQL.java | 56 +- .../standard/QueryDatabaseTable.java | 3 + .../standard/QueryDatabaseTableRecord.java | 2 + .../nifi/processors/standard/ReplaceText.java | 81 +- .../nifi/processors/standard/RouteText.java | 49 +- .../nifi/processors/standard/SplitRecord.java | 25 +- .../nifi/processors/standard/SplitText.java | 87 +- .../processors/standard/TransformXml.java | 132 +- .../nifi/processors/standard/ValidateCsv.java | 52 +- .../processors/standard/ValidateRecord.java | 18 +- .../apache/nifi/processors/standard/Wait.java | 23 +- .../standard/servlets/ListenHTTPServlet.java | 353 +- .../processors/standard/util/FTPTransfer.java | 7 +- .../standard/util/FileTransfer.java | 8 + .../processors/standard/util/JdbcCommon.java | 7 + .../standard/util/NLKBufferedReader.java | 181 - .../standard/util/SFTPTransfer.java | 10 +- .../nifi/queryrecord/FlowFileEnumerator.java | 38 +- .../nifi/queryrecord/FlowFileTable.java | 3 +- .../additionalDetails.html | 13 + .../standard/TestFlattenJson.groovy | 12 + .../standard/TestPutDatabaseRecord.groovy | 103 + ...quest.java => ITestHandleHttpRequest.java} | 268 +- .../standard/TestConvertRecord.java | 61 +- .../processors/standard/TestExecuteSQL.java | 153 +- .../standard/TestExecuteSQLRecord.java | 175 + .../processors/standard/TestFetchFile.java | 77 + .../standard/TestGenerateTableFetch.java | 64 +- .../processors/standard/TestListFile.java | 154 +- .../processors/standard/TestListenHTTP.java | 174 +- .../processors/standard/TestQueryRecord.java | 100 +- .../processors/standard/TestReplaceText.java | 25 + .../processors/standard/TestSplitRecord.java | 15 +- .../processors/standard/TestTransformXml.java | 133 +- .../processors/standard/TestValidateCsv.java | 50 +- .../standard/TestValidateRecord.java | 223 ++ .../nifi/processors/standard/TestWait.java | 39 +- .../TestConvertRecord/input/person.json | 7 + .../TestConvertRecord/schema/person.avsc | 17 + .../nifi-standard-reporting-tasks/pom.xml | 8 +- .../nifi-standard-utils/pom.xml | 4 +- .../nifi-standard-web-test-utils/pom.xml | 4 +- nifi-nar-bundles/nifi-standard-bundle/pom.xml | 17 +- .../nifi-dbcp-service-api/pom.xml | 2 +- .../nifi-dbcp-service-nar/pom.xml | 6 +- .../nifi-dbcp-service/pom.xml | 10 +- .../apache/nifi/dbcp/DBCPConnectionPool.java | 128 +- .../org/apache/nifi/dbcp/DBCPServiceTest.java | 147 + .../nifi-dbcp-service-bundle/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 8 +- .../nifi-distributed-cache-protocol/pom.xml | 4 +- .../nifi-distributed-cache-server/pom.xml | 12 +- .../pom.xml | 4 +- .../pom.xml | 2 +- .../nifi-hbase-client-service-api/pom.xml | 7 +- .../apache/nifi/hbase/HBaseClientService.java | 50 - .../pom.xml | 8 +- .../nifi-hbase_1_1_2-client-service/pom.xml | 12 +- .../nifi/hbase/HBase_1_1_2_ClientService.java | 78 +- .../hbase/validate/ConfigFilesValidator.java | 0 .../pom.xml | 10 +- .../nifi-http-context-map-api/pom.xml | 2 +- .../nifi-http-context-map-nar/pom.xml | 4 +- .../nifi-http-context-map/pom.xml | 4 +- .../nifi-http-context-map-bundle/pom.xml | 2 +- .../nifi-hwx-schema-registry-nar/pom.xml | 6 +- .../nifi-hwx-schema-registry-service/pom.xml | 8 +- .../nifi-hwx-schema-registry-bundle/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 6 +- .../nifi-kerberos-credentials-service/pom.xml | 4 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../nifi-lookup-service-api/pom.xml | 2 +- .../nifi-lookup-services-nar/pom.xml | 6 +- .../nifi-lookup-services/pom.xml | 18 +- .../nifi-lookup-services-bundle/pom.xml | 2 +- .../nifi-proxy-configuration-api/pom.xml | 4 +- .../nifi-proxy-configuration-nar/pom.xml | 6 +- .../nifi-proxy-configuration/pom.xml | 4 +- .../nifi-proxy-configuration-bundle/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 6 +- .../pom.xml | 15 +- .../java/org/apache/nifi/avro/AvroReader.java | 62 +- .../apache/nifi/avro/AvroRecordSetWriter.java | 112 +- .../WriteAvroResultWithExternalSchema.java | 36 +- .../nifi/csv/AbstractCSVRecordReader.java | 9 +- .../apache/nifi/json/JsonRecordSetWriter.java | 117 +- .../org/apache/nifi/json/WriteJsonResult.java | 34 +- .../org/apache/nifi/xml/WriteXMLResult.java | 2 +- .../apache/nifi/avro/TestWriteAvroResult.java | 40 +- .../TestWriteAvroResultWithoutSchema.java | 61 +- .../apache/nifi/csv/TestCSVRecordReader.java | 9 +- .../apache/nifi/json/TestWriteJsonResult.java | 59 +- .../nifi/xml/TestXMLRecordSetWriter.java | 49 + .../pom.xml | 2 +- .../nifi-schema-registry-service-api/pom.xml | 2 +- .../nifi-ssl-context-nar/pom.xml | 4 +- .../nifi-ssl-context-service/pom.xml | 6 +- .../nifi/ssl/StandardSSLContextService.java | 19 +- .../nifi-ssl-context-bundle/pom.xml | 2 +- .../nifi-ssl-context-service-api/pom.xml | 2 +- .../nifi-standard-services-api-nar/pom.xml | 12 +- .../nifi-standard-services/pom.xml | 2 +- .../nifi-stateful-analysis-nar/pom.xml | 4 +- .../nifi-stateful-analysis-processors/pom.xml | 8 +- .../nifi-stateful-analysis-bundle/pom.xml | 4 +- .../nifi-tcp-bundle/nifi-tcp-nar/pom.xml | 6 +- .../nifi-tcp-processors/pom.xml | 6 +- nifi-nar-bundles/nifi-tcp-bundle/pom.xml | 2 +- .../nifi-update-attribute-model/pom.xml | 4 +- .../nifi-update-attribute-nar/pom.xml | 4 +- .../nifi-update-attribute-processor/pom.xml | 11 +- .../attributes/UpdateAttribute.java | 148 +- .../attributes/TestUpdateAttribute.java | 29 - .../nifi-update-attribute-ui/pom.xml | 12 +- .../nifi-update-attribute-bundle/pom.xml | 8 +- .../nifi-websocket-processors-nar/pom.xml | 8 +- .../nifi-websocket-processors/pom.xml | 8 +- .../nifi-websocket-services-api-nar/pom.xml | 8 +- .../nifi-websocket-services-api/pom.xml | 6 +- .../nifi-websocket-services-jetty-nar/pom.xml | 8 +- .../nifi-websocket-services-jetty/pom.xml | 6 +- .../nifi-websocket-bundle/pom.xml | 4 +- .../nifi-windows-event-log-nar/pom.xml | 4 +- .../nifi-windows-event-log-processors/pom.xml | 10 +- .../event/log/ConsumeWindowsEventLog.java | 95 +- .../nifi-windows-event-log-bundle/pom.xml | 4 +- nifi-nar-bundles/pom.xml | 51 +- nifi-toolkit/nifi-toolkit-admin/pom.xml | 14 +- .../notify/conf/nifi-secured.properties | 1 - .../resources/notify/conf/nifi.properties | 1 - .../notify/conf_secure/nifi.properties | 1 - nifi-toolkit/nifi-toolkit-assembly/pom.xml | 16 +- nifi-toolkit/nifi-toolkit-cli/pom.xml | 4 +- .../impl/client/nifi/ControllerClient.java | 14 + .../nifi/impl/JerseyControllerClient.java | 87 + .../cli/impl/command/CommandOption.java | 3 + .../impl/command/nifi/NiFiCommandGroup.java | 12 + .../impl/command/nifi/nodes/ConnectNode.java | 67 + .../impl/command/nifi/nodes/DeleteNode.java | 58 + .../command/nifi/nodes/DisconnectNode.java | 67 + .../cli/impl/command/nifi/nodes/GetNode.java | 59 + .../cli/impl/command/nifi/nodes/GetNodes.java | 52 + .../impl/command/nifi/nodes/OffloadNode.java | 67 + .../toolkit/cli/impl/result/NodeResult.java | 48 + .../toolkit/cli/impl/result/NodesResult.java | 66 + ...stVersionedFlowSnapshotMetadataResult.java | 10 +- .../nifi-toolkit-encrypt-config/pom.xml | 10 +- .../test/resources/nifi_default.properties | 1 - ...sitive_properties_protected_aes.properties | 1 - ...ve_properties_protected_aes_128.properties | 1 - ...operties_protected_aes_password.properties | 1 - ...ties_protected_aes_password_128.properties | 1 - ...ensitive_properties_unprotected.properties | 1 - ...ed_and_empty_protection_schemes.properties | 1 - .../nifi-toolkit-flowanalyzer/pom.xml | 2 +- .../nifi-toolkit-flowfile-repo/pom.xml | 4 +- nifi-toolkit/nifi-toolkit-s2s/pom.xml | 4 +- .../nifi/toolkit/s2s/SiteToSiteCliMain.java | 2 - nifi-toolkit/nifi-toolkit-tls/pom.xml | 8 +- .../test/resources/localhost/nifi.properties | 1 - .../nifi-toolkit-zookeeper-migrator/pom.xml | 2 +- nifi-toolkit/pom.xml | 9 +- pom.xml | 88 +- 1054 files changed, 31299 insertions(+), 12229 deletions(-) create mode 100644 SECURITY.md create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/ProvidedServiceAPI.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/StandardProvidedServiceAPI.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java create mode 100644 nifi-api/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java create mode 100644 nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/RecordIterator.java create mode 100644 nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SingleRecordIterator.java rename nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/{StandardKeytabUser.java => AbstractKerberosUser.java} (86%) create mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/ConfigurationUtil.java rename nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/{KeytabAction.java => KerberosAction.java} (79%) create mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosKeytabUser.java create mode 100644 nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosPasswordUser.java rename nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/{KeytabUser.java => KerberosUser.java} (95%) rename nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/{KeytabUserIT.java => KerberosUserIT.java} (65%) create mode 100644 nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/RepeatingInputStream.java create mode 100644 nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/AbstractTextDemarcator.java create mode 100644 nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/LineDemarcator.java delete mode 100644 nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/processor/TestFormatUtilsGroovy.groovy create mode 100644 nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/util/TestFormatUtilsGroovy.groovy create mode 100644 nifi-commons/nifi-utils/src/test/java/org/apache/nifi/stream/io/util/TestLineDemarcator.java create mode 100644 nifi-docs/src/main/asciidoc/images/cluster_connection_summary.png create mode 100644 nifi-docs/src/main/asciidoc/images/disconnected-node-cluster-mgt.png create mode 100644 nifi-docs/src/main/asciidoc/images/iconConnect.png create mode 100644 nifi-docs/src/main/asciidoc/images/iconDisconnect.png create mode 100644 nifi-docs/src/main/asciidoc/images/iconDownloadTemplate.png create mode 100644 nifi-docs/src/main/asciidoc/images/iconLoadBalance.png create mode 100644 nifi-docs/src/main/asciidoc/images/iconOffload.png create mode 100644 nifi-docs/src/main/asciidoc/images/load_balance_active_connection.png create mode 100644 nifi-docs/src/main/asciidoc/images/load_balance_compression_options.png create mode 100644 nifi-docs/src/main/asciidoc/images/load_balance_configured_connection.png create mode 100644 nifi-docs/src/main/asciidoc/images/load_balance_distributed_connection.png create mode 100644 nifi-docs/src/main/asciidoc/images/offloaded-node-cluster-mgt.png create mode 100644 nifi-docs/src/main/asciidoc/images/offloading-node-cluster-mgt.png create mode 100644 nifi-docs/src/main/asciidoc/images/primary-node-cluster-mgt.png create mode 100644 nifi-docs/src/main/asciidoc/images/summary_connections.png create mode 100644 nifi-docs/src/main/asciidoc/toolkit-guide.adoc delete mode 100644 nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/src/main/java/org/apache/nifi/processors/aws/regions/AWSRegions.java create mode 100644 nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/TagS3Object.java create mode 100644 nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/ITTagS3Object.java create mode 100644 nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestTagS3Object.java create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/pom.xml rename nifi-nar-bundles/{nifi-redis-bundle/nifi-redis-service-api-nar => nifi-cassandra-bundle/nifi-cassandra-services-api-nar}/src/main/resources/META-INF/LICENSE (78%) create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/pom.xml create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/src/main/java/org/apache/nifi/cassandra/CassandraSessionProviderService.java create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/LICENSE create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/pom.xml create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/java/org/apache/nifi/service/CassandraSessionProvider.java create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/MockCassandraProcessor.java create mode 100644 nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/TestCassandraSessionProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/OffloadCode.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/OffloadMessage.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/IllegalNodeOffloadException.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/OffloadedNodeMutableRequestException.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinatorSpec.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/integration/OffloadNodeITSpec.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/reporting/UserAwareEventAccess.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/registry/flow/TestFlowRegistryClient.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowSnippet.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSnippet.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/kerberos/KerberosConfig.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/NonLocalPartitionPartitioner.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/StandardQueueProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/GhostControllerService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/AbstractMetricDescriptor.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/CounterMetricDescriptor.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/provenance/ComponentIdentifierLookup.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/provenance/StandardProvenanceAuthorizableFactory.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/reporting/StandardEventAccess.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/ExtensionManagerFactoryBean.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowServiceSpec.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/queue/clustered/partition/NonLocalPartitionPartitionerSpec.groovy rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/{TestProcessorLifecycle.java => ProcessorLifecycleIT.java} (74%) delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/StandardProcessSchedulerIT.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/pom.xml create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/main/java/org/apache/nifi/nar/ExtensionUiLoader.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/main/java/org/apache/nifi/nar/NarAutoLoader.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/main/java/org/apache/nifi/nar/NarAutoLoaderTask.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/main/java/org/apache/nifi/nar/NarLoader.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/main/java/org/apache/nifi/nar/StandardNarLoader.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/java/org/apache/nifi/nar/TestNarLoader.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/README create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/conf/nifi.properties create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/extensions/nifi-example-processors-nar-1.0.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/extensions/nifi-example-service-api-nar-1.0.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/extensions/nifi-example-service-nar-1.1.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/lib/nifi-framework-nar.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/lib/nifi-jetty-bundle.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-loading-utils/src/test/resources/nifi-example-nars-source.zip create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionDiscoveringManager.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionManagerHolder.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/StandardExtensionDiscoveringManager.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarClassLoadersHolder.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarLoadResult.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-repository-models/src/test/java/org/apache/nifi/controller/repository/TestStandardRepositoryRecord.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/IllegalNodeOffloadExceptionMapper.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/lib/nifi-framework-nar.nar create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/lib/nifi-jetty-bundle.nar create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/bigquery/AbstractBigQueryProcessor.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/bigquery/BigQueryAttributes.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/bigquery/BigQueryUtils.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/main/java/org/apache/nifi/processors/gcp/bigquery/PutBigQueryBatch.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/bigquery/AbstractBQTest.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/bigquery/AbstractBigQueryIT.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/bigquery/PutBigQueryBatchIT.java create mode 100644 nifi-nar-bundles/nifi-gcp-bundle/nifi-gcp-processors/src/test/java/org/apache/nifi/processors/gcp/bigquery/PutBigQueryBatchTest.java create mode 100644 nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/src/test/resources/nested_record.avsc create mode 100644 nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/main/java/org/apache/nifi/processors/mongodb/AbstractMongoQueryProcessor.java create mode 100644 nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/main/java/org/apache/nifi/processors/mongodb/GetMongoRecord.java create mode 100644 nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/main/resources/docs/org.apache.nifi.processors.mongodb.GetMongoRecord/additionalDetails.html create mode 100644 nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/groovy/org/apache/nifi/processors/mongodb/GetMongoRecordIT.groovy create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/main/java/org/apache/nifi/processors/parquet/ConvertAvroToParquet.java create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/main/java/org/apache/nifi/processors/parquet/stream/NifiOutputStream.java create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/main/java/org/apache/nifi/processors/parquet/stream/NifiParquetOutputFile.java create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/main/java/org/apache/nifi/processors/parquet/utils/ParquetUtils.java create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/test/java/org/apache/nifi/processors/parquet/TestConvertAvroToParquet.java create mode 100644 nifi-nar-bundles/nifi-parquet-bundle/nifi-parquet-processors/src/test/resources/avro/all-minus-enum.avsc delete mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/NLKBufferedReader.java rename nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/{TestHandleHttpRequest.java => ITestHandleHttpRequest.java} (56%) create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestConvertRecord/input/person.json create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestConvertRecord/schema/person.avsc rename nifi-nar-bundles/nifi-standard-services/{nifi-hbase-client-service-api => nifi-hbase_1_1_2-client-service-bundle/nifi-hbase_1_1_2-client-service}/src/main/java/org/apache/nifi/hbase/validate/ConfigFilesValidator.java (100%) create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/ConnectNode.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/DeleteNode.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/DisconnectNode.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/GetNode.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/GetNodes.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/nodes/OffloadNode.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/result/NodeResult.java create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/result/NodesResult.java diff --git a/NOTICE b/NOTICE index f5f1a7f16142..7cc5b4881912 100644 --- a/NOTICE +++ b/NOTICE @@ -31,6 +31,9 @@ This includes derived works from the Apache Hive (ASLv2 licensed) project (https The derived work is adapted from release-1.2.1/ql/src/java/org/apache/hadoop/hive/ql/io/orc/WriterImpl.java and can be found in the org.apache.hadoop.hive.ql.io.orc package + The derived work is adapted from + release-3.0.0/serde/src/java/org/apache/hadoop/hive/serde2/JsonSerDe.java + and can be found in the org.apache.hive.streaming.NiFiRecordSerDe class This includes derived works from the Apache Software License V2 library Jolt (https://github.com/bazaarvoice/jolt) Copyright 2013-2014 Bazaarvoice, Inc diff --git a/README.md b/README.md index 9eaa1c663fc9..dc2956740faf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![Build Status](https://travis-ci.org/apache/nifi.svg?branch=master)](https://travis-ci.org/apache/nifi) [![Docker pulls](https://img.shields.io/docker/pulls/apache/nifi.svg)](https://hub.docker.com/r/apache/nifi/) [![Version](https://img.shields.io/maven-central/v/org.apache.nifi/nifi-utils.svg)](https://nifi.apache.org/download.html) -[![HipChat](https://img.shields.io/badge/chat-on%20HipChat-brightgreen.svg)](https://www.hipchat.com/gzh2m5YML) +[![Slack](https://img.shields.io/badge/chat-on%20Slack-brightgreen.svg)](https://join.slack.com/t/apachenifi/shared_invite/enQtNDI2NDMyMTY3MTA5LWJmZDI3MmM1ZmYyODQwZDYwM2MyMDY5ZjkyMDkxY2JmOGMyNmEzYTE0MTRkZTYwYzZlYTJkY2JhZTYyMzcyZGI) [Apache NiFi](https://nifi.apache.org/) is an easy to use, powerful, and reliable system to process and distribute data. @@ -171,12 +171,11 @@ source code. The following provides more details on the included cryptographic software: -Apache NiFi uses BouncyCastle, Jasypt, JCraft Inc., and the built-in -java cryptography libraries for SSL, SSH, and the protection +Apache NiFi uses BouncyCastle, JCraft Inc., and the built-in +Java cryptography libraries for SSL, SSH, and the protection of sensitive configuration parameters. See http://bouncycastle.org/about.html -http://www.jasypt.org/faq.html -http://jcraft.com/c-info.html +http://www.jcraft.com/c-info.html http://www.oracle.com/us/products/export/export-regulations-345813.html for more details on each of these libraries cryptography features. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..e52e6174aa83 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,54 @@ + +# Security Vulnerability Disclosure + +Apache NiFi welcomes the responsible reporting of security vulnerabilities. The NiFi team believes that working with skilled security researchers across the globe is crucial in identifying weaknesses in any technology. If you believe you've found a security issue in our product or service, we encourage you to notify us. We will work with you to resolve the issue promptly. + +## Table of Contents + +* [Disclosure Policy](#disclosure-policy) +* [Exclusions](#exclusions) +* [Reporting Methods](#reporting-methods) +* [Publishing](#publishing) + +## Disclosure Policy + +* Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue. +* Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. +* Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder. + +## Exclusions + +While researching, we'd like to ask you to refrain from: +* Denial of service +* Spamming +* Social engineering (including phishing) of Apache NiFi staff or contractors +* Any physical attempts against Apache NiFi property or data centers + +## Reporting Methods + +NiFi accepts reports in multiple ways: + +* Send an email to [security@nifi.apache.org](mailto:security@nifi.apache.org). This is a private list monitored by the [PMC](https://nifi.apache.org/people.html). For sensitive disclosures, the GPG key fingerprint is *1230 3BB8 1F22 E11C 8725 926A AFF2 B368 23B9 44E9*. +* NiFi has a [HackerOne](https://hackerone.com/apache_nifi) project page. HackerOne provides a triaged process for researchers and organizations to collaboratively report and resolve security vulnerabilities. + +## Publishing + +Apache NiFi follows the [ASF Project Security For Committers](https://www.apache.org/security/committers.html) policy. Thus, any vulnerability is not disclosed by the project until a released version which mitigates the issue is available for download. Once this release is available, the issue is fully documented in the following locations: +* An email is sent to the same addresses as the release announcement (dev@nifi.apache.org, users@nifi.apache.org, announce@apache.org), security@nifi.apache.org, and oss-security@lists.openwall.com +* The [Mitre Common Vulnerabilities and Exposures (CVE) database](https://cveform.mitre.org/) +* The [Apache NiFi Security Page](https://nifi.apache.org/security.html) + +Thank you for helping keep Apache NiFi and our users safe! \ No newline at end of file diff --git a/nifi-api/pom.xml b/nifi-api/pom.xml index 3654aef01f28..cad85237adc6 100644 --- a/nifi-api/pom.xml +++ b/nifi-api/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-api jar diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java b/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java new file mode 100644 index 000000000000..c6e793b69cc7 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java @@ -0,0 +1,285 @@ +/* + * 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.documentation; + +import org.apache.nifi.annotation.behavior.DynamicProperties; +import org.apache.nifi.annotation.behavior.DynamicProperty; +import org.apache.nifi.annotation.behavior.DynamicRelationship; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.ReadsAttribute; +import org.apache.nifi.annotation.behavior.ReadsAttributes; +import org.apache.nifi.annotation.behavior.Restricted; +import org.apache.nifi.annotation.behavior.Stateful; +import org.apache.nifi.annotation.behavior.SystemResourceConsideration; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.DeprecationNotice; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.ConfigurableComponent; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.documentation.init.DocumentationControllerServiceInitializationContext; +import org.apache.nifi.documentation.init.DocumentationProcessorInitializationContext; +import org.apache.nifi.documentation.init.DocumentationReportingInitializationContext; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.reporting.ReportingTask; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Base class for DocumentationWriter that simplifies iterating over all information for a component, creating a separate method + * for each, to ensure that implementations properly override all methods and therefore properly account for all information about + * a component. + * + * Please note that while this class lives within the nifi-api, it is provided primarily as a means for documentation components within + * the NiFi NAR Maven Plugin. Its home is the nifi-api, however, because the API is needed in order to extract the relevant information and + * the NAR Maven Plugin cannot have a direct dependency on nifi-api (doing so would cause a circular dependency). By having this homed within + * the nifi-api, the Maven plugin is able to discover the class dynamically and invoke the one or two methods necessary to create the documentation. + * + * This is a new capability in 1.9.0 in preparation for the Extension Registry and therefore, you should + * NOTE WELL: At this time, while this class is part of nifi-api, it is still evolving and may change in a non-backward-compatible manner or even be + * removed from one incremental release to the next. Use at your own risk! + */ +public abstract class AbstractDocumentationWriter implements ExtensionDocumentationWriter { + + @Override + public final void write(final ConfigurableComponent component) throws IOException { + write(component, null); + } + + @Override + public final void write(final ConfigurableComponent component, final Collection providedServices) throws IOException { + initialize(component); + + writeHeader(component); + writeBody(component); + + if (providedServices != null && component instanceof ControllerService) { + writeProvidedServices(providedServices); + } + + writeFooter(component); + } + + private void initialize(final ConfigurableComponent component) { + try { + if (component instanceof Processor) { + initialize((Processor) component); + } else if (component instanceof ControllerService) { + initialize((ControllerService) component); + } else if (component instanceof ReportingTask) { + initialize((ReportingTask) component); + } + } catch (final InitializationException ie) { + throw new RuntimeException("Failed to initialize " + component, ie); + } + } + + protected void initialize(final Processor processor) { + processor.initialize(new DocumentationProcessorInitializationContext()); + } + + protected void initialize(final ControllerService service) throws InitializationException { + service.initialize(new DocumentationControllerServiceInitializationContext()); + } + + protected void initialize(final ReportingTask reportingTask) throws InitializationException { + reportingTask.initialize(new DocumentationReportingInitializationContext()); + } + + protected void writeBody(final ConfigurableComponent component) throws IOException { + writeExtensionName(component.getClass().getName()); + writeExtensionType(getExtensionType(component)); + writeDeprecationNotice(component.getClass().getAnnotation(DeprecationNotice.class)); + writeDescription(getDescription(component)); + writeTags(getTags(component)); + writeProperties(component.getPropertyDescriptors()); + writeDynamicProperties(getDynamicProperties(component)); + + if (component instanceof Processor) { + final Processor processor = (Processor) component; + + writeRelationships(processor.getRelationships()); + writeDynamicRelationship(getDynamicRelationship(processor)); + writeReadsAttributes(getReadsAttributes(processor)); + writeWritesAttributes(getWritesAttributes(processor)); + } + + writeStatefulInfo(component.getClass().getAnnotation(Stateful.class)); + writeRestrictedInfo(component.getClass().getAnnotation(Restricted.class)); + writeInputRequirementInfo(getInputRequirement(component)); + writeSystemResourceConsiderationInfo(getSystemResourceConsiderations(component)); + writeSeeAlso(component.getClass().getAnnotation(SeeAlso.class)); + } + + + protected String getDescription(final ConfigurableComponent component) { + final CapabilityDescription capabilityDescription = component.getClass().getAnnotation(CapabilityDescription.class); + if (capabilityDescription == null) { + return null; + } + + return capabilityDescription.value(); + } + + protected List getTags(final ConfigurableComponent component) { + final Tags tags = component.getClass().getAnnotation(Tags.class); + if (tags == null) { + return Collections.emptyList(); + } + + final String[] tagValues = tags.value(); + return tagValues == null ? Collections.emptyList() : Arrays.asList(tagValues); + } + + protected List getDynamicProperties(ConfigurableComponent configurableComponent) { + final List dynamicProperties = new ArrayList<>(); + final DynamicProperties dynProps = configurableComponent.getClass().getAnnotation(DynamicProperties.class); + if (dynProps != null) { + Collections.addAll(dynamicProperties, dynProps.value()); + } + + final DynamicProperty dynProp = configurableComponent.getClass().getAnnotation(DynamicProperty.class); + if (dynProp != null) { + dynamicProperties.add(dynProp); + } + + return dynamicProperties; + } + + + private DynamicRelationship getDynamicRelationship(Processor processor) { + return processor.getClass().getAnnotation(DynamicRelationship.class); + } + + + private List getReadsAttributes(final Processor processor) { + final List attributes = new ArrayList<>(); + + final ReadsAttributes readsAttributes = processor.getClass().getAnnotation(ReadsAttributes.class); + if (readsAttributes != null) { + Collections.addAll(attributes, readsAttributes.value()); + } + + final ReadsAttribute readsAttribute = processor.getClass().getAnnotation(ReadsAttribute.class); + if (readsAttribute != null) { + attributes.add(readsAttribute); + } + + return attributes; + } + + + private List getWritesAttributes(Processor processor) { + List attributes = new ArrayList<>(); + + WritesAttributes writesAttributes = processor.getClass().getAnnotation(WritesAttributes.class); + if (writesAttributes != null) { + Collections.addAll(attributes, writesAttributes.value()); + } + + WritesAttribute writeAttribute = processor.getClass().getAnnotation(WritesAttribute.class); + if (writeAttribute != null) { + attributes.add(writeAttribute); + } + + return attributes; + } + + private InputRequirement.Requirement getInputRequirement(final ConfigurableComponent component) { + final InputRequirement annotation = component.getClass().getAnnotation(InputRequirement.class); + return annotation == null ? null : annotation.value(); + } + + private List getSystemResourceConsiderations(final ConfigurableComponent component) { + SystemResourceConsideration[] systemResourceConsiderations = component.getClass().getAnnotationsByType(SystemResourceConsideration.class); + if (systemResourceConsiderations == null) { + return Collections.emptyList(); + } + + return Arrays.asList(systemResourceConsiderations); + } + + protected ExtensionType getExtensionType(final ConfigurableComponent component) { + if (component instanceof Processor) { + return ExtensionType.PROCESSOR; + } + if (component instanceof ControllerService) { + return ExtensionType.CONTROLLER_SERVICE; + } + if (component instanceof ReportingTask) { + return ExtensionType.REPORTING_TASK; + } + throw new AssertionError("Encountered unknown Configurable Component Type for " + component); + } + + + protected abstract void writeHeader(ConfigurableComponent component) throws IOException; + + protected abstract void writeExtensionName(String extensionName) throws IOException; + + protected abstract void writeExtensionType(ExtensionType extensionType) throws IOException; + + protected abstract void writeDeprecationNotice(final DeprecationNotice deprecationNotice) throws IOException; + + + protected abstract void writeDescription(String description) throws IOException; + + protected abstract void writeTags(List tags) throws IOException; + + protected abstract void writeProperties(List properties) throws IOException; + + protected abstract void writeDynamicProperties(List dynamicProperties) throws IOException; + + protected abstract void writeStatefulInfo(Stateful stateful) throws IOException; + + protected abstract void writeRestrictedInfo(Restricted restricted) throws IOException; + + protected abstract void writeInputRequirementInfo(InputRequirement.Requirement requirement) throws IOException; + + protected abstract void writeSystemResourceConsiderationInfo(List considerations) throws IOException; + + protected abstract void writeSeeAlso(SeeAlso seeAlso) throws IOException; + + + + // Processor-specific methods + protected abstract void writeRelationships(Set relationships) throws IOException; + + protected abstract void writeDynamicRelationship(DynamicRelationship dynamicRelationship) throws IOException; + + protected abstract void writeReadsAttributes(List attributes) throws IOException; + + protected abstract void writeWritesAttributes(List attributes) throws IOException; + + + // ControllerService-specific methods + protected abstract void writeProvidedServices(Collection providedServices) throws IOException; + + + protected abstract void writeFooter(ConfigurableComponent component) throws IOException; +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java new file mode 100644 index 000000000000..c533e4e7d879 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java @@ -0,0 +1,42 @@ +/* + * 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.documentation; + +import org.apache.nifi.components.ConfigurableComponent; + +import java.io.IOException; +import java.util.Collection; + +/** + * Generates documentation for an instance of a ConfigurableComponent. + * + * Please note that while this class lives within the nifi-api, it is provided primarily as a means for documentation components within + * the NiFi NAR Maven Plugin. Its home is the nifi-api, however, because the API is needed in order to extract the relevant information and + * the NAR Maven Plugin cannot have a direct dependency on nifi-api (doing so would cause a circular dependency). By having this homed within + * the nifi-api, the Maven plugin is able to discover the class dynamically and invoke the one or two methods necessary to create the documentation. + * + * This is a new capability in 1.9.0 in preparation for the Extension Registry and therefore, you should + * NOTE WELL: At this time, while this class is part of nifi-api, it is still evolving and may change in a non-backward-compatible manner or even be + * removed from one incremental release to the next. Use at your own risk! + */ +public interface ExtensionDocumentationWriter { + + void write(ConfigurableComponent component) throws IOException; + + void write(ConfigurableComponent component, Collection provideServices) throws IOException; + +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java new file mode 100644 index 000000000000..f9869398d278 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/ExtensionType.java @@ -0,0 +1,25 @@ +/* + * 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.documentation; + +public enum ExtensionType { + PROCESSOR, + + CONTROLLER_SERVICE, + + REPORTING_TASK; +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/ProvidedServiceAPI.java b/nifi-api/src/main/java/org/apache/nifi/documentation/ProvidedServiceAPI.java new file mode 100644 index 000000000000..04cd425df555 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/ProvidedServiceAPI.java @@ -0,0 +1,51 @@ +/* + * 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.documentation; + +/** + * Describes a Controller Service API that is provided by some implementation. + * + * Please note that while this class lives within the nifi-api, it is provided primarily as a means for documentation components within + * the NiFi NAR Maven Plugin. Its home is the nifi-api, however, because the API is needed in order to extract the relevant information and + * the NAR Maven Plugin cannot have a direct dependency on nifi-api (doing so would cause a circular dependency). By having this homed within + * the nifi-api, the Maven plugin is able to discover the class dynamically and invoke the one or two methods necessary to create the documentation. + * + * This is a new capability in 1.9.0 in preparation for the Extension Registry and therefore, you should + * NOTE WELL: At this time, while this class is part of nifi-api, it is still evolving and may change in a non-backward-compatible manner or even be + * removed from one incremental release to the next. Use at your own risk! + */ +public interface ProvidedServiceAPI { + /** + * @return the fully qualified class name of the interface implemented by the Controller Service + */ + String getClassName(); + + /** + * @return the Group ID of the bundle that provides the interface + */ + String getGroupId(); + + /** + * @return the Artifact ID of the bundle that provides the interface + */ + String getArtifactId(); + + /** + * @return the Version of the bundle that provides the interface + */ + String getVersion(); +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/StandardProvidedServiceAPI.java b/nifi-api/src/main/java/org/apache/nifi/documentation/StandardProvidedServiceAPI.java new file mode 100644 index 000000000000..b86f4cad6e4f --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/StandardProvidedServiceAPI.java @@ -0,0 +1,51 @@ +/* + * 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.documentation; + +public class StandardProvidedServiceAPI implements ProvidedServiceAPI { + private final String className; + private final String groupId; + private final String artifactId; + private final String version; + + public StandardProvidedServiceAPI(final String className, final String groupId, final String artifactId, final String version) { + this.className = className; + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + @Override + public String getClassName() { + return className; + } + + @Override + public String getGroupId() { + return groupId; + } + + @Override + public String getArtifactId() { + return artifactId; + } + + @Override + public String getVersion() { + return version; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java new file mode 100644 index 000000000000..cdf1e8dc691f --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.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.documentation.init; + +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.controller.ControllerServiceInitializationContext; +import org.apache.nifi.controller.ControllerServiceLookup; +import org.apache.nifi.logging.ComponentLog; + +import java.io.File; +import java.util.UUID; + +public class DocumentationControllerServiceInitializationContext implements ControllerServiceInitializationContext { + private final String id = UUID.randomUUID().toString(); + private final ControllerServiceLookup serviceLookup = new EmptyControllerServiceLookup(); + private final ComponentLog componentLog = new NopComponentLog(); + + @Override + public String getIdentifier() { + return id; + } + + @Override + public ControllerServiceLookup getControllerServiceLookup() { + return serviceLookup; + } + + @Override + public ComponentLog getLogger() { + return componentLog; + } + + @Override + public StateManager getStateManager() { + return new NopStateManager(); + } + + @Override + public String getKerberosServicePrincipal() { + return null; + } + + @Override + public File getKerberosServiceKeytab() { + return null; + } + + @Override + public File getKerberosConfigurationFile() { + return null; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java new file mode 100644 index 000000000000..c7a5e406643e --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java @@ -0,0 +1,65 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.controller.ControllerServiceLookup; +import org.apache.nifi.controller.NodeTypeProvider; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.processor.ProcessorInitializationContext; + +import java.io.File; +import java.util.UUID; + +public class DocumentationProcessorInitializationContext implements ProcessorInitializationContext { + private final String uuid = UUID.randomUUID().toString(); + private final NodeTypeProvider nodeTypeProvider = new StandaloneNodeTypeProvider(); + + @Override + public String getIdentifier() { + return uuid; + } + + @Override + public ComponentLog getLogger() { + return new NopComponentLog(); + } + + @Override + public ControllerServiceLookup getControllerServiceLookup() { + return new EmptyControllerServiceLookup(); + } + + @Override + public NodeTypeProvider getNodeTypeProvider() { + return nodeTypeProvider; + } + + @Override + public String getKerberosServicePrincipal() { + return null; + } + + @Override + public File getKerberosServiceKeytab() { + return null; + } + + @Override + public File getKerberosConfigurationFile() { + return null; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java new file mode 100644 index 000000000000..4697ee8d4c68 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java @@ -0,0 +1,89 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.controller.ControllerServiceLookup; +import org.apache.nifi.controller.NodeTypeProvider; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.reporting.ReportingInitializationContext; +import org.apache.nifi.scheduling.SchedulingStrategy; + +import java.io.File; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class DocumentationReportingInitializationContext implements ReportingInitializationContext { + private final String id = UUID.randomUUID().toString(); + private final ComponentLog componentLog = new NopComponentLog(); + private final NodeTypeProvider nodeTypeProvider = new StandaloneNodeTypeProvider(); + private final String name = "name"; + + @Override + public String getIdentifier() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getSchedulingPeriod(final TimeUnit timeUnit) { + return 0; + } + + @Override + public ControllerServiceLookup getControllerServiceLookup() { + return new EmptyControllerServiceLookup(); + } + + @Override + public String getSchedulingPeriod() { + return "0 sec"; + } + + @Override + public SchedulingStrategy getSchedulingStrategy() { + return SchedulingStrategy.TIMER_DRIVEN; + } + + @Override + public ComponentLog getLogger() { + return componentLog; + } + + @Override + public NodeTypeProvider getNodeTypeProvider() { + return nodeTypeProvider; + } + + @Override + public String getKerberosServicePrincipal() { + return null; + } + + @Override + public File getKerberosServiceKeytab() { + return null; + } + + @Override + public File getKerberosConfigurationFile() { + return null; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java new file mode 100644 index 000000000000..4831198d078e --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java @@ -0,0 +1,54 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.controller.ControllerServiceLookup; + +import java.util.Set; + +public class EmptyControllerServiceLookup implements ControllerServiceLookup { + @Override + public ControllerService getControllerService(final String serviceIdentifier) { + return null; + } + + @Override + public boolean isControllerServiceEnabled(final String serviceIdentifier) { + return false; + } + + @Override + public boolean isControllerServiceEnabling(final String serviceIdentifier) { + return false; + } + + @Override + public boolean isControllerServiceEnabled(final ControllerService service) { + return false; + } + + @Override + public Set getControllerServiceIdentifiers(final Class serviceType) throws IllegalArgumentException { + return null; + } + + @Override + public String getControllerServiceName(final String serviceIdentifier) { + return null; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java new file mode 100644 index 000000000000..50ef5ad254bb --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java @@ -0,0 +1,172 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.logging.LogLevel; + +public class NopComponentLog implements ComponentLog { + @Override + public void warn(final String msg, final Throwable t) { + + } + + @Override + public void warn(final String msg, final Object[] os) { + + } + + @Override + public void warn(final String msg, final Object[] os, final Throwable t) { + + } + + @Override + public void warn(final String msg) { + + } + + @Override + public void trace(final String msg, final Throwable t) { + + } + + @Override + public void trace(final String msg, final Object[] os) { + + } + + @Override + public void trace(final String msg) { + + } + + @Override + public void trace(final String msg, final Object[] os, final Throwable t) { + + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void info(final String msg, final Throwable t) { + + } + + @Override + public void info(final String msg, final Object[] os) { + + } + + @Override + public void info(final String msg) { + + } + + @Override + public void info(final String msg, final Object[] os, final Throwable t) { + + } + + @Override + public String getName() { + return null; + } + + @Override + public void error(final String msg, final Throwable t) { + + } + + @Override + public void error(final String msg, final Object[] os) { + + } + + @Override + public void error(final String msg) { + + } + + @Override + public void error(final String msg, final Object[] os, final Throwable t) { + + } + + @Override + public void debug(final String msg, final Throwable t) { + + } + + @Override + public void debug(final String msg, final Object[] os) { + + } + + @Override + public void debug(final String msg, final Object[] os, final Throwable t) { + + } + + @Override + public void debug(final String msg) { + + } + + @Override + public void log(final LogLevel level, final String msg, final Throwable t) { + + } + + @Override + public void log(final LogLevel level, final String msg, final Object[] os) { + + } + + @Override + public void log(final LogLevel level, final String msg) { + + } + + @Override + public void log(final LogLevel level, final String msg, final Object[] os, final Throwable t) { + + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java new file mode 100644 index 000000000000..5e2c9557c8fd --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java @@ -0,0 +1,43 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateMap; + +import java.util.Map; + +public class NopStateManager implements StateManager { + @Override + public void setState(final Map state, final Scope scope) { + } + + @Override + public StateMap getState(final Scope scope) { + return null; + } + + @Override + public boolean replace(final StateMap oldValue, final Map newValue, final Scope scope) { + return false; + } + + @Override + public void clear(final Scope scope) { + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java b/nifi-api/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java new file mode 100644 index 000000000000..a0c5be6214da --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java @@ -0,0 +1,31 @@ +/* + * 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.documentation.init; + +import org.apache.nifi.controller.NodeTypeProvider; + +public class StandaloneNodeTypeProvider implements NodeTypeProvider { + @Override + public boolean isClustered() { + return false; + } + + @Override + public boolean isPrimary() { + return false; + } +} diff --git a/nifi-api/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java b/nifi-api/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java new file mode 100644 index 000000000000..93dee90022c4 --- /dev/null +++ b/nifi-api/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java @@ -0,0 +1,407 @@ +/* + * 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.documentation.xml; + +import org.apache.nifi.annotation.behavior.DynamicProperty; +import org.apache.nifi.annotation.behavior.DynamicRelationship; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.ReadsAttribute; +import org.apache.nifi.annotation.behavior.Restricted; +import org.apache.nifi.annotation.behavior.Restriction; +import org.apache.nifi.annotation.behavior.Stateful; +import org.apache.nifi.annotation.behavior.SystemResourceConsideration; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.documentation.DeprecationNotice; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.ConfigurableComponent; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.RequiredPermission; +import org.apache.nifi.documentation.AbstractDocumentationWriter; +import org.apache.nifi.documentation.ExtensionType; +import org.apache.nifi.documentation.ProvidedServiceAPI; +import org.apache.nifi.processor.Relationship; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * XML-based implementation of DocumentationWriter + * + * Please note that while this class lives within the nifi-api, it is provided primarily as a means for documentation components within + * the NiFi NAR Maven Plugin. Its home is the nifi-api, however, because the API is needed in order to extract the relevant information and + * the NAR Maven Plugin cannot have a direct dependency on nifi-api (doing so would cause a circular dependency). By having this homed within + * the nifi-api, the Maven plugin is able to discover the class dynamically and invoke the one or two methods necessary to create the documentation. + * + * This is a new capability in 1.9.0 in preparation for the Extension Registry and therefore, you should + * NOTE WELL: At this time, while this class is part of nifi-api, it is still evolving and may change in a non-backward-compatible manner or even be + * removed from one incremental release to the next. Use at your own risk! + */ +public class XmlDocumentationWriter extends AbstractDocumentationWriter { + private final XMLStreamWriter writer; + + public XmlDocumentationWriter(final OutputStream out) throws XMLStreamException { + this.writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out, "UTF-8"); + } + + public XmlDocumentationWriter(final XMLStreamWriter writer) { + this.writer = writer; + } + + @Override + protected void writeHeader(final ConfigurableComponent component) throws IOException { + writeStartElement("extension"); + } + + @Override + protected void writeExtensionName(final String extensionName) throws IOException { + writeTextElement("name", extensionName); + } + + @Override + protected void writeExtensionType(final ExtensionType extensionType) throws IOException { + writeTextElement("type", extensionType.name()); + } + + @Override + protected void writeDeprecationNotice(final DeprecationNotice deprecationNotice) throws IOException { + if (deprecationNotice == null) { + writeEmptyElement("deprecationNotice"); + return; + } + + final Class[] classes = deprecationNotice.alternatives(); + final String[] classNames = deprecationNotice.classNames(); + + final Set alternatives = new LinkedHashSet<>(); + if (classes != null) { + for (final Class alternativeClass : classes) { + alternatives.add(alternativeClass.getName()); + } + } + + if (classNames != null) { + Collections.addAll(alternatives, classNames); + } + + writeDeprecationNotice(deprecationNotice.reason(), alternatives); + } + + private void writeDeprecationNotice(final String reason, final Set alternatives) throws IOException { + writeStartElement("deprecationNotice"); + + writeTextElement("reason", reason); + writeTextArray("alternatives", "alternative", alternatives); + + writeEndElement(); + } + + + @Override + protected void writeDescription(final String description) throws IOException { + writeTextElement("description", description); + } + + @Override + protected void writeTags(final List tags) throws IOException { + writeTextArray("tags", "tag", tags); + } + + @Override + protected void writeProperties(final List properties) throws IOException { + writeArray("properties", properties, this::writeProperty); + } + + private void writeProperty(final PropertyDescriptor property) throws IOException { + writeStartElement("property"); + + writeTextElement("name", property.getName()); + writeTextElement("displayName", property.getDisplayName()); + writeTextElement("description", property.getDescription()); + writeTextElement("defaultValue", property.getDefaultValue()); + writeTextElement("controllerServiceDefinition", property.getControllerServiceDefinition() == null ? null : property.getControllerServiceDefinition().getName()); + writeTextArray("allowableValues", "allowableValue", property.getAllowableValues(), AllowableValue::getDisplayName); + writeBooleanElement("required", property.isRequired()); + writeBooleanElement("sensitive", property.isSensitive()); + writeBooleanElement("expressionLanguageSupported", property.isExpressionLanguageSupported()); + writeTextElement("expressionLanguageScope", property.getExpressionLanguageScope() == null ? null : property.getExpressionLanguageScope().name()); + writeBooleanElement("dynamicallyModifiesClasspath", property.isDynamicClasspathModifier()); + writeBooleanElement("dynamic", property.isDynamic()); + + writeEndElement(); + } + + @Override + protected void writeDynamicProperties(final List dynamicProperties) throws IOException { + writeArray("dynamicProperty", dynamicProperties, this::writeDynamicProperty); + } + + private void writeDynamicProperty(final DynamicProperty property) throws IOException { + writeStartElement("dynamicProperty"); + + writeTextElement("name", property.name()); + writeTextElement("value", property.value()); + writeTextElement("description", property.description()); + writeBooleanElement("expressionLanguageSupported", property.supportsExpressionLanguage()); + writeTextElement("expressionLanguageScope", property.expressionLanguageScope() == null ? null : property.expressionLanguageScope().name()); + + writeEndElement(); + } + + @Override + protected void writeStatefulInfo(final Stateful stateful) throws IOException { + writeStartElement("stateful"); + + if (stateful != null) { + writeTextElement("description", stateful.description()); + writeArray("scopes", Arrays.asList(stateful.scopes()), scope -> writeTextElement("scope", scope.name())); + } + + writeEndElement(); + } + + @Override + protected void writeRestrictedInfo(final Restricted restricted) throws IOException { + writeStartElement("restricted"); + + if (restricted != null) { + writeTextElement("generalRestrictionExplanation", restricted.value()); + + final Restriction[] restrictions = restricted.restrictions(); + if (restrictions != null) { + writeArray("restrictions", Arrays.asList(restrictions), this::writeRestriction); + } + } + + writeEndElement(); + } + + private void writeRestriction(final Restriction restriction) throws IOException { + writeStartElement("restriction"); + + final RequiredPermission permission = restriction.requiredPermission(); + final String label = permission == null ? null : permission.getPermissionLabel(); + writeTextElement("requiredPermission", label); + writeTextElement("explanation", restriction.explanation()); + + writeEndElement(); + } + + @Override + protected void writeInputRequirementInfo(final InputRequirement.Requirement requirement) throws IOException { + writeTextElement("inputRequirement", requirement == null ? null : requirement.name()); + } + + @Override + protected void writeSystemResourceConsiderationInfo(final List considerations) throws IOException { + writeArray("systemResourceConsiderations", considerations, this::writeSystemResourceConsideration); + } + + private void writeSystemResourceConsideration(final SystemResourceConsideration consideration) throws IOException { + writeStartElement("consideration"); + + writeTextElement("resource", consideration.resource() == null ? null : consideration.resource().name()); + writeTextElement("description", consideration.description()); + + writeEndElement(); + } + + @Override + protected void writeSeeAlso(final SeeAlso seeAlso) throws IOException { + if (seeAlso == null) { + writeEmptyElement("seeAlso"); + return; + } + + final Class[] classes = seeAlso.value(); + final String[] classNames = seeAlso.classNames(); + + final Set toSee = new LinkedHashSet<>(); + if (classes != null) { + for (final Class classToSee : classes) { + toSee.add(classToSee.getName()); + } + } + + if (classNames != null) { + Collections.addAll(toSee, classNames); + } + + writeTextArray("seeAlso", "see", toSee); + } + + @Override + protected void writeRelationships(final Set relationships) throws IOException { + writeArray("relationships", relationships,rel -> { + writeStartElement("relationship"); + + writeTextElement("name", rel.getName()); + writeTextElement("description", rel.getDescription()); + writeBooleanElement("autoTerminated", rel.isAutoTerminated()); + + writeEndElement(); + } ); + } + + @Override + protected void writeDynamicRelationship(final DynamicRelationship dynamicRelationship) throws IOException { + writeStartElement("dynamicRelationship"); + + if (dynamicRelationship != null) { + writeTextElement("name", dynamicRelationship.name()); + writeTextElement("description", dynamicRelationship.description()); + } + + writeEndElement(); + } + + @Override + protected void writeReadsAttributes(final List attributes) throws IOException { + writeArray("readsAttributes", attributes, this::writeReadsAttribute); + } + + private void writeReadsAttribute(final ReadsAttribute attribute) throws IOException { + writeStartElement("attribute"); + writeTextElement("name", attribute.attribute()); + writeTextElement("description", attribute.description()); + writeEndElement(); + } + + @Override + protected void writeWritesAttributes(final List attributes) throws IOException { + writeArray("writesAttributes", attributes, this::writeWritesAttribute); + } + + private void writeWritesAttribute(final WritesAttribute attribute) throws IOException { + writeStartElement("attribute"); + writeTextElement("name", attribute.attribute()); + writeTextElement("description", attribute.description()); + writeEndElement(); + } + + @Override + protected void writeFooter(final ConfigurableComponent component) throws IOException { + writeEndElement(); + } + + @Override + protected void writeProvidedServices(final Collection providedServices) throws IOException { + writeStartElement("providedServiceAPIs"); + writeArray("service", providedServices, this::writeProvidedService); + writeEndElement(); + } + + private void writeProvidedService(final ProvidedServiceAPI service) throws IOException { + writeTextElement("className",service.getClassName()); + writeTextElement("groupId",service.getGroupId()); + writeTextElement("artifactId",service.getArtifactId()); + writeTextElement("version",service.getVersion()); + } + + private void writeArray(final String tagName, final Collection values, final ElementWriter writer) throws IOException { + writeStartElement(tagName); + + if (values != null) { + for (final T value : values) { + writer.write(value); + } + } + + writeEndElement(); + } + + + private void writeTextArray(final String outerTagName, final String elementTagName, final Collection values) throws IOException { + writeTextArray(outerTagName, elementTagName, values, String::toString); + } + + private void writeTextArray(final String outerTagName, final String elementTagName, final Collection values, final Function transform) throws IOException { + writeStartElement(outerTagName); + + if (values != null) { + for (final T value : values) { + writeStartElement(elementTagName); + if (value != null) { + writeText(transform.apply(value)); + } + writeEndElement(); + } + } + + writeEndElement(); + } + + private void writeText(final String text) throws IOException { + if (text == null) { + return; + } + + try { + writer.writeCharacters(text); + } catch (XMLStreamException e) { + throw new IOException(e); + } + } + + private void writeEmptyElement(final String elementName) throws IOException { + try { + writer.writeEmptyElement(elementName); + } catch (final XMLStreamException e) { + throw new IOException(e); + } + } + + private void writeStartElement(final String elementName) throws IOException { + try { + writer.writeStartElement(elementName); + } catch (final XMLStreamException e) { + throw new IOException(e); + } + } + + private void writeEndElement() throws IOException { + try { + writer.writeEndElement();; + } catch (final XMLStreamException e) { + throw new IOException(e); + } + } + + private void writeTextElement(final String name, final String text) throws IOException { + writeStartElement(name); + writeText(text); + writeEndElement(); + } + + private void writeBooleanElement(final String name, final boolean value) throws IOException { + writeTextElement(name, String.valueOf(value)); + } + + private interface ElementWriter { + void write(T value) throws IOException; + } +} diff --git a/nifi-assembly/NOTICE b/nifi-assembly/NOTICE index 0bea06a1047d..81b9d9b5dfc7 100644 --- a/nifi-assembly/NOTICE +++ b/nifi-assembly/NOTICE @@ -31,6 +31,9 @@ This includes derived works from the Apache Hive (ASLv2 licensed) project (https The derived work is adapted from release-1.2.1/ql/src/java/org/apache/hadoop/hive/ql/io/orc/WriterImpl.java and can be found in the org.apache.hadoop.hive.ql.io.orc package + The derived work is adapted from + release-3.0.0/serde/src/java/org/apache/hadoop/hive/serde2/JsonSerDe.java + and can be found in the org.apache.hive.streaming.NiFiRecordSerDe class This includes derived works from the Apache Software License V2 library Jolt (https://github.com/bazaarvoice/jolt) Copyright 2013-2014 Bazaarvoice, Inc @@ -308,7 +311,7 @@ The following binary components are provided under the Apache Software License v (ASLv2) Spring Framework The following NOTICE information applies: - Spring Framework 4.1.4.RELEASE + Spring Framework 4.x,5.x.RELEASE Copyright (c) 2002-2015 Pivotal, Inc. (ASLv2) Spring Security @@ -1873,4 +1876,4 @@ SIL OFL 1.1 ****************** The following binary components are provided under the SIL Open Font License 1.1 - (SIL OFL 1.1) FontAwesome (4.6.1 - http://fortawesome.github.io/Font-Awesome/license/) + (SIL OFL 1.1) FontAwesome (4.7.0 - https://fontawesome.com/license/free) diff --git a/nifi-assembly/README.md b/nifi-assembly/README.md index 6e868015e907..c39f51f54e46 100644 --- a/nifi-assembly/README.md +++ b/nifi-assembly/README.md @@ -50,7 +50,7 @@ Apache NiFi was made for dataflow. It supports highly configurable directed grap To start NiFi: - [linux/osx] execute bin/nifi.sh start -- [windows] execute bin/start-nifi.bat +- [windows] execute bin/run-nifi.bat - Direct your browser to http://localhost:8080/nifi/ ## Getting Help diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index 4d7294607f6a..05d1b540a246 100755 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-assembly pom @@ -94,28 +94,28 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-framework-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-runtime - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-bootstrap - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-resources resources - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT runtime zip @@ -123,608 +123,620 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-docs resources - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT runtime zip org.apache.nifi nifi-framework-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-provenance-repository-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-ssl-context-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-distributed-cache-services-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-datadog-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-standard-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-jetty-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-update-attribute-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hadoop-libraries-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hadoop-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-0-8-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-0-9-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-0-10-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-0-11-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-1-0-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kafka-2-0-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-confluent-platform-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-http-context-map-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-html-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-lookup-services-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-poi-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kite-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kudu-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-flume-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-ldap-iaa-providers-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kerberos-iaa-providers-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-dbcp-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-mongodb-client-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-mongodb-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-mongodb-services-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-solr-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-social-media-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hl7-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-ccda-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-language-translation-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-enrich-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-aws-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-aws-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-ambari-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-ignite-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-rethinkdb-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-influxdb-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-network-processors-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-avro-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-media-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-couchbase-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-couchbase-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hbase-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-riemann-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hbase_1_1_2-client-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-azure-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-scripting-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-groovyx-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-5-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-client-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-client-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-restapi-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-lumberjack-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-beats-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-cybersecurity-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-email-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-amqp-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-splunk-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-jms-cf-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-jms-processors-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-cassandra-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT + nar + + + org.apache.nifi + nifi-cassandra-services-api-nar + 1.9.0-SNAPSHOT + nar + + + org.apache.nifi + nifi-cassandra-services-nar + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-spring-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-registry-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hive-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hive-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-site-to-site-reporting-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-record-serialization-services-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-mqtt-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-snmp-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-evtx-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-slack-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-windows-event-log-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-websocket-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-websocket-services-jetty-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-websocket-processors-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-tcp-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-gcp-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-gcp-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-stateful-analysis-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-cdc-mysql-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-parquet-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-hwx-schema-registry-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-redis-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-redis-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-metrics-reporter-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-metrics-reporting-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-livy-controller-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-livy-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-druid-controller-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-druid-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-kerberos-credentials-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-proxy-configuration-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-jolt-record-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar @@ -741,7 +753,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-grpc-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar @@ -758,7 +770,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-atlas-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar @@ -775,7 +787,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-hive3-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar @@ -1051,7 +1063,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-ranger-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar @@ -1062,7 +1074,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-ranger-resources - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-assembly/src/main/assembly/common.xml b/nifi-assembly/src/main/assembly/common.xml index e4a8d7d83138..cad953d72d79 100644 --- a/nifi-assembly/src/main/assembly/common.xml +++ b/nifi-assembly/src/main/assembly/common.xml @@ -91,6 +91,15 @@ + + + . + extensions + + */** + + + ./README.md diff --git a/nifi-bootstrap/pom.xml b/nifi-bootstrap/pom.xml index f0bd4f260ccd..6da9dab8760d 100644 --- a/nifi-bootstrap/pom.xml +++ b/nifi-bootstrap/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-bootstrap jar @@ -27,18 +27,18 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT compile org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT javax.mail @@ -48,7 +48,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-expression-language - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.squareup.okhttp3 @@ -66,7 +66,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-properties-loader - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT net.java.dev.jna diff --git a/nifi-commons/nifi-data-provenance-utils/pom.xml b/nifi-commons/nifi-data-provenance-utils/pom.xml index 3fd192473989..6de25f845375 100644 --- a/nifi-commons/nifi-data-provenance-utils/pom.xml +++ b/nifi-commons/nifi-data-provenance-utils/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-data-provenance-utils jar @@ -25,22 +25,22 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-framework-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.bouncycastle @@ -50,12 +50,12 @@ org.apache.nifi nifi-properties-loader - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-properties - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-commons/nifi-expression-language/pom.xml b/nifi-commons/nifi-expression-language/pom.xml index 91a9844576b2..a89cc2002589 100644 --- a/nifi-commons/nifi-expression-language/pom.xml +++ b/nifi-commons/nifi-expression-language/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-expression-language jar @@ -61,12 +61,12 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.hamcrest diff --git a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g index 6c0bcff19a4f..fb3958c0ad27 100644 --- a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g +++ b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g @@ -232,7 +232,7 @@ ESC | '\\' { setText("\\\\"); } | nextChar = ~('"' | '\'' | 'r' | 'n' | 't' | '\\') { - StringBuilder lBuf = new StringBuilder(); lBuf.append("\\\\").appendCodePoint(nextChar); setText(lBuf.toString()); + StringBuilder lBuf = new StringBuilder(); lBuf.append("\\").appendCodePoint(nextChar); setText(lBuf.toString()); } ) ; diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/StandardPreparedQuery.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/StandardPreparedQuery.java index cf90d8d0cc1b..fc3f9b71ca73 100644 --- a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/StandardPreparedQuery.java +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/StandardPreparedQuery.java @@ -16,12 +16,6 @@ */ package org.apache.nifi.attribute.expression.language; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - import org.apache.nifi.attribute.expression.language.evaluation.Evaluator; import org.apache.nifi.attribute.expression.language.evaluation.literals.StringLiteralEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.selection.AllAttributesEvaluator; @@ -34,7 +28,14 @@ import org.apache.nifi.expression.AttributeValueDecorator; import org.apache.nifi.processor.exception.ProcessException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + public class StandardPreparedQuery implements PreparedQuery { + private static final String EMPTY_STRING = ""; private final List expressions; private volatile VariableImpact variableImpact; @@ -45,6 +46,14 @@ public StandardPreparedQuery(final List expressions) { @Override public String evaluateExpressions(final Map valMap, final AttributeValueDecorator decorator, final Map stateVariables) throws ProcessException { + if (expressions.isEmpty()) { + return EMPTY_STRING; + } + if (expressions.size() == 1) { + final String evaluated = expressions.get(0).evaluate(valMap, decorator, stateVariables); + return evaluated == null ? EMPTY_STRING : evaluated; + } + final StringBuilder sb = new StringBuilder(); for (final Expression expression : expressions) { diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/AndEvaluator.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/AndEvaluator.java index 232fc2677f21..adc41da8501b 100644 --- a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/AndEvaluator.java +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/AndEvaluator.java @@ -27,6 +27,7 @@ public class AndEvaluator extends BooleanEvaluator { private final Evaluator subjectEvaluator; private final Evaluator rhsEvaluator; + private BooleanQueryResult rhsResult; public AndEvaluator(final Evaluator subjectEvaluator, final Evaluator rhsEvaluator) { this.subjectEvaluator = subjectEvaluator; @@ -44,9 +45,18 @@ public QueryResult evaluate(final Map attributes) { return new BooleanQueryResult(false); } + // Returning previously evaluated result. + // The same AndEvaluator can be evaluated multiple times if subjectEvaluator is IteratingEvaluator. + // In that case, it's enough to evaluate the right hand side. + if (rhsResult != null) { + return rhsResult; + } + final QueryResult rhsValue = rhsEvaluator.evaluate(attributes); if (rhsValue == null) { - return new BooleanQueryResult(false); + rhsResult = new BooleanQueryResult(false); + } else { + rhsResult = new BooleanQueryResult(rhsValue.getValue()); } return new BooleanQueryResult(rhsValue.getValue()); diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/OrEvaluator.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/OrEvaluator.java index 719fa11448b0..9c63c2781e4f 100644 --- a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/OrEvaluator.java +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/OrEvaluator.java @@ -27,6 +27,7 @@ public class OrEvaluator extends BooleanEvaluator { private final Evaluator subjectEvaluator; private final Evaluator rhsEvaluator; + private BooleanQueryResult rhsResult; public OrEvaluator(final Evaluator subjectEvaluator, final Evaluator rhsEvaluator) { this.subjectEvaluator = subjectEvaluator; @@ -44,12 +45,21 @@ public QueryResult evaluate(final Map attributes) { return new BooleanQueryResult(true); } + // Returning previously evaluated result. + // The same OrEvaluator can be evaluated multiple times if subjectEvaluator is IteratingEvaluator. + // In that case, it's enough to evaluate the right hand side. + if (rhsResult != null) { + return rhsResult; + } + final QueryResult rhsValue = rhsEvaluator.evaluate(attributes); if (rhsValue == null) { - return new BooleanQueryResult(false); + rhsResult = new BooleanQueryResult(false); + } else { + rhsResult = new BooleanQueryResult(rhsValue.getValue()); } - return new BooleanQueryResult(rhsValue.getValue()); + return rhsResult; } @Override diff --git a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java index 2a43bdc865c6..5b368135eb2c 100644 --- a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java +++ b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java @@ -995,6 +995,42 @@ public void testAnyDelineatedValue() { verifyEquals("${anyDelineatedValue(${abc}, ','):equals('d')}", attributes, false); } + @Test + public void testNestedAnyDelineatedValueOr() { + final Map attributes = new HashMap<>(); + attributes.put("abc", "a,b,c"); + attributes.put("xyz", "x"); + + // Assert each part separately. + assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):equals('c')}", + attributes, null)); + assertEquals("false", Query.evaluateExpressions("${anyDelineatedValue('${xyz}', ','):equals('z')}", + attributes, null)); + + // Combine them with 'or'. + assertEquals("true", Query.evaluateExpressions( + "${anyDelineatedValue('${abc}', ','):equals('c'):or(${anyDelineatedValue('${xyz}', ','):equals('z')})}", + attributes, null)); + } + + @Test + public void testNestedAnyDelineatedValueAnd() { + final Map attributes = new HashMap<>(); + attributes.put("abc", "2,0,1,3"); + attributes.put("xyz", "x,y,z"); + + // Assert each part separately. + assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):gt('2')}", + attributes, null)); + assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${xyz}', ','):equals('z')}", + attributes, null)); + + // Combine them with 'and'. + assertEquals("true", Query.evaluateExpressions( + "${anyDelineatedValue('${abc}', ','):gt('2'):and(${anyDelineatedValue('${xyz}', ','):equals('z')})}", + attributes, null)); + } + @Test public void testAllDelineatedValues() { final Map attributes = new HashMap<>(); diff --git a/nifi-commons/nifi-flowfile-packager/pom.xml b/nifi-commons/nifi-flowfile-packager/pom.xml index cd995dac356e..a135a7d7d623 100644 --- a/nifi-commons/nifi-flowfile-packager/pom.xml +++ b/nifi-commons/nifi-flowfile-packager/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-flowfile-packager jar diff --git a/nifi-commons/nifi-hl7-query-language/pom.xml b/nifi-commons/nifi-hl7-query-language/pom.xml index cbe90071c465..ab9061a45f92 100644 --- a/nifi-commons/nifi-hl7-query-language/pom.xml +++ b/nifi-commons/nifi-hl7-query-language/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-hl7-query-language diff --git a/nifi-commons/nifi-hl7-query-language/src/main/antlr3/org/apache/nifi/hl7/query/antlr/HL7QueryLexer.g b/nifi-commons/nifi-hl7-query-language/src/main/antlr3/org/apache/nifi/hl7/query/antlr/HL7QueryLexer.g index 478028b9f78d..ab9d35510709 100644 --- a/nifi-commons/nifi-hl7-query-language/src/main/antlr3/org/apache/nifi/hl7/query/antlr/HL7QueryLexer.g +++ b/nifi-commons/nifi-hl7-query-language/src/main/antlr3/org/apache/nifi/hl7/query/antlr/HL7QueryLexer.g @@ -162,7 +162,7 @@ ESC | '\\' { setText("\\\\"); } | nextChar = ~('"' | '\'' | 'r' | 'n' | 't' | '\\') { - StringBuilder lBuf = new StringBuilder(); lBuf.append("\\\\").appendCodePoint(nextChar); setText(lBuf.toString()); + StringBuilder lBuf = new StringBuilder(); lBuf.append("\\").appendCodePoint(nextChar); setText(lBuf.toString()); } ) ; diff --git a/nifi-commons/nifi-json-utils/pom.xml b/nifi-commons/nifi-json-utils/pom.xml index d4fdb1617eb2..a299399ea2c9 100644 --- a/nifi-commons/nifi-json-utils/pom.xml +++ b/nifi-commons/nifi-json-utils/pom.xml @@ -18,16 +18,16 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-json-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT jar org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.fasterxml.jackson.core diff --git a/nifi-commons/nifi-logging-utils/pom.xml b/nifi-commons/nifi-logging-utils/pom.xml index 185ee8b31a5d..3dfe75e72996 100644 --- a/nifi-commons/nifi-logging-utils/pom.xml +++ b/nifi-commons/nifi-logging-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-logging-utils Utilities for logging diff --git a/nifi-commons/nifi-properties/pom.xml b/nifi-commons/nifi-properties/pom.xml index ee4237e98de0..2b5e22dbbffa 100644 --- a/nifi-commons/nifi-properties/pom.xml +++ b/nifi-commons/nifi-properties/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-properties diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java index 9ded1a1d23f5..9132eaef4f6a 100644 --- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java +++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -62,6 +62,7 @@ public abstract class NiFiProperties { public static final String FLOW_CONTROLLER_GRACEFUL_SHUTDOWN_PERIOD = "nifi.flowcontroller.graceful.shutdown.period"; public static final String NAR_LIBRARY_DIRECTORY = "nifi.nar.library.directory"; public static final String NAR_LIBRARY_DIRECTORY_PREFIX = "nifi.nar.library.directory."; + public static final String NAR_LIBRARY_AUTOLOAD_DIRECTORY = "nifi.nar.library.autoload.directory"; public static final String NAR_WORKING_DIRECTORY = "nifi.nar.working.directory"; public static final String COMPONENT_DOCS_DIRECTORY = "nifi.documentation.working.directory"; public static final String SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key"; @@ -139,7 +140,6 @@ public abstract class NiFiProperties { public static final String SECURITY_TRUSTSTORE = "nifi.security.truststore"; public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.security.truststoreType"; public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.security.truststorePasswd"; - public static final String SECURITY_NEED_CLIENT_AUTH = "nifi.security.needClientAuth"; public static final String SECURITY_USER_AUTHORIZER = "nifi.security.user.authorizer"; public static final String SECURITY_USER_LOGIN_IDENTITY_PROVIDER = "nifi.security.user.login.identity.provider"; public static final String SECURITY_OCSP_RESPONDER_URL = "nifi.security.ocsp.responder.url"; @@ -248,6 +248,7 @@ public abstract class NiFiProperties { public static final String DEFAULT_NAR_WORKING_DIR = "./work/nar"; public static final String DEFAULT_COMPONENT_DOCS_DIRECTORY = "./work/docs/components"; public static final String DEFAULT_NAR_LIBRARY_DIR = "./lib"; + public static final String DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR = "./extensions"; public static final String DEFAULT_FLOWFILE_REPO_PARTITIONS = "256"; public static final String DEFAULT_FLOWFILE_CHECKPOINT_INTERVAL = "2 min"; public static final int DEFAULT_MAX_FLOWFILES_PER_CLAIM = 100; @@ -573,20 +574,6 @@ public File getLoginIdentityProviderConfigurationFile() { } } - /** - * Will default to true unless the value is explicitly set to false. - * - * @return Whether client auth is required - */ - public boolean getNeedClientAuth() { - boolean needClientAuth = true; - String rawNeedClientAuth = getProperty(SECURITY_NEED_CLIENT_AUTH); - if ("false".equalsIgnoreCase(rawNeedClientAuth)) { - needClientAuth = false; - } - return needClientAuth; - } - // getters for web properties // public Integer getPort() { Integer port = null; @@ -665,7 +652,8 @@ public List getNarLibraryDirectories() { for (String propertyName : getPropertyKeys()) { // determine if the property is a nar library path if (StringUtils.startsWith(propertyName, NAR_LIBRARY_DIRECTORY_PREFIX) - || NAR_LIBRARY_DIRECTORY.equals(propertyName)) { + || NAR_LIBRARY_DIRECTORY.equals(propertyName) + || NAR_LIBRARY_AUTOLOAD_DIRECTORY.equals(propertyName)) { // attempt to resolve the path specified String narLib = getProperty(propertyName); if (!StringUtils.isBlank(narLib)) { @@ -681,6 +669,10 @@ public List getNarLibraryDirectories() { return narLibraryPaths; } + public File getNarAutoLoadDirectory() { + return new File(getProperty(NAR_LIBRARY_AUTOLOAD_DIRECTORY, DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR)); + } + // getters for ui properties // /** diff --git a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties index aaf2e29569be..b243a39ed5b8 100644 --- a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties +++ b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties @@ -81,7 +81,6 @@ nifi.security.keyPasswd= nifi.security.truststore= nifi.security.truststoreType= nifi.security.truststorePasswd= -nifi.security.needClientAuth= nifi.security.user.authorizer= # cluster common properties (cluster manager and nodes must have same values) # diff --git a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties index fb48be3e64d5..fd532a4d5aa7 100644 --- a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties +++ b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties @@ -79,7 +79,6 @@ nifi.security.keyPasswd= nifi.security.truststore= nifi.security.truststoreType= nifi.security.truststorePasswd= -nifi.security.needClientAuth= nifi.security.user.authorizer= # cluster common properties (cluster manager and nodes must have same values) # diff --git a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties index 6d1e03bd16d2..2c58fa9cf1ed 100644 --- a/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties +++ b/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties @@ -81,7 +81,6 @@ nifi.security.keyPasswd= nifi.security.truststore= nifi.security.truststoreType= nifi.security.truststorePasswd= -nifi.security.needClientAuth= nifi.security.user.authorizer= # cluster common properties (cluster manager and nodes must have same values) # diff --git a/nifi-commons/nifi-record-path/pom.xml b/nifi-commons/nifi-record-path/pom.xml index 19bcdf2d59e2..29a598419ee0 100644 --- a/nifi-commons/nifi-record-path/pom.xml +++ b/nifi-commons/nifi-record-path/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-record-path @@ -59,17 +59,22 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-record - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.antlr antlr-runtime 3.5.2 + + com.github.ben-manes.caffeine + caffeine + 2.6.2 + diff --git a/nifi-commons/nifi-record-path/src/main/antlr3/org/apache/nifi/record/path/RecordPathLexer.g b/nifi-commons/nifi-record-path/src/main/antlr3/org/apache/nifi/record/path/RecordPathLexer.g index cd466f7b6a1b..b6a838004bc4 100644 --- a/nifi-commons/nifi-record-path/src/main/antlr3/org/apache/nifi/record/path/RecordPathLexer.g +++ b/nifi-commons/nifi-record-path/src/main/antlr3/org/apache/nifi/record/path/RecordPathLexer.g @@ -152,7 +152,7 @@ ESC | '\\' { setText("\\\\"); } | nextChar = ~('"' | '\'' | 'r' | 'n' | 't' | '\\') { - StringBuilder lBuf = new StringBuilder(); lBuf.append("\\\\").appendCodePoint(nextChar); setText(lBuf.toString()); + StringBuilder lBuf = new StringBuilder(); lBuf.append("\\").appendCodePoint(nextChar); setText(lBuf.toString()); } ) ; diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/RecordPathCache.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/RecordPathCache.java index 243ad1142eb8..8cc9e241f15e 100644 --- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/RecordPathCache.java +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/RecordPathCache.java @@ -17,42 +17,20 @@ package org.apache.nifi.record.path.util; -import java.util.LinkedHashMap; -import java.util.Map; - +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import org.apache.nifi.record.path.RecordPath; public class RecordPathCache { - private final Map compiledRecordPaths; + private final LoadingCache compiledRecordPaths; public RecordPathCache(final int cacheSize) { - compiledRecordPaths = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(final Map.Entry eldest) { - return size() >= cacheSize; - } - }; + compiledRecordPaths = Caffeine.newBuilder() + .maximumSize(cacheSize) + .build(RecordPath::compile); } public RecordPath getCompiled(final String path) { - RecordPath compiled; - synchronized (this) { - compiled = compiledRecordPaths.get(path); - } - - if (compiled != null) { - return compiled; - } - - compiled = RecordPath.compile(path); - - synchronized (this) { - final RecordPath existing = compiledRecordPaths.putIfAbsent(path, compiled); - if (existing != null) { - compiled = existing; - } - } - - return compiled; + return compiledRecordPaths.get(path); } } diff --git a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java index 67c14e6475ec..881fb6412c4f 100644 --- a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java +++ b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java @@ -1026,6 +1026,133 @@ public void testReplaceRegex() { assertEquals("Jxohn Dxoe", RecordPath.compile("replaceRegex(/name, '(?[JD])', '${hello}x')").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("48ohn 48oe", RecordPath.compile("replaceRegex(/name, '(?[JD])', /id)").evaluate(record).getSelectedFields().findFirst().get().getValue()); + + } + + @Test + public void testReplaceRegexEscapedCharacters() { + final List fields = new ArrayList<>(); + fields.add(new RecordField("id", RecordFieldType.INT.getDataType())); + fields.add(new RecordField("name", RecordFieldType.STRING.getDataType())); + + final RecordSchema schema = new SimpleRecordSchema(fields); + + final Map values = new HashMap<>(); + values.put("id", 48); + final Record record = new MapRecord(schema, values); + + // Special character cases + values.put("name", "John Doe"); + assertEquals("Replacing whitespace to new line", + "John\nDoe", RecordPath.compile("replaceRegex(/name, '[\\s]', '\\n')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "John\nDoe"); + assertEquals("Replacing new line to whitespace", + "John Doe", RecordPath.compile("replaceRegex(/name, '\\n', ' ')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "John Doe"); + assertEquals("Replacing whitespace to tab", + "John\tDoe", RecordPath.compile("replaceRegex(/name, '[\\s]', '\\t')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "John\tDoe"); + assertEquals("Replacing tab to whitespace", + "John Doe", RecordPath.compile("replaceRegex(/name, '\\t', ' ')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + } + + @Test + public void testReplaceRegexEscapedQuotes() { + + final List fields = new ArrayList<>(); + fields.add(new RecordField("id", RecordFieldType.INT.getDataType())); + fields.add(new RecordField("name", RecordFieldType.STRING.getDataType())); + + final RecordSchema schema = new SimpleRecordSchema(fields); + + final Map values = new HashMap<>(); + values.put("id", 48); + final Record record = new MapRecord(schema, values); + + // Quotes + // NOTE: At Java code, a single back-slash needs to be escaped with another-back slash, but needn't to do so at NiFi UI. + // The test record path is equivalent to replaceRegex(/name, '\'', '"') + values.put("name", "'John' 'Doe'"); + assertEquals("Replacing quote to double-quote", + "\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, '\\'', '\"')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "\"John\" \"Doe\""); + assertEquals("Replacing double-quote to single-quote", + "'John' 'Doe'", RecordPath.compile("replaceRegex(/name, '\"', '\\'')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "'John' 'Doe'"); + assertEquals("Replacing quote to double-quote, the function arguments are wrapped by double-quote", + "\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, \"'\", \"\\\"\")") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "\"John\" \"Doe\""); + assertEquals("Replacing double-quote to single-quote, the function arguments are wrapped by double-quote", + "'John' 'Doe'", RecordPath.compile("replaceRegex(/name, \"\\\"\", \"'\")") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + } + + @Test + public void testReplaceRegexEscapedBackSlashes() { + + final List fields = new ArrayList<>(); + fields.add(new RecordField("id", RecordFieldType.INT.getDataType())); + fields.add(new RecordField("name", RecordFieldType.STRING.getDataType())); + + final RecordSchema schema = new SimpleRecordSchema(fields); + + final Map values = new HashMap<>(); + values.put("id", 48); + final Record record = new MapRecord(schema, values); + + // Back-slash + // NOTE: At Java code, a single back-slash needs to be escaped with another-back slash, but needn't to do so at NiFi UI. + // The test record path is equivalent to replaceRegex(/name, '\\', '/') + values.put("name", "John\\Doe"); + assertEquals("Replacing a back-slash to forward-slash", + "John/Doe", RecordPath.compile("replaceRegex(/name, '\\\\', '/')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "John/Doe"); + assertEquals("Replacing a forward-slash to back-slash", + "John\\Doe", RecordPath.compile("replaceRegex(/name, '/', '\\\\')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + } + + @Test + public void testReplaceRegexEscapedBrackets() { + + final List fields = new ArrayList<>(); + fields.add(new RecordField("id", RecordFieldType.INT.getDataType())); + fields.add(new RecordField("name", RecordFieldType.STRING.getDataType())); + + final RecordSchema schema = new SimpleRecordSchema(fields); + + final Map values = new HashMap<>(); + values.put("id", 48); + final Record record = new MapRecord(schema, values); + + // Brackets + values.put("name", "J[o]hn Do[e]"); + assertEquals("Square brackets can be escaped with back-slash", + "J(o)hn Do(e)", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\[', '('), '\\]', ')')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); + + values.put("name", "J(o)hn Do(e)"); + assertEquals("Brackets can be escaped with back-slash", + "J[o]hn Do[e]", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\(', '['), '\\)', ']')") + .evaluate(record).getSelectedFields().findFirst().get().getValue()); } @Test diff --git a/nifi-commons/nifi-record/pom.xml b/nifi-commons/nifi-record/pom.xml index d366bf13ce11..3a320f16f9eb 100644 --- a/nifi-commons/nifi-record/pom.xml +++ b/nifi-commons/nifi-record/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-record diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java index 6926c939c021..7f7844896a7b 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java @@ -38,6 +38,9 @@ public class SimpleRecordSchema implements RecordSchema { private final AtomicReference text = new AtomicReference<>(); private final String schemaFormat; private final SchemaIdentifier schemaIdentifier; + private String schemaName; + private String schemaNamespace; + private volatile int hashCode; public SimpleRecordSchema(final List fields) { this(fields, createText(fields), null, false, SchemaIdentifier.EMPTY); @@ -169,7 +172,12 @@ public boolean equals(final Object obj) { @Override public int hashCode() { - return 143 + 3 * fields.hashCode(); + int computed = this.hashCode; + if (computed == 0) { + computed = this.hashCode = 143 + 3 * fields.hashCode(); + } + + return computed; } private static String createText(final List fields) { @@ -213,4 +221,30 @@ public String toString() { public SchemaIdentifier getIdentifier() { return schemaIdentifier; } + + /** + * Set schema name. + * @param schemaName schema name as defined in a root record. + */ + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + @Override + public Optional getSchemaName() { + return Optional.ofNullable(schemaName); + } + + /** + * Set schema namespace. + * @param schemaNamespace schema namespace as defined in a root record. + */ + public void setSchemaNamespace(String schemaNamespace) { + this.schemaNamespace = schemaNamespace; + } + + @Override + public Optional getSchemaNamespace() { + return Optional.ofNullable(schemaNamespace); + } } diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java index 367f2b0b53a1..cdc9a32fea75 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java @@ -76,4 +76,15 @@ public interface RecordSchema { * @return the SchemaIdentifier, which provides various attributes for identifying a schema */ SchemaIdentifier getIdentifier(); + + /** + * @return the name of the schema's root record. + */ + Optional getSchemaName(); + + /** + * @return the namespace of the schema. + */ + Optional getSchemaNamespace(); + } diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/type/RecordDataType.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/type/RecordDataType.java index fc6993fb89fc..f7e963101ea0 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/type/RecordDataType.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/type/RecordDataType.java @@ -42,7 +42,7 @@ public RecordSchema getChildSchema() { @Override public int hashCode() { - return 31 + 41 * getFieldType().hashCode() + 41 * (childSchema == null ? 0 : childSchema.hashCode()); + return 31 + 41 * getFieldType().hashCode(); } @Override diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java index 687d9ede1a50..f206a641a2f4 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java @@ -339,6 +339,11 @@ public static Object[] toArray(final Object value, final String fieldName, final return dest; } + if (value instanceof List) { + final List list = (List)value; + return list.toArray(); + } + throw new IllegalTypeConversionException("Cannot convert value [" + value + "] of type " + value.getClass() + " to Object Array for field " + fieldName); } @@ -597,6 +602,11 @@ public static java.sql.Date toDate(final Object value, final Supplier personFields = new ArrayList<>(); + personFields.add(new RecordField("name", RecordFieldType.STRING.getDataType())); + personFields.add(new RecordField("sibling", RecordFieldType.RECORD.getRecordDataType(schema))); + + schema.setFields(personFields); + + schema.hashCode(); + assertTrue(schema.equals(schema)); + + final SimpleRecordSchema secondSchema = new SimpleRecordSchema(SchemaIdentifier.EMPTY); + secondSchema.setFields(personFields); + assertTrue(schema.equals(secondSchema)); + assertTrue(secondSchema.equals(schema)); + } + private Set set(final String... values) { final Set set = new HashSet<>(); for (final String value : values) { diff --git a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java index a8bc28d04e00..45b65b454025 100644 --- a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java +++ b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/TestDataTypeUtils.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,6 +54,21 @@ public void testDateToTimestamp() { assertEquals("Times didn't match", ts.getTime(), sDate.getTime()); } + /* + * This was a bug in NiFi 1.8 where converting from a Timestamp to a Date with the record path API + * would throw an exception. + */ + @Test + public void testTimestampToDate() { + java.util.Date date = new java.util.Date(); + Timestamp ts = DataTypeUtils.toTimestamp(date, null, null); + assertNotNull(ts); + + java.sql.Date output = DataTypeUtils.toDate(ts, null, null); + assertNotNull(output); + assertEquals("Timestamps didn't match", output.getTime(), ts.getTime()); + } + @Test public void testConvertRecordMapToJavaMap() { assertNull(DataTypeUtils.convertRecordMapToJavaMap(null, null)); @@ -168,6 +184,18 @@ public void testConvertRecordFieldToObject() { } + @Test + public void testToArray() { + final List list = Arrays.asList("Seven", "Eleven", "Thirteen"); + + final Object[] array = DataTypeUtils.toArray(list, "list", null); + + assertEquals(list.size(), array.length); + for (int i = 0; i < list.size(); i++) { + assertEquals(list.get(i), array[i]); + } + } + @Test public void testStringToBytes() { Object bytes = DataTypeUtils.convertType("Hello", RecordFieldType.ARRAY.getArrayDataType(RecordFieldType.BYTE.getDataType()),null, StandardCharsets.UTF_8); diff --git a/nifi-commons/nifi-schema-utils/pom.xml b/nifi-commons/nifi-schema-utils/pom.xml index 8c53b61a29a4..833f0914864a 100644 --- a/nifi-commons/nifi-schema-utils/pom.xml +++ b/nifi-commons/nifi-schema-utils/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-schema-utils diff --git a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/RecordIterator.java b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/RecordIterator.java new file mode 100644 index 000000000000..de35cd538ac2 --- /dev/null +++ b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/RecordIterator.java @@ -0,0 +1,28 @@ +/* + * 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.repository.schema; + +import java.io.Closeable; +import java.io.IOException; + +public interface RecordIterator extends Closeable { + + Record next() throws IOException; + + boolean isNext() throws IOException; + +} diff --git a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordReader.java b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordReader.java index 84f353231acd..daedf376821b 100644 --- a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordReader.java +++ b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordReader.java @@ -17,8 +17,11 @@ package org.apache.nifi.repository.schema; +import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -30,7 +33,6 @@ import java.util.Map; import java.util.Optional; - public class SchemaRecordReader { private final RecordSchema schema; @@ -56,15 +58,24 @@ private static void fillBuffer(final InputStream in, final byte[] destination) t } public Record readRecord(final InputStream in) throws IOException { - final int sentinelByte = in.read(); - if (sentinelByte < 0) { + final int recordIndicator = in.read(); + if (recordIndicator < 0) { return null; } - if (sentinelByte != 1) { - throw new IOException("Expected to read a Sentinel Byte of '1' but got a value of '" + sentinelByte + "' instead"); + if (recordIndicator == SchemaRecordWriter.EXTERNAL_FILE_INDICATOR) { + throw new IOException("Expected to read a Sentinel Byte of '1' indicating that the next record is inline but the Sentinel value was '" + SchemaRecordWriter.EXTERNAL_FILE_INDICATOR + + ", indicating that data was written to an External File. This data cannot be recovered via calls to #readRecord(InputStream) but must be recovered via #readRecords(InputStream)"); + } + + if (recordIndicator != 1) { + throw new IOException("Expected to read a Sentinel Byte of '1' but got a value of '" + recordIndicator + "' instead"); } + return readInlineRecord(in); + } + + private Record readInlineRecord(final InputStream in) throws IOException { final List schemaFields = schema.getFields(); final Map fields = new HashMap<>(schemaFields.size()); @@ -76,6 +87,53 @@ public Record readRecord(final InputStream in) throws IOException { return new FieldMapRecord(fields, schema); } + public RecordIterator readRecords(final InputStream in) throws IOException { + final int recordIndicator = in.read(); + if (recordIndicator < 0) { + return null; + } + + if (recordIndicator == SchemaRecordWriter.INLINE_RECORD_INDICATOR) { + final Record nextRecord = readInlineRecord(in); + return new SingleRecordIterator(nextRecord); + } + + if (recordIndicator != SchemaRecordWriter.EXTERNAL_FILE_INDICATOR) { + throw new IOException("Expected to read a Sentinel Byte of '" + SchemaRecordWriter.INLINE_RECORD_INDICATOR + "' or '" + SchemaRecordWriter.EXTERNAL_FILE_INDICATOR + + "' but encountered a value of '" + recordIndicator + "' instead"); + } + + final DataInputStream dis = new DataInputStream(in); + final String externalFilename = dis.readUTF(); + final File externalFile = new File(externalFilename); + final FileInputStream fis = new FileInputStream(externalFile); + final InputStream bufferedIn = new BufferedInputStream(fis); + + final RecordIterator recordIterator = new RecordIterator() { + @Override + public Record next() throws IOException { + return readRecord(bufferedIn); + } + + @Override + public boolean isNext() throws IOException { + bufferedIn.mark(1); + final int nextByte = bufferedIn.read(); + bufferedIn.reset(); + + return (nextByte > -1); + } + + @Override + public void close() throws IOException { + bufferedIn.close(); + } + }; + + return recordIterator; + } + + private Object readField(final InputStream in, final RecordField field) throws IOException { switch (field.getRepetition()) { diff --git a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordWriter.java b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordWriter.java index 67d558ae47ab..d65e60be9d66 100644 --- a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordWriter.java +++ b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SchemaRecordWriter.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.UTFDataFormatException; @@ -30,6 +31,8 @@ import java.util.Map; public class SchemaRecordWriter { + static final int INLINE_RECORD_INDICATOR = 1; + static final int EXTERNAL_FILE_INDICATOR = 8; public static final int MAX_ALLOWED_UTF_LENGTH = 65_535; @@ -41,7 +44,7 @@ public void writeRecord(final Record record, final OutputStream out) throws IOEx // write sentinel value to indicate that there is a record. This allows the reader to then read one // byte and check if -1. If so, the reader knows there are no more records. If not, then the reader // knows that it should be able to continue reading. - out.write(1); + out.write(INLINE_RECORD_INDICATOR); final byte[] buffer = byteArrayCache.checkOut(); try { @@ -226,4 +229,8 @@ static int getCharsInUTF8Limit(final String str, final int utf8Limit) { return charsInOriginal; } + public void writeExternalFileReference(final DataOutputStream out, final File externalFile) throws IOException { + out.write(EXTERNAL_FILE_INDICATOR); + out.writeUTF(externalFile.getAbsolutePath()); + } } diff --git a/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SingleRecordIterator.java b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SingleRecordIterator.java new file mode 100644 index 000000000000..cc007fc82773 --- /dev/null +++ b/nifi-commons/nifi-schema-utils/src/main/java/org/apache/nifi/repository/schema/SingleRecordIterator.java @@ -0,0 +1,45 @@ +/* + * 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.repository.schema; + +public class SingleRecordIterator implements RecordIterator { + private final Record record; + private boolean consumed = false; + + public SingleRecordIterator(final Record record) { + this.record = record; + } + + @Override + public Record next() { + if (consumed) { + return null; + } + + consumed = true; + return record; + } + + @Override + public void close() { + } + + @Override + public boolean isNext() { + return !consumed; + } +} diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml index da684c5735c5..6da8dfb7ba64 100644 --- a/nifi-commons/nifi-security-utils/pom.xml +++ b/nifi-commons/nifi-security-utils/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-security-utils @@ -30,12 +30,12 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.slf4j @@ -64,7 +64,7 @@ org.apache.nifi nifi-properties - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT de.svenkubiak diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/StandardKeytabUser.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/AbstractKerberosUser.java similarity index 86% rename from nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/StandardKeytabUser.java rename to nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/AbstractKerberosUser.java index 7302ee00eba0..32eb9bb3d9d2 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/StandardKeytabUser.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/AbstractKerberosUser.java @@ -23,7 +23,6 @@ import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosTicket; -import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import java.security.PrivilegedAction; @@ -34,14 +33,9 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -/** - * Used to authenticate and execute actions when Kerberos is enabled and a keytab is being used. - * - * Some of the functionality in this class is adapted from Hadoop's UserGroupInformation. - */ -public class StandardKeytabUser implements KeytabUser { +public abstract class AbstractKerberosUser implements KerberosUser { - private static final Logger LOGGER = LoggerFactory.getLogger(StandardKeytabUser.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractKerberosUser.class); static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; @@ -50,18 +44,15 @@ public class StandardKeytabUser implements KeytabUser { */ static final float TICKET_RENEW_WINDOW = 0.80f; - private final String principal; - private final String keytabFile; - private final AtomicBoolean loggedIn = new AtomicBoolean(false); + protected final String principal; + protected final AtomicBoolean loggedIn = new AtomicBoolean(false); - private Subject subject; - private LoginContext loginContext; + protected Subject subject; + protected LoginContext loginContext; - public StandardKeytabUser(final String principal, final String keytabFile) { + public AbstractKerberosUser(final String principal) { this.principal = principal; - this.keytabFile = keytabFile; - Validate.notBlank(principal); - Validate.notBlank(keytabFile); + Validate.notBlank(this.principal); } /** @@ -80,19 +71,19 @@ public synchronized void login() throws LoginException { if (loginContext == null) { LOGGER.debug("Initializing new login context..."); this.subject = new Subject(); - - final Configuration config = new KeytabConfiguration(principal, keytabFile); - this.loginContext = new LoginContext("KeytabConf", subject, null, config); + this.loginContext = createLoginContext(subject); } loginContext.login(); loggedIn.set(true); LOGGER.debug("Successful login for {}", new Object[]{principal}); } catch (LoginException le) { - throw new LoginException("Unable to login with " + principal + " and " + keytabFile + " due to: " + le.getMessage()); + throw new LoginException("Unable to login with " + principal + " due to: " + le.getMessage()); } } + protected abstract LoginContext createLoginContext(final Subject subject) throws LoginException; + /** * Performs a logout of the current user. * @@ -244,14 +235,6 @@ public String getPrincipal() { return principal; } - /** - * @return the keytab file for this user - */ - @Override - public String getKeytabFile() { - return keytabFile; - } - // Visible for testing Subject getSubject() { return this.subject; diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/ConfigurationUtil.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/ConfigurationUtil.java new file mode 100644 index 000000000000..131ff2233a05 --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/ConfigurationUtil.java @@ -0,0 +1,25 @@ +/* + * 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.security.krb; + +public interface ConfigurationUtil { + + boolean IS_IBM = System.getProperty("java.vendor", "").contains("IBM"); + String IBM_KRB5_LOGIN_MODULE = "com.ibm.security.auth.module.Krb5LoginModule"; + String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + +} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabAction.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosAction.java similarity index 79% rename from nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabAction.java rename to nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosAction.java index 5e3f5925fa86..bd3e1f92649b 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabAction.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosAction.java @@ -25,24 +25,24 @@ import java.security.PrivilegedAction; /** - * Helper class for processors to perform an action as a KeytabUser. + * Helper class for processors to perform an action as a KerberosUser. */ -public class KeytabAction { +public class KerberosAction { - private final KeytabUser keytabUser; + private final KerberosUser kerberosUser; private final PrivilegedAction action; private final ProcessContext context; private final ComponentLog logger; - public KeytabAction(final KeytabUser keytabUser, - final PrivilegedAction action, - final ProcessContext context, - final ComponentLog logger) { - this.keytabUser = keytabUser; + public KerberosAction(final KerberosUser kerberosUser, + final PrivilegedAction action, + final ProcessContext context, + final ComponentLog logger) { + this.kerberosUser = kerberosUser; this.action = action; this.context = context; this.logger = logger; - Validate.notNull(this.keytabUser); + Validate.notNull(this.kerberosUser); Validate.notNull(this.action); Validate.notNull(this.context); Validate.notNull(this.logger); @@ -50,10 +50,10 @@ public KeytabAction(final KeytabUser keytabUser, public void execute() { // lazily login the first time the processor executes - if (!keytabUser.isLoggedIn()) { + if (!kerberosUser.isLoggedIn()) { try { - keytabUser.login(); - logger.info("Successful login for {}", new Object[]{keytabUser.getPrincipal()}); + kerberosUser.login(); + logger.info("Successful login for {}", new Object[]{kerberosUser.getPrincipal()}); } catch (LoginException e) { // make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately context.yield(); @@ -63,7 +63,7 @@ public void execute() { // check if we need to re-login, will only happen if re-login window is reached (80% of TGT life) try { - keytabUser.checkTGTAndRelogin(); + kerberosUser.checkTGTAndRelogin(); } catch (LoginException e) { // make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately context.yield(); @@ -72,15 +72,15 @@ public void execute() { // attempt to execute the action, if an exception is caught attempt to logout/login and retry try { - keytabUser.doAs(action); + kerberosUser.doAs(action); } catch (SecurityException se) { logger.info("Privileged action failed, attempting relogin and retrying..."); logger.debug("", se); try { - keytabUser.logout(); - keytabUser.login(); - keytabUser.doAs(action); + kerberosUser.logout(); + kerberosUser.login(); + kerberosUser.doAs(action); } catch (Exception e) { // make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately context.yield(); diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosKeytabUser.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosKeytabUser.java new file mode 100644 index 000000000000..a2af82e08a0c --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosKeytabUser.java @@ -0,0 +1,59 @@ +/* + * 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.security.krb; + +import org.apache.commons.lang3.Validate; + +import javax.security.auth.Subject; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** + * Used to authenticate and execute actions when Kerberos is enabled and a keytab is being used. + * + * Some of the functionality in this class is adapted from Hadoop's UserGroupInformation. + */ +public class KerberosKeytabUser extends AbstractKerberosUser { + + private final String keytabFile; + + public KerberosKeytabUser(final String principal, final String keytabFile) { + super(principal); + this.keytabFile = keytabFile; + Validate.notBlank(keytabFile); + } + + @Override + protected LoginContext createLoginContext(Subject subject) throws LoginException { + final Configuration config = new KeytabConfiguration(principal, keytabFile); + return new LoginContext("KeytabConf", subject, null, config); + } + + /** + * @return the keytab file for this user + */ + public String getKeytabFile() { + return keytabFile; + } + + // Visible for testing + Subject getSubject() { + return this.subject; + } + +} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosPasswordUser.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosPasswordUser.java new file mode 100644 index 000000000000..d81fc850d7b9 --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosPasswordUser.java @@ -0,0 +1,110 @@ +/* + * 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.security.krb; + +import org.apache.commons.lang3.Validate; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import java.io.IOException; +import java.util.HashMap; + +/** + * KerberosUser that authenticates via username and password instead of keytab. + */ +public class KerberosPasswordUser extends AbstractKerberosUser { + + private final String password; + + public KerberosPasswordUser(final String principal, final String password) { + super(principal); + this.password = password; + Validate.notBlank(this.password); + } + + @Override + protected LoginContext createLoginContext(final Subject subject) throws LoginException { + final Configuration configuration = new PasswordConfig(); + final CallbackHandler callbackHandler = new UsernamePasswordCallbackHandler(principal, password); + return new LoginContext("PasswordConf", subject, callbackHandler, configuration); + } + + /** + * JAAS Configuration to use when logging in with username/password. + */ + private static class PasswordConfig extends Configuration { + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + HashMap options = new HashMap(); + options.put("storeKey", "true"); + options.put("refreshKrb5Config", "true"); + + final String krbLoginModuleName = ConfigurationUtil.IS_IBM + ? ConfigurationUtil.IBM_KRB5_LOGIN_MODULE : ConfigurationUtil.SUN_KRB5_LOGIN_MODULE; + + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + krbLoginModuleName, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options + ) + }; + } + + } + + /** + * CallbackHandler that provides the given username and password. + */ + private static class UsernamePasswordCallbackHandler implements CallbackHandler { + + private final String username; + private final String password; + + public UsernamePasswordCallbackHandler(final String username, final String password) { + this.username = username; + this.password = password; + Validate.notBlank(this.username); + Validate.notBlank(this.password); + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (final Callback callback : callbacks) { + if (callback instanceof NameCallback) { + final NameCallback nameCallback = (NameCallback) callback; + nameCallback.setName(username); + } else if (callback instanceof PasswordCallback) { + final PasswordCallback passwordCallback = (PasswordCallback) callback; + passwordCallback.setPassword(password.toCharArray()); + } else { + throw new IllegalStateException("Unexpected callback type: " + callback.getClass().getCanonicalName()); + } + } + } + + } + +} diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabUser.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosUser.java similarity index 95% rename from nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabUser.java rename to nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosUser.java index 42089c2d2647..16e4fd2b66e5 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabUser.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KerberosUser.java @@ -24,7 +24,7 @@ /** * A keytab-based user that can login/logout and perform actions as the given user. */ -public interface KeytabUser { +public interface KerberosUser { /** * Performs a login for the given user. @@ -80,9 +80,4 @@ T doAs(PrivilegedExceptionAction action) */ String getPrincipal(); - /** - * @return the keytab file for this user - */ - String getKeytabFile(); - } diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabConfiguration.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabConfiguration.java index 0ad0efe13c69..24af9b207ed4 100644 --- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabConfiguration.java +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabConfiguration.java @@ -28,10 +28,6 @@ */ public class KeytabConfiguration extends Configuration { - static final boolean IS_IBM = System.getProperty("java.vendor", "").contains("IBM"); - static final String IBM_KRB5_LOGIN_MODULE = "com.ibm.security.auth.module.Krb5LoginModule"; - static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; - private final String principal; private final String keytabFile; @@ -53,7 +49,7 @@ public KeytabConfiguration(final String principal, final String keytabFile) { options.put("principal", principal); options.put("refreshKrb5Config", "true"); - if (IS_IBM) { + if (ConfigurationUtil.IS_IBM) { options.put("useKeytab", keytabFile); options.put("credsType", "both"); } else { @@ -64,7 +60,8 @@ public KeytabConfiguration(final String principal, final String keytabFile) { options.put("storeKey", "true"); } - final String krbLoginModuleName = IS_IBM ? IBM_KRB5_LOGIN_MODULE : SUN_KRB5_LOGIN_MODULE; + final String krbLoginModuleName = ConfigurationUtil.IS_IBM + ? ConfigurationUtil.IBM_KRB5_LOGIN_MODULE : ConfigurationUtil.SUN_KRB5_LOGIN_MODULE; this.kerberosKeytabConfigEntry = new AppConfigurationEntry( krbLoginModuleName, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KDCServer.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KDCServer.java index 5669b07be27f..478b4dee16eb 100644 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KDCServer.java +++ b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KDCServer.java @@ -67,8 +67,11 @@ public String getRealm() { return kdc.getRealm(); } - public void createKeytabFile(final File keytabFile, final String... names) throws Exception { + public void createKeytabPrincipal(final File keytabFile, final String... names) throws Exception { kdc.createPrincipal(keytabFile, names); } + public void createPasswordPrincipal(final String principal, final String password) throws Exception { + kdc.createPrincipal(principal, password); + } } diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KeytabUserIT.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KerberosUserIT.java similarity index 65% rename from nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KeytabUserIT.java rename to nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KerberosUserIT.java index 795f4fb40a82..e6360089c47b 100644 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KeytabUserIT.java +++ b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/KerberosUserIT.java @@ -37,7 +37,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; -public class KeytabUserIT { +public class KerberosUserIT { @ClassRule public static TemporaryFolder tmpDir = new TemporaryFolder(); @@ -50,6 +50,9 @@ public class KeytabUserIT { private static KerberosPrincipal principal2; private static File principal2KeytabFile; + private static KerberosPrincipal principal3; + private static final String principal3Password = "changeme"; + @BeforeClass public static void setupClass() throws Exception { kdc = new KDCServer(tmpDir.newFolder("mini-kdc_")); @@ -58,31 +61,34 @@ public static void setupClass() throws Exception { principal1 = new KerberosPrincipal("user1@" + kdc.getRealm()); principal1KeytabFile = tmpDir.newFile("user1.keytab"); - kdc.createKeytabFile(principal1KeytabFile, "user1"); + kdc.createKeytabPrincipal(principal1KeytabFile, "user1"); principal2 = new KerberosPrincipal("user2@" + kdc.getRealm()); principal2KeytabFile = tmpDir.newFile("user2.keytab"); - kdc.createKeytabFile(principal2KeytabFile, "user2"); + kdc.createKeytabPrincipal(principal2KeytabFile, "user2"); + + principal3 = new KerberosPrincipal("user3@" + kdc.getRealm()); + kdc.createPasswordPrincipal("user3", principal3Password); } @Test - public void testSuccessfulLoginAndLogout() throws LoginException { + public void testKeytabUserSuccessfulLoginAndLogout() throws LoginException { // perform login for user1 - final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); + final KerberosUser user1 = new KerberosKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); user1.login(); // perform login for user2 - final KeytabUser user2 = new StandardKeytabUser(principal2.getName(), principal2KeytabFile.getAbsolutePath()); + final KerberosUser user2 = new KerberosKeytabUser(principal2.getName(), principal2KeytabFile.getAbsolutePath()); user2.login(); // verify user1 Subject only has user1 principal - final Subject user1Subject = ((StandardKeytabUser) user1).getSubject(); + final Subject user1Subject = ((KerberosKeytabUser) user1).getSubject(); final Set user1SubjectPrincipals = user1Subject.getPrincipals(); assertEquals(1, user1SubjectPrincipals.size()); assertEquals(principal1.getName(), user1SubjectPrincipals.iterator().next().getName()); // verify user2 Subject only has user2 principal - final Subject user2Subject = ((StandardKeytabUser) user2).getSubject(); + final Subject user2Subject = ((KerberosKeytabUser) user2).getSubject(); final Set user2SubjectPrincipals = user2Subject.getPrincipals(); assertEquals(1, user2SubjectPrincipals.size()); assertEquals(principal2.getName(), user2SubjectPrincipals.iterator().next().getName()); @@ -101,9 +107,9 @@ public void testSuccessfulLoginAndLogout() throws LoginException { } @Test - public void testLoginWithUnknownPrincipal() throws LoginException { + public void testKeytabLoginWithUnknownPrincipal() throws LoginException { final String unknownPrincipal = "doesnotexist@" + kdc.getRealm(); - final KeytabUser user1 = new StandardKeytabUser(unknownPrincipal, principal1KeytabFile.getAbsolutePath()); + final KerberosUser user1 = new KerberosKeytabUser(unknownPrincipal, principal1KeytabFile.getAbsolutePath()); try { user1.login(); fail("Login should have failed"); @@ -113,9 +119,38 @@ public void testLoginWithUnknownPrincipal() throws LoginException { } } + @Test + public void testPasswordUserSuccessfulLoginAndLogout() throws LoginException { + // perform login for user + final KerberosUser user = new KerberosPasswordUser(principal3.getName(), principal3Password); + user.login(); + + // verify user Subject only has user principal + final Subject userSubject = ((KerberosPasswordUser) user).getSubject(); + final Set userSubjectPrincipals = userSubject.getPrincipals(); + assertEquals(1, userSubjectPrincipals.size()); + assertEquals(principal3.getName(), userSubjectPrincipals.iterator().next().getName()); + + // call check/relogin and verify neither user performed a relogin + assertFalse(user.checkTGTAndRelogin()); + + // perform logout for both users + user.logout(); + + // verify subjects have no more principals + assertEquals(0, userSubject.getPrincipals().size()); + } + + @Test(expected = LoginException.class) + public void testPasswordUserLoginWithInvalidPassword() throws LoginException { + // perform login for user + final KerberosUser user = new KerberosPasswordUser("user3", "NOT THE PASSWORD"); + user.login(); + } + @Test public void testCheckTGTAndRelogin() throws LoginException, InterruptedException { - final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); + final KerberosUser user1 = new KerberosKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); user1.login(); // Since we set the lifetime to 15 seconds we should hit a relogin before 15 attempts @@ -136,7 +171,7 @@ public void testCheckTGTAndRelogin() throws LoginException, InterruptedException @Test public void testKeytabAction() { - final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); + final KerberosUser user1 = new KerberosKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath()); final AtomicReference resultHolder = new AtomicReference<>(null); final PrivilegedAction privilegedAction = () -> { @@ -148,8 +183,8 @@ public void testKeytabAction() { final ComponentLog logger = Mockito.mock(ComponentLog.class); // create the action to test and execute it - final KeytabAction keytabAction = new KeytabAction(user1, privilegedAction, context, logger); - keytabAction.execute(); + final KerberosAction kerberosAction = new KerberosAction(user1, privilegedAction, context, logger); + kerberosAction.execute(); // if the result holder has the string success then we know the action executed assertEquals("SUCCESS", resultHolder.get()); diff --git a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/TestKeytabConfiguration.java b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/TestKeytabConfiguration.java index f1056365c1e8..663fa06329c7 100644 --- a/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/TestKeytabConfiguration.java +++ b/nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/krb/TestKeytabConfiguration.java @@ -39,7 +39,7 @@ public void testCreatingKeytabConfiguration() { assertEquals(1, entries.length); final AppConfigurationEntry entry = entries[0]; - assertEquals(KeytabConfiguration.SUN_KRB5_LOGIN_MODULE, entry.getLoginModuleName()); + assertEquals(ConfigurationUtil.SUN_KRB5_LOGIN_MODULE, entry.getLoginModuleName()); assertEquals(principal, entry.getOptions().get("principal")); assertEquals(keytab, entry.getOptions().get("keyTab")); } diff --git a/nifi-commons/nifi-site-to-site-client/pom.xml b/nifi-commons/nifi-site-to-site-client/pom.xml index cf9ab8c30c5d..0f98e9d85edf 100644 --- a/nifi-commons/nifi-site-to-site-client/pom.xml +++ b/nifi-commons/nifi-site-to-site-client/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-site-to-site-client @@ -28,22 +28,22 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-framework-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.commons @@ -58,7 +58,7 @@ org.apache.nifi nifi-client-dto - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.httpcomponents diff --git a/nifi-commons/nifi-socket-utils/pom.xml b/nifi-commons/nifi-socket-utils/pom.xml index f4a4668ae305..f190d8d4a93e 100644 --- a/nifi-commons/nifi-socket-utils/pom.xml +++ b/nifi-commons/nifi-socket-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-socket-utils Utilities for socket communication @@ -30,12 +30,12 @@ org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-logging-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT commons-net @@ -55,7 +55,7 @@ org.apache.nifi nifi-properties - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-commons/nifi-utils/pom.xml b/nifi-commons/nifi-utils/pom.xml index 2ec820004284..976838ce8ce2 100644 --- a/nifi-commons/nifi-utils/pom.xml +++ b/nifi-commons/nifi-utils/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT jar This nifi-utils module should be a general purpose place to store widely @@ -38,7 +38,7 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/RepeatingInputStream.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/RepeatingInputStream.java new file mode 100644 index 000000000000..f542741a1aed --- /dev/null +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/RepeatingInputStream.java @@ -0,0 +1,103 @@ +/* + * 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.stream.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +public class RepeatingInputStream extends InputStream { + private final byte[] toRepeat; + private final int maxIterations; + + private InputStream bais; + private int repeatCount; + + + public RepeatingInputStream(final byte[] toRepeat, final int iterations) { + if (iterations < 1) { + throw new IllegalArgumentException(); + } + if (Objects.requireNonNull(toRepeat).length == 0) { + throw new IllegalArgumentException(); + } + + this.toRepeat = toRepeat; + this.maxIterations = iterations; + + repeat(); + bais = new ByteArrayInputStream(toRepeat); + repeatCount = 1; + } + + @Override + public int read() throws IOException { + final int value = bais.read(); + if (value > -1) { + return value; + } + + final boolean repeated = repeat(); + if (repeated) { + return bais.read(); + } + + return -1; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + final int value = bais.read(b, off, len); + if (value > -1) { + return value; + } + + final boolean repeated = repeat(); + if (repeated) { + return bais.read(b, off, len); + } + + return -1; + } + + @Override + public int read(final byte[] b) throws IOException { + final int value = bais.read(b); + if (value > -1) { + return value; + } + + final boolean repeated = repeat(); + if (repeated) { + return bais.read(b); + } + + return -1; + } + + private boolean repeat() { + if (repeatCount >= maxIterations) { + return false; + } + + repeatCount++; + bais = new ByteArrayInputStream(toRepeat); + + return true; + } +} diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/AbstractTextDemarcator.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/AbstractTextDemarcator.java new file mode 100644 index 000000000000..f10f66df3c93 --- /dev/null +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/AbstractTextDemarcator.java @@ -0,0 +1,147 @@ +/* + * 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.stream.io.util; + +import org.apache.nifi.stream.io.exception.TokenTooLargeException; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.BufferOverflowException; + +public abstract class AbstractTextDemarcator implements Closeable { + + private static final int INIT_BUFFER_SIZE = 8192; + + private final Reader reader; + + /* + * The maximum allowed size of the token. In the event such size is exceeded + * TokenTooLargeException is thrown. + */ + private final int maxDataSize; + + /* + * Buffer into which the bytes are read from the provided stream. The size + * of the buffer is defined by the 'initialBufferSize' provided in the + * constructor or defaults to the value of INIT_BUFFER_SIZE constant. + */ + char[] buffer; + + /* + * Starting offset of the demarcated token within the current 'buffer'. + */ + int index; + + /* + * Starting offset of the demarcated token within the current 'buffer'. Keep + * in mind that while most of the time it is the same as the 'index' it may + * also have a value of 0 at which point it serves as a signal to the fill() + * operation that buffer needs to be expended if end of token is not reached + * (see fill() operation for more details). + */ + int mark; + + /* + * The length of the bytes valid for reading. It is different from the + * buffer length, since this number may be smaller (e.g., at he end of the + * stream) then actual buffer length. It is set by the fill() operation + * every time more bytes read into buffer. + */ + int availableBytesLength; + + /** + * Constructs an instance of demarcator with provided {@link InputStream} + * and max buffer size. Each demarcated token must fit within max buffer + * size, otherwise the exception will be raised. + */ + AbstractTextDemarcator(Reader reader, int maxDataSize) { + this(reader, maxDataSize, INIT_BUFFER_SIZE); + } + + /** + * Constructs an instance of demarcator with provided {@link InputStream} + * and max buffer size and initial buffer size. Each demarcated token must + * fit within max buffer size, otherwise the exception will be raised. + */ + AbstractTextDemarcator(Reader reader, int maxDataSize, int initialBufferSize) { + this.validate(reader, maxDataSize, initialBufferSize); + this.reader = reader; + this.buffer = new char[initialBufferSize]; + this.maxDataSize = maxDataSize; + } + + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * Will fill the current buffer from current 'index' position, expanding it + * and or shuffling it if necessary. If buffer exceeds max buffer size a + * {@link TokenTooLargeException} will be thrown. + * + * @throws IOException + * if unable to read from the stream + */ + void fill() throws IOException { + if (this.index >= this.buffer.length) { + if (this.mark == 0) { // expand + long expandedSize = Math.min(this.buffer.length * 2, this.buffer.length + 1_048_576); + if (expandedSize > maxDataSize) { + throw new BufferOverflowException(); + } + + char[] newBuff = new char[(int) expandedSize]; + System.arraycopy(this.buffer, 0, newBuff, 0, this.buffer.length); + this.buffer = newBuff; + } else { // shift the data left in the buffer + int length = this.index - this.mark; + System.arraycopy(this.buffer, this.mark, this.buffer, 0, length); + this.index = length; + this.mark = 0; + } + } + + int bytesRead; + /* + * The do/while pattern is used here similar to the way it is used in + * BufferedReader essentially protecting from assuming the EOS until it + * actually is since not every implementation of InputStream guarantees + * that bytes are always available while the stream is open. + */ + do { + bytesRead = reader.read(this.buffer, this.index, this.buffer.length - this.index); + } while (bytesRead == 0); + this.availableBytesLength = bytesRead != -1 ? this.index + bytesRead : -1; + } + + + /** + * Validates prerequisites for constructor arguments + */ + private void validate(Reader reader, int maxDataSize, int initialBufferSize) { + if (reader == null) { + throw new IllegalArgumentException("'reader' must not be null"); + } else if (maxDataSize <= 0) { + throw new IllegalArgumentException("'maxDataSize' must be > 0"); + } else if (initialBufferSize <= 0) { + throw new IllegalArgumentException("'initialBufferSize' must be > 0"); + } + } +} diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/LineDemarcator.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/LineDemarcator.java new file mode 100644 index 000000000000..c08b1a427409 --- /dev/null +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/util/LineDemarcator.java @@ -0,0 +1,116 @@ +/* + * 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.stream.io.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +/** + * A demarcator that scans an InputStream for line endings (carriage returns and new lines) and returns + * lines of text one-at-a-time. This is similar to BufferedReader but with a very important distinction: while + * BufferedReader returns the lines of text after stripping off any line endings, this class returns text including the + * line endings. So, for example, if the following text is provided: + * + * ABC\rXYZ\nABCXYZ\r\nhello + * + * Then calls to {@link #nextLine()} will result in 4 String values being returned: + * + *
    + *
  • ABC\r
  • + *
  • XYZ\n
  • + *
  • ABCXYZ\r\n
  • + *
  • hello
  • + *
+ * + * All subsequent calls to {@link #nextLine()} will return null. + */ +public class LineDemarcator extends AbstractTextDemarcator { + private static final char CARRIAGE_RETURN = '\r'; + private static final char NEW_LINE = '\n'; + + private char lastChar; + + public LineDemarcator(final InputStream in, final Charset charset, final int maxDataSize, final int initialBufferSize) { + this(new InputStreamReader(in, charset), maxDataSize, initialBufferSize); + } + + public LineDemarcator(final Reader reader, final int maxDataSize, final int initialBufferSize) { + super(reader, maxDataSize, initialBufferSize); + } + + /** + * Will read the next line of text from the {@link InputStream} returning null + * when it reaches the end of the stream. + * + * @throws IOException if unable to read from the stream + */ + public String nextLine() throws IOException { + while (this.availableBytesLength != -1) { + if (this.index >= this.availableBytesLength) { + this.fill(); + } + + if (this.availableBytesLength != -1) { + char charVal; + int i; + for (i = this.index; i < this.availableBytesLength; i++) { + charVal = this.buffer[i]; + + try { + if (charVal == NEW_LINE) { + this.index = i + 1; + + final int size = this.index - this.mark; + final String line = new String(this.buffer, mark, size); + + this.mark = this.index; + return line; + } else if (lastChar == CARRIAGE_RETURN) { + // Point this.index to i+1 because that's the next byte that we want to consume. + this.index = i + 1; + + // Size is equal to where the line began, up to index-1 because we don't want to consume the last byte encountered. + final int size = this.index - 1 - this.mark; + final String line = new String(this.buffer, mark, size); + + // set 'mark' to index - 1 because we don't want to consume the last byte that we've encountered, since we're basing our + // line on the previous byte. + this.mark = this.index - 1; + return line; + } + } finally { + lastChar = charVal; + } + } + + this.index = i; + } else { + final int size = this.index - this.mark; + if (size == 0) { + return null; + } + + return new String(this.buffer, mark, size); + } + } + + return null; + } +} diff --git a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java index 1c9140b7d97d..7d2992f34a3d 100644 --- a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java +++ b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java @@ -17,12 +17,13 @@ package org.apache.nifi.util; import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; public class FormatUtils { - private static final String UNION = "|"; // for Data Sizes @@ -41,8 +42,9 @@ public class FormatUtils { private static final String WEEKS = join(UNION, "w", "wk", "wks", "week", "weeks"); private static final String VALID_TIME_UNITS = join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS); - public static final String TIME_DURATION_REGEX = "(\\d+)\\s*(" + VALID_TIME_UNITS + ")"; + public static final String TIME_DURATION_REGEX = "([\\d.]+)\\s*(" + VALID_TIME_UNITS + ")"; public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX); + private static final List TIME_UNIT_MULTIPLIERS = Arrays.asList(1000L, 1000L, 1000L, 60L, 60L, 24L); /** * Formats the specified count by adding commas. @@ -58,7 +60,7 @@ public static String formatCount(final long count) { * Formats the specified duration in 'mm:ss.SSS' format. * * @param sourceDuration the duration to format - * @param sourceUnit the unit to interpret the duration + * @param sourceUnit the unit to interpret the duration * @return representation of the given time data in minutes/seconds */ public static String formatMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) { @@ -79,7 +81,7 @@ public static String formatMinutesSeconds(final long sourceDuration, final TimeU * Formats the specified duration in 'HH:mm:ss.SSS' format. * * @param sourceDuration the duration to format - * @param sourceUnit the unit to interpret the duration + * @param sourceUnit the unit to interpret the duration * @return representation of the given time data in hours/minutes/seconds */ public static String formatHoursMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) { @@ -139,65 +141,230 @@ public static String formatDataSize(final double dataSize) { return format.format(dataSize) + " bytes"; } + /** + * Returns a time duration in the requested {@link TimeUnit} after parsing the {@code String} + * input. If the resulting value is a decimal (i.e. + * {@code 25 hours -> TimeUnit.DAYS = 1.04}), the value is rounded. + * + * @param value the raw String input (i.e. "28 minutes") + * @param desiredUnit the requested output {@link TimeUnit} + * @return the whole number value of this duration in the requested units + * @deprecated As of Apache NiFi 1.9.0, because this method only returns whole numbers, use {@link #getPreciseTimeDuration(String, TimeUnit)} when possible. + */ + @Deprecated public static long getTimeDuration(final String value, final TimeUnit desiredUnit) { + return Math.round(getPreciseTimeDuration(value, desiredUnit)); + } + + /** + * Returns the parsed and converted input in the requested units. + *

+ * If the value is {@code 0 <= x < 1} in the provided units, the units will first be converted to a smaller unit to get a value >= 1 (i.e. 0.5 seconds -> 500 milliseconds). + * This is because the underlying unit conversion cannot handle decimal values. + *

+ * If the value is {@code x >= 1} but x is not a whole number, the units will first be converted to a smaller unit to attempt to get a whole number value (i.e. 1.5 seconds -> 1500 milliseconds). + *

+ * If the value is {@code x < 1000} and the units are {@code TimeUnit.NANOSECONDS}, the result will be a whole number of nanoseconds, rounded (i.e. 123.4 ns -> 123 ns). + *

+ * This method handles decimal values over {@code 1 ns}, but {@code < 1 ns} will return {@code 0} in any other unit. + *

+ * Examples: + *

+ * "10 seconds", {@code TimeUnit.MILLISECONDS} -> 10_000.0 + * "0.010 s", {@code TimeUnit.MILLISECONDS} -> 10.0 + * "0.010 s", {@code TimeUnit.SECONDS} -> 0.010 + * "0.010 ns", {@code TimeUnit.NANOSECONDS} -> 1 + * "0.010 ns", {@code TimeUnit.MICROSECONDS} -> 0 + * + * @param value the {@code String} input + * @param desiredUnit the desired output {@link TimeUnit} + * @return the parsed and converted amount (without a unit) + */ + public static double getPreciseTimeDuration(final String value, final TimeUnit desiredUnit) { final Matcher matcher = TIME_DURATION_PATTERN.matcher(value.toLowerCase()); if (!matcher.matches()) { - throw new IllegalArgumentException("Value '" + value + "' is not a valid Time Duration"); + throw new IllegalArgumentException("Value '" + value + "' is not a valid time duration"); } final String duration = matcher.group(1); final String units = matcher.group(2); - TimeUnit specifiedTimeUnit = null; - switch (units.toLowerCase()) { + + double durationVal = Double.parseDouble(duration); + TimeUnit specifiedTimeUnit; + + // The TimeUnit enum doesn't have a value for WEEKS, so handle this case independently + if (isWeek(units)) { + specifiedTimeUnit = TimeUnit.DAYS; + durationVal *= 7; + } else { + specifiedTimeUnit = determineTimeUnit(units); + } + + // The units are now guaranteed to be in DAYS or smaller + long durationLong; + if (durationVal == Math.rint(durationVal)) { + durationLong = Math.round(durationVal); + } else { + // Try reducing the size of the units to make the input a long + List wholeResults = makeWholeNumberTime(durationVal, specifiedTimeUnit); + durationLong = (long) wholeResults.get(0); + specifiedTimeUnit = (TimeUnit) wholeResults.get(1); + } + + return desiredUnit.convert(durationLong, specifiedTimeUnit); + } + + /** + * Converts the provided time duration value to one that can be represented as a whole number. + * Returns a {@code List} containing the new value as a {@code long} at index 0 and the + * {@link TimeUnit} at index 1. If the incoming value is already whole, it is returned as is. + * If the incoming value cannot be made whole, a whole approximation is returned. For values + * {@code >= 1 TimeUnit.NANOSECONDS}, the value is rounded (i.e. 123.4 ns -> 123 ns). + * For values {@code < 1 TimeUnit.NANOSECONDS}, the constant [1L, {@code TimeUnit.NANOSECONDS}] is returned as the smallest measurable unit of time. + *

+ * Examples: + *

+ * 1, {@code TimeUnit.SECONDS} -> [1, {@code TimeUnit.SECONDS}] + * 1.1, {@code TimeUnit.SECONDS} -> [1100, {@code TimeUnit.MILLISECONDS}] + * 0.1, {@code TimeUnit.SECONDS} -> [100, {@code TimeUnit.MILLISECONDS}] + * 0.1, {@code TimeUnit.NANOSECONDS} -> [1, {@code TimeUnit.NANOSECONDS}] + * + * @param decimal the time duration as a decimal + * @param timeUnit the current time unit + * @return the time duration as a whole number ({@code long}) and the smaller time unit used + */ + protected static List makeWholeNumberTime(double decimal, TimeUnit timeUnit) { + // If the value is already a whole number, return it and the current time unit + if (decimal == Math.rint(decimal)) { + return Arrays.asList(new Object[]{(long) decimal, timeUnit}); + } else if (TimeUnit.NANOSECONDS == timeUnit) { + // The time unit is as small as possible + if (decimal < 1.0) { + decimal = 1; + } else { + decimal = Math.rint(decimal); + } + return Arrays.asList(new Object[]{(long) decimal, timeUnit}); + } else { + // Determine the next time unit and the respective multiplier + TimeUnit smallerTimeUnit = getSmallerTimeUnit(timeUnit); + long multiplier = calculateMultiplier(timeUnit, smallerTimeUnit); + + // Recurse with the original number converted to the smaller unit + return makeWholeNumberTime(decimal * multiplier, smallerTimeUnit); + } + } + + /** + * Returns the numerical multiplier to convert a value from {@code originalTimeUnit} to + * {@code newTimeUnit} (i.e. for {@code TimeUnit.DAYS -> TimeUnit.MINUTES} would return + * 24 * 60 = 1440). If the original and new units are the same, returns 1. If the new unit + * is larger than the original (i.e. the result would be less than 1), throws an + * {@link IllegalArgumentException}. + * + * @param originalTimeUnit the source time unit + * @param newTimeUnit the destination time unit + * @return the numerical multiplier between the units + */ + protected static long calculateMultiplier(TimeUnit originalTimeUnit, TimeUnit newTimeUnit) { + if (originalTimeUnit == newTimeUnit) { + return 1; + } else if (originalTimeUnit.ordinal() < newTimeUnit.ordinal()) { + throw new IllegalArgumentException("The original time unit '" + originalTimeUnit + "' must be larger than the new time unit '" + newTimeUnit + "'"); + } else { + int originalOrd = originalTimeUnit.ordinal(); + int newOrd = newTimeUnit.ordinal(); + + List unitMultipliers = TIME_UNIT_MULTIPLIERS.subList(newOrd, originalOrd); + return unitMultipliers.stream().reduce(1L, (a, b) -> (long) a * b); + } + } + + /** + * Returns the next smallest {@link TimeUnit} (i.e. {@code TimeUnit.DAYS -> TimeUnit.HOURS}). + * If the parameter is {@code null} or {@code TimeUnit.NANOSECONDS}, an + * {@link IllegalArgumentException} is thrown because there is no valid smaller TimeUnit. + * + * @param originalUnit the TimeUnit + * @return the next smaller TimeUnit + */ + protected static TimeUnit getSmallerTimeUnit(TimeUnit originalUnit) { + if (originalUnit == null || TimeUnit.NANOSECONDS == originalUnit) { + throw new IllegalArgumentException("Cannot determine a smaller time unit than '" + originalUnit + "'"); + } else { + return TimeUnit.values()[originalUnit.ordinal() - 1]; + } + } + + /** + * Returns {@code true} if this raw unit {@code String} is parsed as representing "weeks", which does not have a value in the {@link TimeUnit} enum. + * + * @param rawUnit the String containing the desired unit + * @return true if the unit is "weeks"; false otherwise + */ + protected static boolean isWeek(final String rawUnit) { + switch (rawUnit) { + case "w": + case "wk": + case "wks": + case "week": + case "weeks": + return true; + default: + return false; + } + } + + /** + * Returns the {@link TimeUnit} enum that maps to the provided raw {@code String} input. The + * highest time unit is {@code TimeUnit.DAYS}. Any input that cannot be parsed will result in + * an {@link IllegalArgumentException}. + * + * @param rawUnit the String to parse + * @return the TimeUnit + */ + protected static TimeUnit determineTimeUnit(String rawUnit) { + switch (rawUnit.toLowerCase()) { case "ns": case "nano": case "nanos": case "nanoseconds": - specifiedTimeUnit = TimeUnit.NANOSECONDS; - break; + return TimeUnit.NANOSECONDS; + case "µs": + case "micro": + case "micros": + case "microseconds": + return TimeUnit.MICROSECONDS; case "ms": case "milli": case "millis": case "milliseconds": - specifiedTimeUnit = TimeUnit.MILLISECONDS; - break; + return TimeUnit.MILLISECONDS; case "s": case "sec": case "secs": case "second": case "seconds": - specifiedTimeUnit = TimeUnit.SECONDS; - break; + return TimeUnit.SECONDS; case "m": case "min": case "mins": case "minute": case "minutes": - specifiedTimeUnit = TimeUnit.MINUTES; - break; + return TimeUnit.MINUTES; case "h": case "hr": case "hrs": case "hour": case "hours": - specifiedTimeUnit = TimeUnit.HOURS; - break; + return TimeUnit.HOURS; case "d": case "day": case "days": - specifiedTimeUnit = TimeUnit.DAYS; - break; - case "w": - case "wk": - case "wks": - case "week": - case "weeks": - final long durationVal = Long.parseLong(duration); - return desiredUnit.convert(durationVal, TimeUnit.DAYS)*7; + return TimeUnit.DAYS; + default: + throw new IllegalArgumentException("Could not parse '" + rawUnit + "' to TimeUnit"); } - - final long durationVal = Long.parseLong(duration); - return desiredUnit.convert(durationVal, specifiedTimeUnit); } public static String formatUtilization(final double utilization) { @@ -225,7 +392,7 @@ private static String join(final String delimiter, final String... values) { * 3 seconds, 8 millis, 3 nanos - if includeTotalNanos = false, * 3 seconds, 8 millis, 3 nanos (3008000003 nanos) - if includeTotalNanos = true * - * @param nanos the number of nanoseconds to format + * @param nanos the number of nanoseconds to format * @param includeTotalNanos whether or not to include the total number of nanoseconds in parentheses in the returned value * @return a human-readable String that is a formatted representation of the given number of nanoseconds. */ diff --git a/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/processor/TestFormatUtilsGroovy.groovy b/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/processor/TestFormatUtilsGroovy.groovy deleted file mode 100644 index f3e4f4640226..000000000000 --- a/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/processor/TestFormatUtilsGroovy.groovy +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.processor - -import org.apache.nifi.util.FormatUtils -import org.junit.After -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import java.util.concurrent.TimeUnit - -@RunWith(JUnit4.class) -class TestFormatUtilsGroovy extends GroovyTestCase { - private static final Logger logger = LoggerFactory.getLogger(TestFormatUtilsGroovy.class) - - @BeforeClass - public static void setUpOnce() throws Exception { - logger.metaClass.methodMissing = { String name, args -> - logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") - } - } - - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - /** - * New feature test - */ - @Test - void testShouldConvertWeeks() { - // Arrange - final List WEEKS = ["1 week", "1 wk", "1 w", "1 wks", "1 weeks"] - final long EXPECTED_DAYS = 7L - - // Act - List days = WEEKS.collect { String week -> - FormatUtils.getTimeDuration(week, TimeUnit.DAYS) - } - logger.converted(days) - - // Assert - assert days.every { it == EXPECTED_DAYS } - } - - - - @Test - void testShouldHandleNegativeWeeks() { - // Arrange - final List WEEKS = ["-1 week", "-1 wk", "-1 w", "-1 weeks", "- 1 week"] - - // Act - List msgs = WEEKS.collect { String week -> - shouldFail(IllegalArgumentException) { - FormatUtils.getTimeDuration(week, TimeUnit.DAYS) - } - } - - // Assert - assert msgs.every { it =~ /Value '.*' is not a valid Time Duration/ } - } - - - - /** - * Regression test - */ - @Test - void testShouldHandleInvalidAbbreviations() { - // Arrange - final List WEEKS = ["1 work", "1 wek", "1 k"] - - // Act - List msgs = WEEKS.collect { String week -> - shouldFail(IllegalArgumentException) { - FormatUtils.getTimeDuration(week, TimeUnit.DAYS) - } - } - - // Assert - assert msgs.every { it =~ /Value '.*' is not a valid Time Duration/ } - - } - - - /** - * New feature test - */ - @Test - void testShouldHandleNoSpaceInInput() { - // Arrange - final List WEEKS = ["1week", "1wk", "1w", "1wks", "1weeks"] - final long EXPECTED_DAYS = 7L - - // Act - List days = WEEKS.collect { String week -> - FormatUtils.getTimeDuration(week, TimeUnit.DAYS) - } - logger.converted(days) - - // Assert - assert days.every { it == EXPECTED_DAYS } - } -} \ No newline at end of file diff --git a/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/util/TestFormatUtilsGroovy.groovy b/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/util/TestFormatUtilsGroovy.groovy new file mode 100644 index 000000000000..8fc9d0c1937d --- /dev/null +++ b/nifi-commons/nifi-utils/src/test/groovy/org/apache/nifi/util/TestFormatUtilsGroovy.groovy @@ -0,0 +1,488 @@ +/* + * 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.util + + +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.TimeUnit + +@RunWith(JUnit4.class) +class TestFormatUtilsGroovy extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(TestFormatUtilsGroovy.class) + + @BeforeClass + static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() throws Exception { + + } + + @After + void tearDown() throws Exception { + + } + + /** + * New feature test + */ + @Test + void testGetTimeDurationShouldConvertWeeks() { + // Arrange + final List WEEKS = ["1 week", "1 wk", "1 w", "1 wks", "1 weeks"] + final long EXPECTED_DAYS = 7L + + // Act + List days = WEEKS.collect { String week -> + FormatUtils.getTimeDuration(week, TimeUnit.DAYS) + } + logger.converted(days) + + // Assert + assert days.every { it == EXPECTED_DAYS } + } + + + @Test + void testGetTimeDurationShouldHandleNegativeWeeks() { + // Arrange + final List WEEKS = ["-1 week", "-1 wk", "-1 w", "-1 weeks", "- 1 week"] + + // Act + List msgs = WEEKS.collect { String week -> + shouldFail(IllegalArgumentException) { + FormatUtils.getTimeDuration(week, TimeUnit.DAYS) + } + } + + // Assert + assert msgs.every { it =~ /Value '.*' is not a valid time duration/ } + } + + /** + * Regression test + */ + @Test + void testGetTimeDurationShouldHandleInvalidAbbreviations() { + // Arrange + final List WEEKS = ["1 work", "1 wek", "1 k"] + + // Act + List msgs = WEEKS.collect { String week -> + shouldFail(IllegalArgumentException) { + FormatUtils.getTimeDuration(week, TimeUnit.DAYS) + } + } + + // Assert + assert msgs.every { it =~ /Value '.*' is not a valid time duration/ } + + } + + /** + * New feature test + */ + @Test + void testGetTimeDurationShouldHandleNoSpaceInInput() { + // Arrange + final List WEEKS = ["1week", "1wk", "1w", "1wks", "1weeks"] + final long EXPECTED_DAYS = 7L + + // Act + List days = WEEKS.collect { String week -> + FormatUtils.getTimeDuration(week, TimeUnit.DAYS) + } + logger.converted(days) + + // Assert + assert days.every { it == EXPECTED_DAYS } + } + + /** + * New feature test + */ + @Test + void testGetTimeDurationShouldHandleDecimalValues() { + // Arrange + final List WHOLE_NUMBERS = ["10 ms", "10 millis", "10 milliseconds"] + final List DECIMAL_NUMBERS = ["0.010 s", "0.010 seconds"] + final long EXPECTED_MILLIS = 10 + + // Act + List parsedWholeMillis = WHOLE_NUMBERS.collect { String whole -> + FormatUtils.getTimeDuration(whole, TimeUnit.MILLISECONDS) + } + logger.converted(parsedWholeMillis) + + List parsedDecimalMillis = DECIMAL_NUMBERS.collect { String decimal -> + FormatUtils.getTimeDuration(decimal, TimeUnit.MILLISECONDS) + } + logger.converted(parsedDecimalMillis) + + // Assert + assert parsedWholeMillis.every { it == EXPECTED_MILLIS } + assert parsedDecimalMillis.every { it == EXPECTED_MILLIS } + } + + /** + * Regression test for custom week logic + */ + @Test + void testGetPreciseTimeDurationShouldHandleWeeks() { + // Arrange + final String ONE_WEEK = "1 week" + final Map ONE_WEEK_IN_OTHER_UNITS = [ + (TimeUnit.DAYS) : 7, + (TimeUnit.HOURS) : 7 * 24, + (TimeUnit.MINUTES) : 7 * 24 * 60, + (TimeUnit.SECONDS) : (long) 7 * 24 * 60 * 60, + (TimeUnit.MILLISECONDS): (long) 7 * 24 * 60 * 60 * 1000, + (TimeUnit.MICROSECONDS): (long) 7 * 24 * 60 * 60 * ((long) 1000 * 1000), + (TimeUnit.NANOSECONDS) : (long) 7 * 24 * 60 * 60 * ((long) 1000 * 1000 * 1000), + ] + + // Act + Map oneWeekInOtherUnits = TimeUnit.values()[0..<-1].collectEntries { TimeUnit destinationUnit -> + [destinationUnit, FormatUtils.getPreciseTimeDuration(ONE_WEEK, destinationUnit)] + } + logger.converted(oneWeekInOtherUnits) + + // Assert + oneWeekInOtherUnits.each { TimeUnit k, double value -> + assert value == ONE_WEEK_IN_OTHER_UNITS[k] + } + } + + /** + * Positive flow test for custom week logic with decimal value + */ + @Test + void testGetPreciseTimeDurationShouldHandleDecimalWeeks() { + // Arrange + final String ONE_AND_A_HALF_WEEKS = "1.5 week" + final Map ONE_POINT_FIVE_WEEKS_IN_OTHER_UNITS = [ + (TimeUnit.DAYS) : 7, + (TimeUnit.HOURS) : 7 * 24, + (TimeUnit.MINUTES) : 7 * 24 * 60, + (TimeUnit.SECONDS) : (long) 7 * 24 * 60 * 60, + (TimeUnit.MILLISECONDS): (long) 7 * 24 * 60 * 60 * 1000, + (TimeUnit.MICROSECONDS): (long) 7 * 24 * 60 * 60 * ((long) 1000 * 1000), + (TimeUnit.NANOSECONDS) : (long) 7 * 24 * 60 * 60 * ((long) 1000 * 1000 * 1000), + ].collectEntries { k, v -> [k, v * 1.5] } + + // Act + Map onePointFiveWeeksInOtherUnits = TimeUnit.values()[0..<-1].collectEntries { TimeUnit destinationUnit -> + [destinationUnit, FormatUtils.getPreciseTimeDuration(ONE_AND_A_HALF_WEEKS, destinationUnit)] + } + logger.converted(onePointFiveWeeksInOtherUnits) + + // Assert + onePointFiveWeeksInOtherUnits.each { TimeUnit k, double value -> + assert value == ONE_POINT_FIVE_WEEKS_IN_OTHER_UNITS[k] + } + } + + /** + * Positive flow test for decimal time inputs + */ + @Test + void testGetPreciseTimeDurationShouldHandleDecimalValues() { + // Arrange + final List WHOLE_NUMBERS = ["10 ms", "10 millis", "10 milliseconds"] + final List DECIMAL_NUMBERS = ["0.010 s", "0.010 seconds"] + final float EXPECTED_MILLIS = 10.0 + + // Act + List parsedWholeMillis = WHOLE_NUMBERS.collect { String whole -> + FormatUtils.getPreciseTimeDuration(whole, TimeUnit.MILLISECONDS) + } + logger.converted(parsedWholeMillis) + + List parsedDecimalMillis = DECIMAL_NUMBERS.collect { String decimal -> + FormatUtils.getPreciseTimeDuration(decimal, TimeUnit.MILLISECONDS) + } + logger.converted(parsedDecimalMillis) + + // Assert + assert parsedWholeMillis.every { it == EXPECTED_MILLIS } + assert parsedDecimalMillis.every { it == EXPECTED_MILLIS } + } + + /** + * Positive flow test for decimal inputs that are extremely small + */ + @Test + void testGetPreciseTimeDurationShouldHandleSmallDecimalValues() { + // Arrange + final Map SCENARIOS = [ + "decimalNanos" : [originalUnits: TimeUnit.NANOSECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 123.4, expectedValue: 123.0], + "lessThanOneNano" : [originalUnits: TimeUnit.NANOSECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 0.9, expectedValue: 1], + "lessThanOneNanoToMillis": [originalUnits: TimeUnit.NANOSECONDS, expectedUnits: TimeUnit.MILLISECONDS, originalValue: 0.9, expectedValue: 0], + "decimalMillisToNanos" : [originalUnits: TimeUnit.MILLISECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 123.4, expectedValue: 123_400_000], + ] + + // Act + Map results = SCENARIOS.collectEntries { String k, Map values -> + logger.debug("Evaluating ${k}: ${values}") + String input = "${values.originalValue} ${values.originalUnits.name()}" + [k, FormatUtils.getPreciseTimeDuration(input, values.expectedUnits)] + } + logger.info(results) + + // Assert + results.every { String key, double value -> + assert value == SCENARIOS[key].expectedValue + } + } + + /** + * Positive flow test for decimal inputs that can be converted (all equal values) + */ + @Test + void testMakeWholeNumberTimeShouldHandleDecimals() { + // Arrange + final List DECIMAL_TIMES = [ + [0.000_000_010, TimeUnit.SECONDS], + [0.000_010, TimeUnit.MILLISECONDS], + [0.010, TimeUnit.MICROSECONDS] + ] + final long EXPECTED_NANOS = 10L + + // Act + List parsedWholeNanos = DECIMAL_TIMES.collect { List it -> + FormatUtils.makeWholeNumberTime(it[0] as float, it[1] as TimeUnit) + } + logger.converted(parsedWholeNanos) + + // Assert + assert parsedWholeNanos.every { it == [EXPECTED_NANOS, TimeUnit.NANOSECONDS] } + } + + /** + * Positive flow test for decimal inputs that can be converted (metric values) + */ + @Test + void testMakeWholeNumberTimeShouldHandleMetricConversions() { + // Arrange + final Map SCENARIOS = [ + "secondsToMillis": [originalUnits: TimeUnit.SECONDS, expectedUnits: TimeUnit.MILLISECONDS, expectedValue: 123_400, originalValue: 123.4], + "secondsToMicros": [originalUnits: TimeUnit.SECONDS, expectedUnits: TimeUnit.MICROSECONDS, originalValue: 1.000_345, expectedValue: 1_000_345], + "millisToNanos" : [originalUnits: TimeUnit.MILLISECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 0.75, expectedValue: 750_000], + "nanosToNanosGE1": [originalUnits: TimeUnit.NANOSECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 123.4, expectedValue: 123], + "nanosToNanosLE1": [originalUnits: TimeUnit.NANOSECONDS, expectedUnits: TimeUnit.NANOSECONDS, originalValue: 0.123, expectedValue: 1], + ] + + // Act + Map results = SCENARIOS.collectEntries { String k, Map values -> + logger.debug("Evaluating ${k}: ${values}") + [k, FormatUtils.makeWholeNumberTime(values.originalValue, values.originalUnits)] + } + logger.info(results) + + // Assert + results.every { String key, List values -> + assert values.first() == SCENARIOS[key].expectedValue + assert values.last() == SCENARIOS[key].expectedUnits + } + } + + /** + * Positive flow test for decimal inputs that can be converted (non-metric values) + */ + @Test + void testMakeWholeNumberTimeShouldHandleNonMetricConversions() { + // Arrange + final Map SCENARIOS = [ + "daysToHours" : [originalUnits: TimeUnit.DAYS, expectedUnits: TimeUnit.HOURS, expectedValue: 36, originalValue: 1.5], + "hoursToMinutes" : [originalUnits: TimeUnit.HOURS, expectedUnits: TimeUnit.MINUTES, originalValue: 1.5, expectedValue: 90], + "hoursToMinutes2": [originalUnits: TimeUnit.HOURS, expectedUnits: TimeUnit.MINUTES, originalValue: 0.75, expectedValue: 45], + ] + + // Act + Map results = SCENARIOS.collectEntries { String k, Map values -> + logger.debug("Evaluating ${k}: ${values}") + [k, FormatUtils.makeWholeNumberTime(values.originalValue, values.originalUnits)] + } + logger.info(results) + + // Assert + results.every { String key, List values -> + assert values.first() == SCENARIOS[key].expectedValue + assert values.last() == SCENARIOS[key].expectedUnits + } + } + + /** + * Positive flow test for whole inputs + */ + @Test + void testMakeWholeNumberTimeShouldHandleWholeNumbers() { + // Arrange + final List WHOLE_TIMES = [ + [10.0, TimeUnit.DAYS], + [10.0, TimeUnit.HOURS], + [10.0, TimeUnit.MINUTES], + [10.0, TimeUnit.SECONDS], + [10.0, TimeUnit.MILLISECONDS], + [10.0, TimeUnit.MICROSECONDS], + [10.0, TimeUnit.NANOSECONDS], + ] + + // Act + List parsedWholeTimes = WHOLE_TIMES.collect { List it -> + FormatUtils.makeWholeNumberTime(it[0] as float, it[1] as TimeUnit) + } + logger.converted(parsedWholeTimes) + + // Assert + parsedWholeTimes.eachWithIndex { List elements, int i -> + assert elements[0] instanceof Long + assert elements[0] == 10L + assert elements[1] == WHOLE_TIMES[i][1] + } + } + + /** + * Negative flow test for nanosecond inputs (regardless of value, the unit cannot be converted) + */ + @Test + void testMakeWholeNumberTimeShouldHandleNanoseconds() { + // Arrange + final List WHOLE_TIMES = [ + [1100.0, TimeUnit.NANOSECONDS], + [2.1, TimeUnit.NANOSECONDS], + [1.0, TimeUnit.NANOSECONDS], + [0.1, TimeUnit.NANOSECONDS], + ] + + final List EXPECTED_TIMES = [ + [1100L, TimeUnit.NANOSECONDS], + [2L, TimeUnit.NANOSECONDS], + [1L, TimeUnit.NANOSECONDS], + [1L, TimeUnit.NANOSECONDS], + ] + + // Act + List parsedWholeTimes = WHOLE_TIMES.collect { List it -> + FormatUtils.makeWholeNumberTime(it[0] as float, it[1] as TimeUnit) + } + logger.converted(parsedWholeTimes) + + // Assert + assert parsedWholeTimes == EXPECTED_TIMES + } + + /** + * Positive flow test for whole inputs + */ + @Test + void testShouldGetSmallerTimeUnit() { + // Arrange + final List UNITS = TimeUnit.values() as List + + // Act + def nullMsg = shouldFail(IllegalArgumentException) { + FormatUtils.getSmallerTimeUnit(null) + } + logger.expected(nullMsg) + + def nanosMsg = shouldFail(IllegalArgumentException) { + FormatUtils.getSmallerTimeUnit(TimeUnit.NANOSECONDS) + } + logger.expected(nanosMsg) + + List smallerTimeUnits = UNITS[1..-1].collect { TimeUnit unit -> + FormatUtils.getSmallerTimeUnit(unit) + } + logger.converted(smallerTimeUnits) + + // Assert + assert nullMsg == "Cannot determine a smaller time unit than 'null'" + assert nanosMsg == "Cannot determine a smaller time unit than 'NANOSECONDS'" + assert smallerTimeUnits == UNITS[0..<-1] + } + + /** + * Positive flow test for multipliers based on valid time units + */ + @Test + void testShouldCalculateMultiplier() { + // Arrange + final Map SCENARIOS = [ + "allUnits" : [original: TimeUnit.DAYS, destination: TimeUnit.NANOSECONDS, expectedMultiplier: (long) 24 * 60 * 60 * (long) 1_000_000_000], + "microsToNanos" : [original: TimeUnit.MICROSECONDS, destination: TimeUnit.NANOSECONDS, expectedMultiplier: 1_000], + "millisToNanos" : [original: TimeUnit.MILLISECONDS, destination: TimeUnit.NANOSECONDS, expectedMultiplier: 1_000_000], + "millisToMicros": [original: TimeUnit.MILLISECONDS, destination: TimeUnit.MICROSECONDS, expectedMultiplier: 1_000], + "daysToHours" : [original: TimeUnit.DAYS, destination: TimeUnit.HOURS, expectedMultiplier: 24], + "daysToSeconds" : [original: TimeUnit.DAYS, destination: TimeUnit.SECONDS, expectedMultiplier: 24 * 60 * 60], + ] + + // Act + Map results = SCENARIOS.collectEntries { String k, Map values -> + logger.debug("Evaluating ${k}: ${values}") + [k, FormatUtils.calculateMultiplier(values.original, values.destination)] + } + logger.converted(results) + + // Assert + results.every { String key, long value -> + assert value == SCENARIOS[key].expectedMultiplier + } + } + + /** + * Negative flow test for multipliers based on incorrectly-ordered time units + */ + @Test + void testCalculateMultiplierShouldHandleIncorrectUnits() { + // Arrange + final Map SCENARIOS = [ + "allUnits" : [original: TimeUnit.NANOSECONDS, destination: TimeUnit.DAYS], + "nanosToMicros": [original: TimeUnit.NANOSECONDS, destination: TimeUnit.MICROSECONDS], + "hoursToDays" : [original: TimeUnit.HOURS, destination: TimeUnit.DAYS], + ] + + // Act + Map results = SCENARIOS.collectEntries { String k, Map values -> + logger.debug("Evaluating ${k}: ${values}") + def msg = shouldFail(IllegalArgumentException) { + FormatUtils.calculateMultiplier(values.original, values.destination) + } + logger.expected(msg) + [k, msg] + } + + // Assert + results.every { String key, String value -> + assert value =~ "The original time unit '.*' must be larger than the new time unit '.*'" + } + } + + // TODO: Microsecond parsing +} \ No newline at end of file diff --git a/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/stream/io/util/TestLineDemarcator.java b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/stream/io/util/TestLineDemarcator.java new file mode 100644 index 000000000000..85cf85ebf96e --- /dev/null +++ b/nifi-commons/nifi-utils/src/test/java/org/apache/nifi/stream/io/util/TestLineDemarcator.java @@ -0,0 +1,148 @@ +/* + * 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.stream.io.util; + +import org.apache.nifi.stream.io.RepeatingInputStream; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +public class TestLineDemarcator { + + @Test + public void testSingleCharacterLines() throws IOException { + final String input = "A\nB\nC\rD\r\nE\r\nF\r\rG"; + + final List lines = getLines(input); + assertEquals(Arrays.asList("A\n", "B\n", "C\r", "D\r\n", "E\r\n", "F\r", "\r", "G"), lines); + } + + + @Test + public void testEmptyStream() throws IOException { + final List lines = getLines(""); + assertEquals(Collections.emptyList(), lines); + } + + @Test + public void testOnlyEmptyLines() throws IOException { + final String input = "\r\r\r\n\n\n\r\n"; + + final List lines = getLines(input); + assertEquals(Arrays.asList("\r", "\r", "\r\n", "\n", "\n", "\r\n"), lines); + } + + @Test + public void testOnBufferSplit() throws IOException { + final String input = "ABC\r\nXYZ"; + final List lines = getLines(input, 10, 4); + + assertEquals(Arrays.asList("ABC\r\n", "XYZ"), lines); + } + + @Test + public void testEndsWithCarriageReturn() throws IOException { + final List lines = getLines("ABC\r"); + assertEquals(Arrays.asList("ABC\r"), lines); + } + + @Test + public void testEndsWithNewLine() throws IOException { + final List lines = getLines("ABC\n"); + assertEquals(Arrays.asList("ABC\n"), lines); + } + + @Test + public void testEndsWithCarriageReturnNewLine() throws IOException { + final List lines = getLines("ABC\r\n"); + assertEquals(Arrays.asList("ABC\r\n"), lines); + } + + @Test + public void testReadAheadInIsEol() throws IOException { + final String input = "he\ra-to-a\rb-to-b\rc-to-c\r\nd-to-d"; + final List lines = getLines(input, 10, 10); + + assertEquals(Arrays.asList("he\r", "a-to-a\r", "b-to-b\r", "c-to-c\r\n", "d-to-d"), lines); + } + + @Test + public void testFirstCharMatchOnly() throws IOException { + final List lines = getLines("\nThe quick brown fox jumped over the lazy dog."); + assertEquals(Arrays.asList("\n", "The quick brown fox jumped over the lazy dog."), lines); + } + + @Test + @Ignore("Intended only for manual testing. While this can take a while to run, it can be very helpful for manual testing before and after a change to the class. However, we don't want this to " + + "run in automated tests because we have no way to compare from one run to another, so it will only slow down automated tests.") + public void testPerformance() throws IOException { + final String lines = "The\nquick\nbrown\nfox\njumped\nover\nthe\nlazy\ndog.\r\n\n"; + final byte[] bytes = lines.getBytes(StandardCharsets.UTF_8); + + for (int i=0; i < 100; i++) { + final long start = System.nanoTime(); + + long count = 0; + try (final InputStream in = new RepeatingInputStream(bytes, 1_000_000); + final LineDemarcator demarcator = new LineDemarcator(in, StandardCharsets.UTF_8, 8192, 8192)) { + + while (demarcator.nextLine() != null) { + count++; + } + } + + final long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + System.out.println("Took " + millis + " millis to demarcate " + count + " lines"); + } + } + + private List getLines(final String text) throws IOException { + return getLines(text, 8192, 8192); + } + + private List getLines(final String text, final int maxDataSize, final int bufferSize) throws IOException { + final byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + + final List lines = new ArrayList<>(); + + try (final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + final Reader reader = new InputStreamReader(bais, StandardCharsets.UTF_8); + final LineDemarcator demarcator = new LineDemarcator(reader, maxDataSize, bufferSize)) { + + String line; + while ((line = demarcator.nextLine()) != null) { + lines.add(line); + } + } + + return lines; + } + +} diff --git a/nifi-commons/nifi-web-utils/pom.xml b/nifi-commons/nifi-web-utils/pom.xml index 4d38aad23491..56688958d88e 100644 --- a/nifi-commons/nifi-web-utils/pom.xml +++ b/nifi-commons/nifi-web-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-web-utils @@ -29,7 +29,7 @@ org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT commons-codec diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java index 90a83a96f689..fbf5c1948ac6 100644 --- a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java +++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Stream; import javax.net.ssl.SSLContext; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; @@ -45,6 +46,7 @@ public final class WebUtils { private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"; private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"; + private static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix"; private WebUtils() { } @@ -199,7 +201,8 @@ public static String sanitizeContextPath(ServletRequest request, String whitelis } /** - * Determines the context path if populated in {@code X-ProxyContextPath} or {@code X-ForwardContext} headers. If not populated, returns an empty string. + * Determines the context path if populated in {@code X-ProxyContextPath}, {@code X-ForwardContext}, + * or {@code X-Forwarded-Prefix} headers. If not populated, returns an empty string. * * @param request the HTTP request * @return the provided context path or an empty string @@ -208,18 +211,20 @@ public static String determineContextPath(HttpServletRequest request) { String contextPath = request.getContextPath(); String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER); String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER); + String prefix = request.getHeader(FORWARDED_PREFIX_HTTP_HEADER); logger.debug("Context path: " + contextPath); String determinedContextPath = ""; - // If either header is set, log both - if (anyNotBlank(proxyContextPath, forwardedContext)) { + // If a context path header is set, log each + if (anyNotBlank(proxyContextPath, forwardedContext, prefix)) { logger.debug(String.format("On the request, the following context paths were parsed" + - " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s", - proxyContextPath, forwardedContext)); + " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s\n\tX-Forwarded-Prefix: %s", + proxyContextPath, forwardedContext, prefix)); - // Implementing preferred order here: PCP, FCP - determinedContextPath = StringUtils.isNotBlank(proxyContextPath) ? proxyContextPath : forwardedContext; + // Implementing preferred order here: PCP, FC, FP + determinedContextPath = Stream.of(proxyContextPath, forwardedContext, prefix) + .filter(StringUtils::isNotBlank).findFirst().orElse(""); } logger.debug("Determined context path: " + determinedContextPath); diff --git a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy index b0a119140eb2..6b68457a9349 100644 --- a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy +++ b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy @@ -32,10 +32,10 @@ import sun.security.x509.X500Name import javax.net.ssl.SSLPeerUnverifiedException import javax.servlet.http.HttpServletRequest import javax.ws.rs.core.UriBuilderException -import javax.ws.rs.client.Client; +import javax.ws.rs.client.Client import javax.net.ssl.SSLContext -import javax.net.ssl.HostnameVerifier; -import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier +import java.security.cert.X509Certificate @RunWith(JUnit4.class) @@ -44,9 +44,10 @@ class WebUtilsTest extends GroovyTestCase { static final String PCP_HEADER = "X-ProxyContextPath" static final String FC_HEADER = "X-Forwarded-Context" + static final String FP_HEADER = "X-Forwarded-Prefix" static final String WHITELISTED_PATH = "/some/context/path" - private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request"; + private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request" @BeforeClass static void setUpOnce() throws Exception { @@ -78,6 +79,9 @@ class WebUtilsTest extends GroovyTestCase { case FC_HEADER: return keys["forward"] break + case FP_HEADER: + return keys["prefix"] + break default: return "" } @@ -94,8 +98,12 @@ class WebUtilsTest extends GroovyTestCase { // Variety of requests with different ordering of context paths (the correct one is always "some/context/path" HttpServletRequest proxyRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH]) HttpServletRequest forwardedRequest = mockRequest([forward: CORRECT_CONTEXT_PATH]) + HttpServletRequest prefixRequest = mockRequest([prefix: CORRECT_CONTEXT_PATH]) HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, forward: WRONG_CONTEXT_PATH]) - List requests = [proxyRequest, forwardedRequest, proxyBeforeForwardedRequest] + HttpServletRequest proxyBeforePrefixRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH]) + HttpServletRequest forwardBeforePrefixRequest = mockRequest([forward: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH]) + List requests = [proxyRequest, forwardedRequest, prefixRequest, proxyBeforeForwardedRequest, + proxyBeforePrefixRequest, forwardBeforePrefixRequest] // Act requests.each { HttpServletRequest request -> @@ -117,8 +125,12 @@ class WebUtilsTest extends GroovyTestCase { HttpServletRequest proxySpacesRequest = mockRequest([proxy: " "]) HttpServletRequest forwardedRequest = mockRequest([forward: ""]) HttpServletRequest forwardedSpacesRequest = mockRequest([forward: " "]) - HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: "", forward: ""]) - List requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, proxyBeforeForwardedRequest] + HttpServletRequest prefixRequest = mockRequest([prefix: ""]) + HttpServletRequest prefixSpacesRequest = mockRequest([prefix: " "]) + HttpServletRequest proxyBeforeForwardedOrPrefixRequest = mockRequest([proxy: "", forward: "", prefix: ""]) + HttpServletRequest proxyBeforeForwardedOrPrefixSpacesRequest = mockRequest([proxy: " ", forward: " ", prefix: " "]) + List requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, prefixRequest, prefixSpacesRequest, + proxyBeforeForwardedOrPrefixRequest, proxyBeforeForwardedOrPrefixSpacesRequest] // Act requests.each { HttpServletRequest request -> @@ -156,7 +168,9 @@ class WebUtilsTest extends GroovyTestCase { HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "any/context/path"]) HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "any/context/path", forward: "any/other/context/path"]) - List requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader] + HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "any/context/path", forward: "any/other/context/path", + prefix: "any/other/prefix/path"]) + List requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader, requestWithProxyAndForwardAndPrefixHeader] // Act requests.each { HttpServletRequest request -> @@ -179,7 +193,10 @@ class WebUtilsTest extends GroovyTestCase { HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"]) HttpServletRequest requestWithForwardHeader = mockRequest([forward: "some/context/path"]) HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path"]) - List requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader] + HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path", + prefix: "any/other/prefix/path"]) + List requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader, + requestWithProxyAndForwardAndPrefixHeader] // Act requests.each { HttpServletRequest request -> @@ -194,15 +211,19 @@ class WebUtilsTest extends GroovyTestCase { @Test void testGetResourcePathShouldAllowContextPathHeaderIfElementInMultipleWhitelist() throws Exception { // Arrange - String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path"].join(",") + String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path", "/a/prefix/path"].join(",") logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}") final List VALID_RESOURCE_PATHS = multipleWhitelistedPaths.split(",").collect { "$it/actualResource" } HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"]) HttpServletRequest requestWithForwardHeader = mockRequest([forward: "another/path"]) + HttpServletRequest requestWithPrefixHeader = mockRequest([prefix: "a/prefix/path"]) HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "a/third/path", forward: "any/other/context/path"]) - List requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader] + HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "a/third/path", forward: "any/other/context/path", + prefix: "any/other/prefix/path"]) + List requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader, + requestWithPrefixHeader, requestWithProxyAndForwardAndPrefixHeader] // Act requests.each { HttpServletRequest request -> diff --git a/nifi-commons/nifi-write-ahead-log/pom.xml b/nifi-commons/nifi-write-ahead-log/pom.xml index ede5d3116e4b..fa08064e954a 100644 --- a/nifi-commons/nifi-write-ahead-log/pom.xml +++ b/nifi-commons/nifi-write-ahead-log/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-commons - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-write-ahead-log jar @@ -30,7 +30,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/LengthDelimitedJournal.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/LengthDelimitedJournal.java index c10d3669f03e..d9fdc97fea55 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/LengthDelimitedJournal.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/LengthDelimitedJournal.java @@ -39,15 +39,19 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.file.Files; import java.text.DecimalFormat; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.UUID; public class LengthDelimitedJournal implements WriteAheadJournal { private static final Logger logger = LoggerFactory.getLogger(LengthDelimitedJournal.class); + private static final int DEFAULT_MAX_IN_HEAP_SERIALIZATION_BYTES = 5 * 1024 * 1024; // 5 MB + private static final JournalSummary INACTIVE_JOURNAL_SUMMARY = new StandardJournalSummary(-1L, -1L, 0); private static final int JOURNAL_ENCODING_VERSION = 1; private static final byte TRANSACTION_FOLLOWS = 64; @@ -55,9 +59,11 @@ public class LengthDelimitedJournal implements WriteAheadJournal { private static final int NUL_BYTE = 0; private final File journalFile; + private final File overflowDirectory; private final long initialTransactionId; private final SerDeFactory serdeFactory; private final ObjectPool streamPool; + private final int maxInHeapSerializationBytes; private SerDe serde; private FileOutputStream fileOut; @@ -72,13 +78,56 @@ public class LengthDelimitedJournal implements WriteAheadJournal { private final ByteBuffer transactionPreamble = ByteBuffer.allocate(12); // guarded by synchronized block public LengthDelimitedJournal(final File journalFile, final SerDeFactory serdeFactory, final ObjectPool streamPool, final long initialTransactionId) { + this(journalFile, serdeFactory, streamPool, initialTransactionId, DEFAULT_MAX_IN_HEAP_SERIALIZATION_BYTES); + } + + public LengthDelimitedJournal(final File journalFile, final SerDeFactory serdeFactory, final ObjectPool streamPool, final long initialTransactionId, + final int maxInHeapSerializationBytes) { this.journalFile = journalFile; + this.overflowDirectory = new File(journalFile.getParentFile(), "overflow-" + getBaseFilename(journalFile)); this.serdeFactory = serdeFactory; this.serde = serdeFactory.createSerDe(null); this.streamPool = streamPool; this.initialTransactionId = initialTransactionId; this.currentTransactionId = initialTransactionId; + this.maxInHeapSerializationBytes = maxInHeapSerializationBytes; + } + + public void dispose() { + logger.debug("Deleting Journal {} because it is now encapsulated in the latest Snapshot", journalFile.getName()); + if (!journalFile.delete() && journalFile.exists()) { + logger.warn("Unable to delete expired journal file " + journalFile + "; this file should be deleted manually."); + } + + if (overflowDirectory.exists()) { + final File[] overflowFiles = overflowDirectory.listFiles(); + if (overflowFiles == null) { + logger.warn("Unable to obtain listing of files that exist in 'overflow directory' " + overflowDirectory + + " - this directory and any files within it can now be safely removed manually"); + return; + } + + for (final File overflowFile : overflowFiles) { + if (!overflowFile.delete() && overflowFile.exists()) { + logger.warn("After expiring journal file " + journalFile + ", unable to remove 'overflow file' " + overflowFile + " - this file should be removed manually"); + } + } + + if (!overflowDirectory.delete()) { + logger.warn("After expiring journal file " + journalFile + ", unable to remove 'overflow directory' " + overflowDirectory + " - this file should be removed manually"); + } + } + } + + private static String getBaseFilename(final File file) { + final String name = file.getName(); + final int index = name.lastIndexOf("."); + if (index < 0) { + return name; + } + + return name.substring(0, index); } private synchronized OutputStream getOutputStream() throws FileNotFoundException { @@ -181,12 +230,64 @@ public void update(final Collection records, final RecordLookup recordLook checkState(); + File overflowFile = null; final ByteArrayDataOutputStream bados = streamPool.borrowObject(); + try { - for (final T record : records) { - final Object recordId = serde.getRecordIdentifier(record); - final T previousRecordState = recordLookup.lookup(recordId); - serde.serializeEdit(previousRecordState, record, bados.getDataOutputStream()); + FileOutputStream overflowFileOut = null; + + try { + DataOutputStream dataOut = bados.getDataOutputStream(); + for (final T record : records) { + final Object recordId = serde.getRecordIdentifier(record); + final T previousRecordState = recordLookup.lookup(recordId); + serde.serializeEdit(previousRecordState, record, dataOut); + + final int size = bados.getByteArrayOutputStream().size(); + if (serde.isWriteExternalFileReferenceSupported() && size > maxInHeapSerializationBytes) { + if (!overflowDirectory.exists()) { + Files.createDirectory(overflowDirectory.toPath()); + } + + // If we have exceeded our threshold for how much to serialize in memory, + // flush the in-memory representation to an 'overflow file' and then update + // the Data Output Stream that is used to write to the file also. + overflowFile = new File(overflowDirectory, UUID.randomUUID().toString()); + logger.debug("Length of update with {} records exceeds in-memory max of {} bytes. Overflowing to {}", records.size(), maxInHeapSerializationBytes, overflowFile); + + overflowFileOut = new FileOutputStream(overflowFile); + bados.getByteArrayOutputStream().writeTo(overflowFileOut); + bados.getByteArrayOutputStream().reset(); + + // change dataOut to point to the File's Output Stream so that all subsequent records are written to the file. + dataOut = new DataOutputStream(new BufferedOutputStream(overflowFileOut)); + + // We now need to write to the ByteArrayOutputStream a pointer to the overflow file + // so that what is written to the actual journal is that pointer. + serde.writeExternalFileReference(overflowFile, bados.getDataOutputStream()); + } + } + + dataOut.flush(); + + // If we overflowed to an external file, we need to be sure that we sync to disk before + // updating the Journal. Otherwise, we could get to a state where the Journal was flushed to disk without the + // external file being flushed. This would result in a missed update to the FlowFile Repository. + if (overflowFileOut != null) { + if (logger.isDebugEnabled()) { // avoid calling File.length() if not necessary + logger.debug("Length of update to overflow file is {} bytes", overflowFile.length()); + } + + overflowFileOut.getFD().sync(); + } + } finally { + if (overflowFileOut != null) { + try { + overflowFileOut.close(); + } catch (final Exception e) { + logger.warn("Failed to close open file handle to overflow file {}", overflowFile, e); + } + } } final ByteArrayOutputStream baos = bados.getByteArrayOutputStream(); @@ -210,12 +311,20 @@ public void update(final Collection records, final RecordLookup recordLook logger.debug("Wrote Transaction {} to journal {} with length {} and {} records", transactionId, journalFile, baos.size(), records.size()); } catch (final Throwable t) { poison(t); + + if (overflowFile != null) { + if (!overflowFile.delete() && overflowFile.exists()) { + logger.warn("Failed to cleanup temporary overflow file " + overflowFile + " - this file should be cleaned up manually."); + } + } + throw t; } finally { streamPool.returnObject(bados); } } + private void checkState() throws IOException { if (poisoned) { throw new IOException("Cannot update journal file " + journalFile + " because this journal has already encountered a failure when attempting to write to the file. " @@ -335,7 +444,7 @@ public JournalRecovery recoverRecords(final Map recordMap, final Set< final ByteCountingInputStream transactionByteCountingIn = new ByteCountingInputStream(transactionLimitingIn); final DataInputStream transactionDis = new DataInputStream(transactionByteCountingIn); - while (transactionByteCountingIn.getBytesConsumed() < transactionLength) { + while (transactionByteCountingIn.getBytesConsumed() < transactionLength || serde.isMoreInExternalFile()) { final T record = serde.deserializeEdit(transactionDis, recordMap, serdeAndVersion.getVersion()); // Update our RecordMap so that we have the most up-to-date version of the Record. diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/SequentialAccessWriteAheadLog.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/SequentialAccessWriteAheadLog.java index cba5184ea8c6..11eb31cdd38a 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/SequentialAccessWriteAheadLog.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/SequentialAccessWriteAheadLog.java @@ -300,10 +300,8 @@ public int checkpoint() throws IOException { snapshot.writeSnapshot(snapshotCapture); for (final File existingJournal : existingJournals) { - logger.debug("Deleting Journal {} because it is now encapsulated in the latest Snapshot", existingJournal.getName()); - if (!existingJournal.delete() && existingJournal.exists()) { - logger.warn("Unable to delete expired journal file " + existingJournal + "; this file should be deleted manually."); - } + final WriteAheadJournal journal = new LengthDelimitedJournal<>(existingJournal, serdeFactory, streamPool, nextTransactionId); + journal.dispose(); } final long totalNanos = System.nanoTime() - startNanos; diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/WriteAheadJournal.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/WriteAheadJournal.java index c44e1cb2377a..d4fb6cbed474 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/WriteAheadJournal.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/apache/nifi/wali/WriteAheadJournal.java @@ -53,4 +53,9 @@ public interface WriteAheadJournal extends Closeable { * @return true if the journal is healthy and can be written to, false if either the journal has been closed or is poisoned */ boolean isHealthy(); + + /** + * Destroys any resources that the journal occupies + */ + void dispose(); } diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java index eabac9dc80cc..8db2d878856d 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java @@ -1103,6 +1103,9 @@ public Set recoverNextTransaction(final Map currentRecordMap, final S record; try { record = serde.deserializeEdit(recoveryIn, currentRecordMap, recoveryVersion); + if (record == null) { + throw new EOFException(); + } } catch (final EOFException eof) { throw eof; } catch (final Exception e) { diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/SerDe.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/SerDe.java index d1919e7f4c0f..356cf8457338 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/SerDe.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/SerDe.java @@ -18,6 +18,7 @@ import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.util.Map; @@ -151,4 +152,37 @@ default void readHeader(DataInputStream in) throws IOException { */ default void close() throws IOException { } + + /** + * Optional method that a SerDe can support that indicates that the contents of the next update should be found + * in the given external File. + * + * @param externalFile the file that contains the update information + * @param out the DataOutputStream to write the external file reference to + * @throws IOException if unable to write the update + * @throws UnsupportedOperationException if this SerDe does not support this operation + */ + default void writeExternalFileReference(File externalFile, DataOutputStream out) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * Indicates whether or not a call to {@link #writeExternalFileReference(File, DataOutputStream)} is valid for this implementation + * @return true if calls to {@link #writeExternalFileReference(File, DataOutputStream)} are supported, false if calling + * the method will result in an {@link UnsupportedOperationException} being thrown. + */ + default boolean isWriteExternalFileReferenceSupported() { + return false; + } + + /** + * If the last call to read data from this SerDe resulted in data being read from an External File, and there is more data in that External File, + * then this method will return true. Otherwise, it will return false. + * + * @return true if more data available in External File, false otherwise. + * @throws IOException if unable to read from External File to determine data availability + */ + default boolean isMoreInExternalFile() throws IOException { + return false; + } } diff --git a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/WriteAheadRepository.java b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/WriteAheadRepository.java index 7f0e8281e316..05fc8a57cd1d 100644 --- a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/WriteAheadRepository.java +++ b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/WriteAheadRepository.java @@ -89,7 +89,7 @@ public interface WriteAheadRepository { *

* Recovers all External Swap locations that were persisted. If this method * is to be called, it must be called AFTER {@link #recoverRecords()} and - * BEFORE {@link update}. + * BEFORE {@link #update(Collection, boolean)}}. *

* * @return swap location diff --git a/nifi-commons/nifi-write-ahead-log/src/test/java/org/apache/nifi/wali/TestSequentialAccessWriteAheadLog.java b/nifi-commons/nifi-write-ahead-log/src/test/java/org/apache/nifi/wali/TestSequentialAccessWriteAheadLog.java index 4fc0fe794519..6d24445093dc 100644 --- a/nifi-commons/nifi-write-ahead-log/src/test/java/org/apache/nifi/wali/TestSequentialAccessWriteAheadLog.java +++ b/nifi-commons/nifi-write-ahead-log/src/test/java/org/apache/nifi/wali/TestSequentialAccessWriteAheadLog.java @@ -17,10 +17,17 @@ package org.apache.nifi.wali; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.wali.DummyRecord; +import org.wali.DummyRecordSerde; +import org.wali.SerDeFactory; +import org.wali.SingletonSerDeFactory; +import org.wali.UpdateType; +import org.wali.WriteAheadRepository; import java.io.File; import java.io.IOException; @@ -38,22 +45,69 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; -import org.wali.DummyRecord; -import org.wali.DummyRecordSerde; -import org.wali.SerDeFactory; -import org.wali.SingletonSerDeFactory; -import org.wali.UpdateType; -import org.wali.WriteAheadRepository; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class TestSequentialAccessWriteAheadLog { @Rule public TestName testName = new TestName(); + + @Test + public void testUpdateWithExternalFile() throws IOException { + final DummyRecordSerde serde = new DummyRecordSerde(); + final SequentialAccessWriteAheadLog repo = createWriteRepo(serde); + + final List records = new ArrayList<>(); + for (int i = 0; i < 350_000; i++) { + final DummyRecord record = new DummyRecord(String.valueOf(i), UpdateType.CREATE); + records.add(record); + } + + repo.update(records, false); + repo.shutdown(); + + assertEquals(1, serde.getExternalFileReferences().size()); + + final SequentialAccessWriteAheadLog recoveryRepo = createRecoveryRepo(); + final Collection recovered = recoveryRepo.recoverRecords(); + + // ensure that we get the same records back, but the order may be different, so wrap both collections + // in a HashSet so that we can compare unordered collections of the same type. + assertEquals(new HashSet<>(records), new HashSet<>(recovered)); + } + + @Test + public void testUpdateWithExternalFileFollowedByInlineUpdate() throws IOException { + final DummyRecordSerde serde = new DummyRecordSerde(); + final SequentialAccessWriteAheadLog repo = createWriteRepo(serde); + + final List records = new ArrayList<>(); + for (int i = 0; i < 350_000; i++) { + final DummyRecord record = new DummyRecord(String.valueOf(i), UpdateType.CREATE); + records.add(record); + } + + repo.update(records, false); + + final DummyRecord subsequentRecord = new DummyRecord("350001", UpdateType.CREATE); + repo.update(Collections.singleton(subsequentRecord), false); + repo.shutdown(); + + assertEquals(1, serde.getExternalFileReferences().size()); + + final SequentialAccessWriteAheadLog recoveryRepo = createRecoveryRepo(); + final Collection recovered = recoveryRepo.recoverRecords(); + + // ensure that we get the same records back, but the order may be different, so wrap both collections + // in a HashSet so that we can compare unordered collections of the same type. + final Set expectedRecords = new HashSet<>(records); + expectedRecords.add(subsequentRecord); + assertEquals(expectedRecords, new HashSet<>(recovered)); + } + @Test public void testRecoverWithNoCheckpoint() throws IOException { final SequentialAccessWriteAheadLog repo = createWriteRepo(); @@ -145,12 +199,15 @@ private SequentialAccessWriteAheadLog createRecoveryRepo() throws I } private SequentialAccessWriteAheadLog createWriteRepo() throws IOException { + return createWriteRepo(new DummyRecordSerde()); + } + + private SequentialAccessWriteAheadLog createWriteRepo(final DummyRecordSerde serde) throws IOException { final File targetDir = new File("target"); final File storageDir = new File(targetDir, testName.getMethodName()); deleteRecursively(storageDir); assertTrue(storageDir.mkdirs()); - final DummyRecordSerde serde = new DummyRecordSerde(); final SerDeFactory serdeFactory = new SingletonSerDeFactory<>(serde); final SequentialAccessWriteAheadLog repo = new SequentialAccessWriteAheadLog<>(storageDir, serdeFactory); diff --git a/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/DummyRecordSerde.java b/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/DummyRecordSerde.java index 1f6aede9dbf5..920349365ba7 100644 --- a/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/DummyRecordSerde.java +++ b/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/DummyRecordSerde.java @@ -16,17 +16,31 @@ */ package org.wali; +import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; public class DummyRecordSerde implements SerDe { + private static final int INLINE_RECORD_INDICATOR = 1; + private static final int EXTERNAL_FILE_INDICATOR = 8; private int throwIOEAfterNserializeEdits = -1; private int throwOOMEAfterNserializeEdits = -1; private int serializeEditCount = 0; + private final Set externalFilesWritten = new HashSet<>(); + private Queue externalRecords; + @SuppressWarnings("fallthrough") @Override public void serializeEdit(final DummyRecord previousState, final DummyRecord record, final DataOutputStream out) throws IOException { @@ -37,6 +51,7 @@ public void serializeEdit(final DummyRecord previousState, final DummyRecord rec throw new OutOfMemoryError("Serialized " + (serializeEditCount - 1) + " records successfully, so now it's time to throw OOME"); } + out.write(INLINE_RECORD_INDICATOR); out.writeUTF(record.getUpdateType().name()); out.writeUTF(record.getId()); @@ -72,6 +87,57 @@ public void serializeRecord(final DummyRecord record, final DataOutputStream out @Override @SuppressWarnings("fallthrough") public DummyRecord deserializeRecord(final DataInputStream in, final int version) throws IOException { + if (externalRecords != null) { + final DummyRecord record = externalRecords.poll(); + if (record != null) { + return record; + } + + externalRecords = null; + } + + final int recordLocationIndicator = in.read(); + if (recordLocationIndicator == EXTERNAL_FILE_INDICATOR) { + final String externalFilename = in.readUTF(); + final File externalFile = new File(externalFilename); + + try (final InputStream fis = new FileInputStream(externalFile); + final InputStream bufferedIn = new BufferedInputStream(fis); + final DataInputStream dis = new DataInputStream(bufferedIn)) { + + externalRecords = new LinkedBlockingQueue<>(); + + DummyRecord record; + while ((record = deserializeRecordInline(dis, version, true)) != null) { + externalRecords.offer(record); + } + + return externalRecords.poll(); + } + } else if (recordLocationIndicator == INLINE_RECORD_INDICATOR) { + return deserializeRecordInline(in, version, false); + } else { + throw new IOException("Encountered invalid record location indicator: " + recordLocationIndicator); + } + } + + @Override + public boolean isMoreInExternalFile() { + return externalRecords != null && !externalRecords.isEmpty(); + } + + private DummyRecord deserializeRecordInline(final DataInputStream in, final int version, final boolean expectInlineRecordIndicator) throws IOException { + if (expectInlineRecordIndicator) { + final int locationIndicator = in.read(); + if (locationIndicator < 0) { + return null; + } + + if (locationIndicator != INLINE_RECORD_INDICATOR) { + throw new IOException("Expected inline record indicator but encountered " + locationIndicator); + } + } + final String updateTypeName = in.readUTF(); final UpdateType updateType = UpdateType.valueOf(updateTypeName); final String id = in.readUTF(); @@ -135,4 +201,21 @@ public void setThrowOOMEAfterNSerializeEdits(final int n) { public String getLocation(final DummyRecord record) { return record.getSwapLocation(); } + + @Override + public boolean isWriteExternalFileReferenceSupported() { + return true; + } + + @Override + public void writeExternalFileReference(final File externalFile, final DataOutputStream out) throws IOException { + out.write(EXTERNAL_FILE_INDICATOR); + out.writeUTF(externalFile.getAbsolutePath()); + + externalFilesWritten.add(externalFile); + } + + public Set getExternalFileReferences() { + return Collections.unmodifiableSet(externalFilesWritten); + } } diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml index 6ddebf80753d..b6a99a8e04ff 100644 --- a/nifi-commons/pom.xml +++ b/nifi-commons/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-commons diff --git a/nifi-docker/dockerhub/DockerImage.txt b/nifi-docker/dockerhub/DockerImage.txt index 12b872aa21a1..18a254a821c4 100644 --- a/nifi-docker/dockerhub/DockerImage.txt +++ b/nifi-docker/dockerhub/DockerImage.txt @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -apache/nifi:1.8.0 +apache/nifi:1.9.0 diff --git a/nifi-docker/dockerhub/Dockerfile b/nifi-docker/dockerhub/Dockerfile index 7c844ce09824..08c38c647fb6 100644 --- a/nifi-docker/dockerhub/Dockerfile +++ b/nifi-docker/dockerhub/Dockerfile @@ -22,7 +22,7 @@ LABEL site="https://nifi.apache.org" ARG UID=1000 ARG GID=1000 -ARG NIFI_VERSION=1.8.0 +ARG NIFI_VERSION=1.9.0 ARG BASE_URL=https://archive.apache.org/dist ARG MIRROR_BASE_URL=${MIRROR_BASE_URL:-${BASE_URL}} ARG NIFI_BINARY_PATH=${NIFI_BINARY_PATH:-/nifi/${NIFI_VERSION}/nifi-${NIFI_VERSION}-bin.zip} diff --git a/nifi-docker/dockerhub/README.md b/nifi-docker/dockerhub/README.md index dabf7eb6c20c..70aefeea978d 100644 --- a/nifi-docker/dockerhub/README.md +++ b/nifi-docker/dockerhub/README.md @@ -78,7 +78,7 @@ You can also pass in environment variables to change the NiFi communication port docker run --name nifi \ -p 9090:9090 \ -d \ - -e NIFI_WEB_HTTP_PORT='9090' + -e NIFI_WEB_HTTP_PORT='9090' \ apache/nifi:latest For a list of the environment variables recognised in this build, look into the .sh/secure.sh and .sh/start.sh scripts @@ -191,6 +191,9 @@ can be published to the host. The Variable Registry can be configured for the docker image using the `NIFI_VARIABLE_REGISTRY_PROPERTIES` environment variable. -======= +======= +**NOTE**: If NiFi is proxied at context paths other than the root path of the proxy, the paths need to be set in the +_nifi.web.proxy.context.path_ property, which can be assigned via the environment variable _NIFI\_WEB\_PROXY\_CONTEXT\_PATH_. + **NOTE**: If mapping the HTTPS port specifying trusted hosts should be provided for the property _nifi.web.proxy.host_. This property can be specified to running instances via specifying an environment variable at container instantiation of _NIFI\_WEB\_PROXY\_HOST_. diff --git a/nifi-docker/dockerhub/pom.xml b/nifi-docker/dockerhub/pom.xml index 837ca3ef53dc..84e2845167c3 100644 --- a/nifi-docker/dockerhub/pom.xml +++ b/nifi-docker/dockerhub/pom.xml @@ -15,7 +15,7 @@ org.apache.nifi nifi-docker - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT dockerhub diff --git a/nifi-docker/dockerhub/sh/start.sh b/nifi-docker/dockerhub/sh/start.sh index 936d277cf919..447da40a26d2 100755 --- a/nifi-docker/dockerhub/sh/start.sh +++ b/nifi-docker/dockerhub/sh/start.sh @@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string' "${NIFI_ZK_CONNECT_S prop_replace 'nifi.zookeeper.root.node' "${NIFI_ZK_ROOT_NODE:-/nifi}" prop_replace 'nifi.cluster.flow.election.max.wait.time' "${NIFI_ELECTION_MAX_WAIT:-5 mins}" prop_replace 'nifi.cluster.flow.election.max.candidates' "${NIFI_ELECTION_MAX_CANDIDATES:-}" +prop_replace 'nifi.web.proxy.context.path' "${NIFI_WEB_PROXY_CONTEXT_PATH:-}" . "${scripts_dir}/update_cluster_state_management.sh" @@ -53,7 +54,6 @@ case ${AUTH} in echo 'Enabling LDAP user authentication' # Reference ldap-provider in properties prop_replace 'nifi.security.user.login.identity.provider' 'ldap-provider' - prop_replace 'nifi.security.needClientAuth' 'WANT' . "${scripts_dir}/secure.sh" . "${scripts_dir}/update_login_providers.sh" diff --git a/nifi-docker/dockermaven/pom.xml b/nifi-docker/dockermaven/pom.xml index 2d146359165d..d9694e5a0873 100644 --- a/nifi-docker/dockermaven/pom.xml +++ b/nifi-docker/dockermaven/pom.xml @@ -15,7 +15,7 @@ org.apache.nifi nifi-docker - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT dockermaven diff --git a/nifi-docker/dockermaven/sh/start.sh b/nifi-docker/dockermaven/sh/start.sh index 936d277cf919..447da40a26d2 100755 --- a/nifi-docker/dockermaven/sh/start.sh +++ b/nifi-docker/dockermaven/sh/start.sh @@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string' "${NIFI_ZK_CONNECT_S prop_replace 'nifi.zookeeper.root.node' "${NIFI_ZK_ROOT_NODE:-/nifi}" prop_replace 'nifi.cluster.flow.election.max.wait.time' "${NIFI_ELECTION_MAX_WAIT:-5 mins}" prop_replace 'nifi.cluster.flow.election.max.candidates' "${NIFI_ELECTION_MAX_CANDIDATES:-}" +prop_replace 'nifi.web.proxy.context.path' "${NIFI_WEB_PROXY_CONTEXT_PATH:-}" . "${scripts_dir}/update_cluster_state_management.sh" @@ -53,7 +54,6 @@ case ${AUTH} in echo 'Enabling LDAP user authentication' # Reference ldap-provider in properties prop_replace 'nifi.security.user.login.identity.provider' 'ldap-provider' - prop_replace 'nifi.security.needClientAuth' 'WANT' . "${scripts_dir}/secure.sh" . "${scripts_dir}/update_login_providers.sh" diff --git a/nifi-docker/pom.xml b/nifi-docker/pom.xml index 6faf03bfb890..b10c51ca1649 100644 --- a/nifi-docker/pom.xml +++ b/nifi-docker/pom.xml @@ -15,12 +15,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-docker - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-docs/pom.xml b/nifi-docs/pom.xml index 185d471dd5c9..9c0b6cbb56cc 100644 --- a/nifi-docs/pom.xml +++ b/nifi-docs/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom nifi-docs diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 05aeff755a4d..0244cfa2fee3 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -73,9 +73,38 @@ When NiFi first starts up, the following files and directories are created: See the <> section of this guide for more information about configuring NiFi repositories and configuration files. +== Port Configuration + +=== NiFi +The following table lists the default ports used by NiFi and the corresponding property in the _nifi.properties_ file. + +[options="header,footer"] +|================================================================================================================================================== +| Function | Property | Default Value +|HTTP Port | `nifi.web.http.port` | `8080` +|HTTPS Port* | `nifi.web.https.port` | `9443` +|Remote Input Socket Port* | `nifi.remote.input.socket.port` | `10443` +|Cluster Node Protocol Port* | `nifi.cluster.node.protocol.port` | `11443` +|Cluster Node Load Balancing Port | `nifi.cluster.node.load.balance.port` | `6342` +|Web HTTP Forwarding Port | `nifi.web.http.port.forwarding` | _none_ +|================================================================================================================================================== + +NOTE: The ports marked with an asterisk (*) have property values that are blank by default in _nifi.properties_. The values shown in the table are the default values for these ports when <> is used to generate _nifi.properties_ for a secured NiFi instance. The default Certificate Authority Port used by <> is `8443`. + +=== Embedded Zookeeper +The following table lists the default ports used by an <> and the corresponding property in the _zookeeper.properties_ file. + +[options="header,footer"] +|================================================================================================================================================== +| Function | Property | Default Value +|Zookeeper Client Port | `clientPort` | `2181` +|Zookeeper Server Quorum and Leader Election Ports | `server.1` | _none_ +|================================================================================================================================================== + +NOTE: Commented examples for the Zookeeper server ports are included in the _zookeeper.properties_ file in the form `server.N=nifi-nodeN-hostname:2888:3888`. == Configuration Best Practices -NOTE: If you are running on Linux, consider these best practices. Typical Linux defaults are not necessarily well-tuned for the needs of an IO intensive application like NiFi. For all of these areas, your distribution's requirements may vary. Use these sections as advice, but +If you are running on Linux, consider these best practices. Typical Linux defaults are not necessarily well-tuned for the needs of an IO intensive application like NiFi. For all of these areas, your distribution's requirements may vary. Use these sections as advice, but consult your distribution-specific documentation for how best to achieve these recommendations. Maximum File Handles:: @@ -139,7 +168,6 @@ NiFi provides several different configuration options for security purposes. The |`nifi.security.truststore` | Filename of the Truststore that will be used to authorize those connecting to NiFi. A secured instance with no Truststore will refuse all incoming connections. |`nifi.security.truststoreType` | The type of the Truststore. Must be either `PKCS12` or `JKS`. JKS is the preferred type, PKCS12 files will be loaded with BouncyCastle provider. |`nifi.security.truststorePasswd` | The password for the Truststore. -|`nifi.security.needClientAuth` | Set to `true` to specify that connecting clients must authenticate themselves. This property is used by the NiFi cluster protocol to indicate that nodes in the cluster will be authenticated and must have certificates that are trusted by the Truststores. If not set, the default value is `true`. |================================================================================================================================================== Once the above properties have been configured, we can enable the User Interface to be accessed over HTTPS instead of HTTP. This is accomplished @@ -150,460 +178,25 @@ properties can be specified. NOTE: It is important when enabling HTTPS that the `nifi.web.http.port` property be unset. NiFi only supports running on HTTP *or* HTTPS, not both simultaneously. -Similar to `nifi.security.needClientAuth`, the web server can be configured to require certificate based client authentication for users accessing -the User Interface. In order to do this it must be configured to not support username/password authentication using <> or <>. Either of these options -will configure the web server to WANT certificate based client authentication. This will allow it to support users with certificates and those without -that may be logging in with their credentials or those accessing anonymously. If username/password authentication and anonymous access are not configured, -the web server will REQUIRE certificate based client authentication. See <> for more details. +NiFi's web server will REQUIRE certificate based client authentication for users accessing the User Interface when not configured with an alternative +authentication mechanism which would require one way SSL (for instance LDAP, OpenId Connect, etc). Enabling an alternative authentication mechanism will +configure the web server to WANT certificate base client authentication. This will allow it to support users with certificates and those without that +may be logging in with credentials. See <> for more details. Now that the User Interface has been secured, we can easily secure Site-to-Site connections and inner-cluster communications, as well. This is -accomplished by setting the `nifi.remote.input.secure` and `nifi.cluster.protocol.is.secure` properties, respectively, to `true`. - +accomplished by setting the `nifi.remote.input.secure` and `nifi.cluster.protocol.is.secure` properties, respectively, to `true`. These communications +will always REQUIRE two way SSL as the nodes will use their configured keystore/truststore for authentication. +[[tls_generation_toolkit]] === TLS Generation Toolkit -In order to facilitate the secure setup of NiFi, you can use the `tls-toolkit` command line utility to automatically generate the required keystores, truststore, and relevant configuration files. This is especially useful for securing multiple NiFi nodes, which can be a tedious and error-prone process. - -Wildcard certificates (i.e. two nodes `node1.nifi.apache.org` and `node2.nifi.apache.org` being assigned the same certificate with a CN or SAN entry of `+*.nifi.apache.org+`) are *not officially supported* and *not recommended*. There are numerous disadvantages to using wildcard certificates, and a cluster working with wildcard certificates has occurred in previous versions out of lucky accidents, not intentional support. Wildcard SAN entries are acceptable *if* each cert maintains an additional unique SAN entry and CN entry. - -==== Potential issues with wildcard certificates - -* In many places throughout the codebase, cluster communications use certificate identities many times to identify a node, and if the certificate simply presents a wildcard DN, that doesn’t resolve to a specific node -* Admins may need to provide a custom node identity in _authorizers.xml_ for `*.nifi.apache.org` because all proxy actions only resolve to the cert DN (see <>) -* Admins have no traceability into which node performed an action because they all resolve to the same DN -* Admins running multiple instances on the same machine using different ports to identify them can accidentally put `node1` hostname with `node2` port, and the address will resolve fine because it’s using the same certificate, but the host header handler will block it because the `node1` hostname is (correctly) not listed as an acceptable host for `node2` instance -* If the wildcard certificate is compromised, all nodes are compromised - -NOTE: JKS keystores and truststores are recommended for NiFi. This tool allows the specification of other keystore types on the command line but will ignore a type of PKCS12 for use as the truststore because that format has some compatibility issues between BouncyCastle and Oracle implementations. - -The `tls-toolkit` command line tool has two primary modes of operation: - -1. Standalone -- generates the certificate authority, keystores, truststores, and _nifi.properties_ files in one command. -2. Client/Server mode -- uses a Certificate Authority Server that accepts Certificate Signing Requests from clients, signs them, and sends the resulting certificates back. Both client and server validate the other’s identity through a shared secret. - -==== Standalone -Standalone mode is invoked by running `./bin/tls-toolkit.sh standalone -h` which prints the usage information along with descriptions of options that can be specified. - -You can use the following command line options with the `tls-toolkit` in standalone mode: - -* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) -* `--additionalCACertificate ` Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary -* `-B`,`--clientCertPassword ` Password for client certificate. Must either be one value or one for each client DN (auto-generate if not specified) -* `-c`,`--certificateAuthorityHostname ` Hostname of NiFi Certificate Authority (default: `localhost`) -* `-C`,`--clientCertDn ` Generate client certificate suitable for use in browser with specified DN (Can be specified multiple times) -* `-d`,`--days ` Number of days issued certificate should be valid for (default: `1095`) -* `-f`,`--nifiPropertiesFile ` Base _nifi.properties_ file to update (Embedded file identical to the one in a default NiFi install will be used if not specified) -* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore -* `-G`,`--globalPortSequence ` Use sequential ports that are calculated for all hosts according to the provided hostname expressions (Can be specified multiple times, MUST BE SAME FROM RUN TO RUN) -* `-h`,`--help` Print help and exit -* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) -* `-K`,`--keyPassword ` Key password to use. Must either be one value or one for each host (auto-generate if not specified) -* `-n`,`--hostnames ` Comma separated list of hostnames -* `--nifiDnPrefix ` String to prepend to hostname(s) when determining DN (default: `CN=`) -* `--nifiDnSuffix ` String to append to hostname(s) when determining DN (default: `, OU=NIFI`) -* `-o`,`--outputDirectory ` The directory to output keystores, truststore, config files (default: `../bin`) -* `-O`,`--isOverwrite` Overwrite existing host output -* `-P`,`--trustStorePassword ` Keystore password to use. Must either be one value or one for each host (auto-generate if not specified) -* `-s`,`--signingAlgorithm ` Algorithm to use for signing certificates (default: `SHA256WITHRSA`) -* `-S`,`--keyStorePassword ` Keystore password to use. Must either be one value or one for each host (auto-generate if not specified) -* `--subjectAlternativeNames ` Comma-separated list of domains to use as Subject Alternative Names in the certificate -* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) +In order to facilitate the secure setup of NiFi, you can use the `tls-toolkit` command line utility to automatically generate the required keystores, truststore, and relevant configuration files. This is especially useful for securing multiple NiFi nodes, which can be a tedious and error-prone process. For more information, see the <> section in the link:toolkit-guide.html[NiFi Toolkit Guide]. Related topics include: +* <> +* <> +* <> +* <> -Hostname Patterns: - -* Square brackets can be used in order to easily specify a range of hostnames. Example: `[01-20]` -* Parentheses can be used in order to specify that more than one NiFi instance will run on the given host(s). Example: `(5)` - -Examples: - -Create 4 sets of keystore, truststore, _nifi.properties_ for localhost along with a client certificate with the given DN: ----- -bin/tls-toolkit.sh standalone -n 'localhost(4)' -C 'CN=username,OU=NIFI' ----- - -Create keystore, truststore, _nifi.properties_ for 10 NiFi hostnames in each of 4 subdomains: ----- -bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain' ----- - -Create 2 sets of keystore, truststore, _nifi.properties_ for 10 NiFi hostnames in each of 4 subdomains along with a client certificate with the given DN: ----- -bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain(2)' -C 'CN=username,OU=NIFI' ----- - -==== Client/Server -Client/Server mode relies on a long-running Certificate Authority (CA) to issue certificates. The CA can be stopped when you’re not bringing nodes online. - - -===== Server - -The CA server is invoked by running `./bin/tls-toolkit.sh server -h` which prints the usage information along with descriptions of options that can be specified. - -You can use the following command line options with the `tls-toolkit` in server mode: - -* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) -* `--configJsonIn ` The place to read configuration info from (defaults to the value of configJson), implies useConfigJson if set (default: `configJson` value) -* `-d`,`--days ` Number of days issued certificate should be valid for (default: `1095`) -* `-D`,`--dn ` The dn to use for the CA certificate (default: `CN=YOUR_CA_HOSTNAME,OU=NIFI`) -* `-f`,`--configJson ` The place to write configuration info (default: `config.json`) -* `-F`,`--useConfigJson` Flag specifying that all configuration is read from `configJson` to facilitate automated use (otherwise `configJson` will only be written to) -* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore -* `-h`,`--help` Print help and exit -* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) -* `-p`,`--PORT ` The port for the Certificate Authority to listen on (default: `8443`) -* `-s`,`--signingAlgorithm ` Algorithm to use for signing certificates (default: `SHA256WITHRSA`) -* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) -* `-t`,`--token ` The token to use to prevent MITM (required and must be same as one used by clients) - -===== Client - -The client can be used to request new Certificates from the CA. The client utility generates a keypair and Certificate Signing Request (CSR) and sends the CSR to the Certificate Authority. The client is invoked by running `./bin/tls-toolkit.sh client -h` which prints the usage information along with descriptions of options that can be specified. - -You can use the following command line options with the `tls-toolkit` in client mode: - -* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) -* `-c`,`--certificateAuthorityHostname ` Hostname of NiFi Certificate Authority (default: `localhost`) -* `-C`,`--certificateDirectory ` The directory to write the CA certificate (default: `.`) -* `--configJsonIn ` The place to read configuration info from, implies `useConfigJson` if set (default: `configJson` value) -* `-D`,`--dn ` The DN to use for the client certificate (default: `CN=,OU=NIFI`) (this is auto-populated by the tool) -* `-f`,`--configJson ` The place to write configuration info (default: `config.json`) -* `-F`,`--useConfigJson` Flag specifying that all configuration is read from `configJson` to facilitate automated use (otherwise `configJson` will only be written to) -* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore -* `-h`,`--help` Print help and exit -* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) -* `-p`,`--PORT ` The port to use to communicate with the Certificate Authority (default: `8443`) -* `--subjectAlternativeNames ` Comma-separated list of domains to use as Subject Alternative Names in the certificate -* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) -* `-t`,`--token ` The token to use to prevent MITM (required and must be same as one used by CA) - -After running the client you will have the CA’s certificate, a keystore, a truststore, and a `config.json` with information about them as well as their passwords. - -For a client certificate that can be easily imported into the browser, specify: `-T PKCS12`. - -==== Using An Existing Intermediate Certificate Authority (CA) - -In some enterprise scenarios, a security/IT team may provide a signing certificate that has already been signed by the organization's certificate authority (CA). This *intermediate CA* can be used to sign the *node* (sometimes referred to as *leaf*) certificates that will be installed on each NiFi node, or the *client certificates* used to identify users. In order to inject the existing signing certificate into the toolkit process, follow these steps: - -. Generate or obtain the signed intermediate CA keys in the following format (see additional commands below): - * Public certificate in PEM format: `nifi-cert.pem` - * Private key in PEM format: `nifi-key.key` -. Place the files in the *toolkit working directory*. This is the directory where the tool is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. - * For example, given the following scenario, the toolkit command can be run from its location as long as the output directory `-o` is `../hardcoded/`, and the existing `nifi-cert.pem` and `nifi-key.key` will be used. - ** e.g. `$ ./toolkit/bin/tls-toolkit.sh standalone -o ./hardcoded/ -n 'node4.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` will result in a new directory at `./hardcoded/node4.nifi.apache.org` with a keystore and truststore containing a certificate signed by `./hardcoded/nifi-key.key` - * If the `-o` argument is not provided, the default working directory (`.`) must contain `nifi-cert.pem` and `nifi-key.key` - ** e.g. `$ cd ./hardcoded/ && ../toolkit/bin/tls-toolkit.sh standalone -n 'node5.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` - -``` -# Example directory structure *before* commands above are run - -🔓 0s @ 18:07:58 $ tree -L 2 -. -├── hardcoded -│   ├── CN=myusername.hardcoded_OU=NiFi.p12 -│   ├── CN=myusername.hardcoded_OU=NiFi.password -│   ├── nifi-cert.pem -│   ├── nifi-key.key -│   ├── node1.nifi.apache.org -│   ├── node2.nifi.apache.org -│   └── node3.nifi.apache.org -└── toolkit -    ├── LICENSE -    ├── NOTICE -    ├── README -    ├── bin -    ├── conf -    ├── docs -    └── lib -``` - -===== Additional Commands - -The `nifi-cert.pem` and `nifi-key.key` files should be ASCII-armored (Base64-encoded ASCII) files containing the CA public certificate and private key respectively. Here are sample files of each to show the expected format: - -====== nifi-cert.pem - -``` -# The first command shows the actual content of the encoded file, and the second parses it and shows the internal values - -.../certs $ more nifi-cert.pem ------BEGIN CERTIFICATE----- -MIIDZTCCAk2gAwIBAgIKAWTeM3kDAAAAADANBgkqhkiG9w0BAQsFADAxMQ0wCwYD -VQQLDAROSUZJMSAwHgYDVQQDDBduaWZpLWNhLm5pZmkuYXBhY2hlLm9yZzAeFw0x -ODA3MjgwMDA0MzJaFw0yMTA3MjcwMDA0MzJaMDExDTALBgNVBAsMBE5JRkkxIDAe -BgNVBAMMF25pZmktY2EubmlmaS5hcGFjaGUub3JnMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAqkVrrC+AkFbjnCpupSy84tTFDsRVUIWYj/k2pVwC145M -3bpr0pRCzLuzovAjFCmT5L+isTvNjhionsqif07Ebd/M2psYE/Rih2MULsX6KgRe -1nRUiBeKF08hlmSBMGDFPj39yDzE/V9edxV/KGjRqVgw/Qy0vwaS5uWdXnLDhzoV -4/Mz7lGmYoMasZ1uexlH93jjBl1+EFL2Xoa06oLbEojJ9TKaWhpG8ietEedf7WM0 -zqBEz2kHo9ddFk9yxiCkT4SUKnDWkhwc/o6us1vEXoSw+tmufHY/A3gVihjWPIGz -qyLFl9JuN7CyJepkVVqTdskBG7S85G/kBlizUj5jOwIDAQABo38wfTAOBgNVHQ8B -Af8EBAMCAf4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUKiWBKbMMQ1zUabD4gI7L -VOWOcy0wHwYDVR0jBBgwFoAUKiWBKbMMQ1zUabD4gI7LVOWOcy0wHQYDVR0lBBYw -FAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAxfHFIZLOw -mwIqnSI/ir8f/uzDMq06APHGdhdeIKV0HR74BtK95KFg42zeXxAEFeic98PC/FPV -tKpm2WUa1slMB+oP27cRx5Znr2+pktaqnM7f2JgMeJ8bduNH3RUkr9jwgkcJRwyC -I4fwHC9k18aizNdOf2q2UgQXxNXaLYPe17deuNVwwrflMgeFfVrwbT2uPJTMRi1D -FQyc6haF4vsOSSRzE6OyDoc+/1PpyPW75OeSXeVCbc3AEAvRuTZMBQvBQUqVM51e -MDG+K3rCeieSBPOnGNrEC/PiA/CvaMXBEog+xPAw1SgYfuCz4rlM3BdRa54z3+oO -lc8xbzd7w8Q3 ------END CERTIFICATE----- -.../certs $ openssl x509 -in nifi-cert.pem -text -noout -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 01:64:de:33:79:03:00:00:00:00 - Signature Algorithm: sha256WithRSAEncryption - Issuer: OU=NIFI, CN=nifi-ca.nifi.apache.org - Validity - Not Before: Jul 28 00:04:32 2018 GMT - Not After : Jul 27 00:04:32 2021 GMT - Subject: OU=NIFI, CN=nifi-ca.nifi.apache.org - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:aa:45:6b:ac:2f:80:90:56:e3:9c:2a:6e:a5:2c: - bc:e2:d4:c5:0e:c4:55:50:85:98:8f:f9:36:a5:5c: - 02:d7:8e:4c:dd:ba:6b:d2:94:42:cc:bb:b3:a2:f0: - 23:14:29:93:e4:bf:a2:b1:3b:cd:8e:18:a8:9e:ca: - a2:7f:4e:c4:6d:df:cc:da:9b:18:13:f4:62:87:63: - 14:2e:c5:fa:2a:04:5e:d6:74:54:88:17:8a:17:4f: - 21:96:64:81:30:60:c5:3e:3d:fd:c8:3c:c4:fd:5f: - 5e:77:15:7f:28:68:d1:a9:58:30:fd:0c:b4:bf:06: - 92:e6:e5:9d:5e:72:c3:87:3a:15:e3:f3:33:ee:51: - a6:62:83:1a:b1:9d:6e:7b:19:47:f7:78:e3:06:5d: - 7e:10:52:f6:5e:86:b4:ea:82:db:12:88:c9:f5:32: - 9a:5a:1a:46:f2:27:ad:11:e7:5f:ed:63:34:ce:a0: - 44:cf:69:07:a3:d7:5d:16:4f:72:c6:20:a4:4f:84: - 94:2a:70:d6:92:1c:1c:fe:8e:ae:b3:5b:c4:5e:84: - b0:fa:d9:ae:7c:76:3f:03:78:15:8a:18:d6:3c:81: - b3:ab:22:c5:97:d2:6e:37:b0:b2:25:ea:64:55:5a: - 93:76:c9:01:1b:b4:bc:e4:6f:e4:06:58:b3:52:3e: - 63:3b - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Key Usage: critical - Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment, Key Agreement, Certificate Sign, CRL Sign - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Key Identifier: - 2A:25:81:29:B3:0C:43:5C:D4:69:B0:F8:80:8E:CB:54:E5:8E:73:2D - X509v3 Authority Key Identifier: - keyid:2A:25:81:29:B3:0C:43:5C:D4:69:B0:F8:80:8E:CB:54:E5:8E:73:2D - - X509v3 Extended Key Usage: - TLS Web Client Authentication, TLS Web Server Authentication - Signature Algorithm: sha256WithRSAEncryption - 31:7c:71:48:64:b3:b0:9b:02:2a:9d:22:3f:8a:bf:1f:fe:ec: - c3:32:ad:3a:00:f1:c6:76:17:5e:20:a5:74:1d:1e:f8:06:d2: - bd:e4:a1:60:e3:6c:de:5f:10:04:15:e8:9c:f7:c3:c2:fc:53: - d5:b4:aa:66:d9:65:1a:d6:c9:4c:07:ea:0f:db:b7:11:c7:96: - 67:af:6f:a9:92:d6:aa:9c:ce:df:d8:98:0c:78:9f:1b:76:e3: - 47:dd:15:24:af:d8:f0:82:47:09:47:0c:82:23:87:f0:1c:2f: - 64:d7:c6:a2:cc:d7:4e:7f:6a:b6:52:04:17:c4:d5:da:2d:83: - de:d7:b7:5e:b8:d5:70:c2:b7:e5:32:07:85:7d:5a:f0:6d:3d: - ae:3c:94:cc:46:2d:43:15:0c:9c:ea:16:85:e2:fb:0e:49:24: - 73:13:a3:b2:0e:87:3e:ff:53:e9:c8:f5:bb:e4:e7:92:5d:e5: - 42:6d:cd:c0:10:0b:d1:b9:36:4c:05:0b:c1:41:4a:95:33:9d: - 5e:30:31:be:2b:7a:c2:7a:27:92:04:f3:a7:18:da:c4:0b:f3: - e2:03:f0:af:68:c5:c1:12:88:3e:c4:f0:30:d5:28:18:7e:e0: - b3:e2:b9:4c:dc:17:51:6b:9e:33:df:ea:0e:95:cf:31:6f:37: - 7b:c3:c4:37 -``` - -====== nifi-key.key - -``` -# The first command shows the actual content of the encoded file, and the second parses it and shows the internal values - -.../certs $ more nifi-key.key ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAqkVrrC+AkFbjnCpupSy84tTFDsRVUIWYj/k2pVwC145M3bpr -0pRCzLuzovAjFCmT5L+isTvNjhionsqif07Ebd/M2psYE/Rih2MULsX6KgRe1nRU -iBeKF08hlmSBMGDFPj39yDzE/V9edxV/KGjRqVgw/Qy0vwaS5uWdXnLDhzoV4/Mz -7lGmYoMasZ1uexlH93jjBl1+EFL2Xoa06oLbEojJ9TKaWhpG8ietEedf7WM0zqBE -z2kHo9ddFk9yxiCkT4SUKnDWkhwc/o6us1vEXoSw+tmufHY/A3gVihjWPIGzqyLF -l9JuN7CyJepkVVqTdskBG7S85G/kBlizUj5jOwIDAQABAoIBAAdWRnV89oVBuT0Z -dvsXGmyLzpH8U9DMcO6DRp+Jf3XaY+WKCutgCCDaVbtHrbtIr17EAzav5QOifGGb -SbVCp6Q0aJdi5360oSpEUrJRRZ5Z4dxL1vimSwUGG+RnIEn9YYJ1GWJve+2PFnr7 -KieLnL03V6UPzxoMJnhcnJNdTp+dBwzSazVQwye2csSJlVMk49t2lxBwce7ohuh+ -9fL7G3HU5S9d08QT1brknMHahcw1SYyJd0KSjRJCB6wAxnAZmJYJ1jQCI8YICq0j -RX2rhxEXuEMXQcaiFQXzCrmQEXreKUISDvNeu/h7YU9UvJWPZSFGnEGgnMP2XvQm -EjK3rQECgYEA5+OkpLsiLNMHGzj72PiBkq82sTLQJ2+8udYp6PheOGkhjjXoBse5 -YynyHlQt6CnVpJQ33mQUkJ+3ils0SMFtmI3rz3udzleek1so2L2J3+CI4kt7fFCb -FFbVXv+dLNrm+tOw68J48asyad8kEnHYq9Us+/3MLDmFJYTthkgzCpECgYEAu/ml -lQaWaZAQcQ8UuVeasxMYoN8zMmzfrkxc8AfNwKxF9nc44ywo4nJr+u/UVRGYpRgM -rdll5vz0Iq68qk03spaW7vDJn8hJQhkReQw1it9Fp/51r9MHzGTVarORJGa2oZ0g -iNe8LNizD3bQ19hEvju9mn0x9Q62Q7dapVpffwsCgYEAtC1TPpQQ59dIjERom5vr -wffWfTTIO/w8HgFkKxrgyuAVLJSCJtKFH6H1+M7bpKrsz6ZDCs+kkwMm76ASLf3t -lD2h3mNkqHG4SzLnuBD90jB666pO1rci6FjYDap7i+DC3F4j9+vxYYXt9Aln09UV -z94hx+LaA/rlk9OHY3EyB6ECgYBA/cCtNNjeaKv2mxM8PbjD/289d85YueHgfpCH -gPs3iZiq7W+iw8ri+FKzMSaFvw66zgTcOtULtxulviqG6ym9umk29dOQRgxmKQqs -gnckq6uGuOjxwJHqrlZHjQw6vLSaThxIk+aAzu+iAh+U8TZbW4ZjmrOiGdMUuJlD -oGpyHwKBgQCRjfqQjRelYVtU7j6BD9BDbCfmipwaRNP0CuAGOVtS+UnJuaIhsXFQ -QGEBuOnfFijIvb7YcXRL4plRYPMvDqYRNObuI6A+1xNtr000nxa/HUfzKVeI9Tsn -9AKMWnXS8ZcfStsVf3oDFffXYRqCaWeuhpMmg9TwdXoAuwfpE5GCmw== ------END RSA PRIVATE KEY----- -.../certs $ openssl rsa -in nifi-key.key -text -noout -Private-Key: (2048 bit) -modulus: - 00:aa:45:6b:ac:2f:80:90:56:e3:9c:2a:6e:a5:2c: - bc:e2:d4:c5:0e:c4:55:50:85:98:8f:f9:36:a5:5c: - 02:d7:8e:4c:dd:ba:6b:d2:94:42:cc:bb:b3:a2:f0: - 23:14:29:93:e4:bf:a2:b1:3b:cd:8e:18:a8:9e:ca: - a2:7f:4e:c4:6d:df:cc:da:9b:18:13:f4:62:87:63: - 14:2e:c5:fa:2a:04:5e:d6:74:54:88:17:8a:17:4f: - 21:96:64:81:30:60:c5:3e:3d:fd:c8:3c:c4:fd:5f: - 5e:77:15:7f:28:68:d1:a9:58:30:fd:0c:b4:bf:06: - 92:e6:e5:9d:5e:72:c3:87:3a:15:e3:f3:33:ee:51: - a6:62:83:1a:b1:9d:6e:7b:19:47:f7:78:e3:06:5d: - 7e:10:52:f6:5e:86:b4:ea:82:db:12:88:c9:f5:32: - 9a:5a:1a:46:f2:27:ad:11:e7:5f:ed:63:34:ce:a0: - 44:cf:69:07:a3:d7:5d:16:4f:72:c6:20:a4:4f:84: - 94:2a:70:d6:92:1c:1c:fe:8e:ae:b3:5b:c4:5e:84: - b0:fa:d9:ae:7c:76:3f:03:78:15:8a:18:d6:3c:81: - b3:ab:22:c5:97:d2:6e:37:b0:b2:25:ea:64:55:5a: - 93:76:c9:01:1b:b4:bc:e4:6f:e4:06:58:b3:52:3e: - 63:3b -publicExponent: 65537 (0x10001) -privateExponent: - 07:56:46:75:7c:f6:85:41:b9:3d:19:76:fb:17:1a: - 6c:8b:ce:91:fc:53:d0:cc:70:ee:83:46:9f:89:7f: - 75:da:63:e5:8a:0a:eb:60:08:20:da:55:bb:47:ad: - bb:48:af:5e:c4:03:36:af:e5:03:a2:7c:61:9b:49: - b5:42:a7:a4:34:68:97:62:e7:7e:b4:a1:2a:44:52: - b2:51:45:9e:59:e1:dc:4b:d6:f8:a6:4b:05:06:1b: - e4:67:20:49:fd:61:82:75:19:62:6f:7b:ed:8f:16: - 7a:fb:2a:27:8b:9c:bd:37:57:a5:0f:cf:1a:0c:26: - 78:5c:9c:93:5d:4e:9f:9d:07:0c:d2:6b:35:50:c3: - 27:b6:72:c4:89:95:53:24:e3:db:76:97:10:70:71: - ee:e8:86:e8:7e:f5:f2:fb:1b:71:d4:e5:2f:5d:d3: - c4:13:d5:ba:e4:9c:c1:da:85:cc:35:49:8c:89:77: - 42:92:8d:12:42:07:ac:00:c6:70:19:98:96:09:d6: - 34:02:23:c6:08:0a:ad:23:45:7d:ab:87:11:17:b8: - 43:17:41:c6:a2:15:05:f3:0a:b9:90:11:7a:de:29: - 42:12:0e:f3:5e:bb:f8:7b:61:4f:54:bc:95:8f:65: - 21:46:9c:41:a0:9c:c3:f6:5e:f4:26:12:32:b7:ad: - 01 -prime1: - 00:e7:e3:a4:a4:bb:22:2c:d3:07:1b:38:fb:d8:f8: - 81:92:af:36:b1:32:d0:27:6f:bc:b9:d6:29:e8:f8: - 5e:38:69:21:8e:35:e8:06:c7:b9:63:29:f2:1e:54: - 2d:e8:29:d5:a4:94:37:de:64:14:90:9f:b7:8a:5b: - 34:48:c1:6d:98:8d:eb:cf:7b:9d:ce:57:9e:93:5b: - 28:d8:bd:89:df:e0:88:e2:4b:7b:7c:50:9b:14:56: - d5:5e:ff:9d:2c:da:e6:fa:d3:b0:eb:c2:78:f1:ab: - 32:69:df:24:12:71:d8:ab:d5:2c:fb:fd:cc:2c:39: - 85:25:84:ed:86:48:33:0a:91 -prime2: - 00:bb:f9:a5:95:06:96:69:90:10:71:0f:14:b9:57: - 9a:b3:13:18:a0:df:33:32:6c:df:ae:4c:5c:f0:07: - cd:c0:ac:45:f6:77:38:e3:2c:28:e2:72:6b:fa:ef: - d4:55:11:98:a5:18:0c:ad:d9:65:e6:fc:f4:22:ae: - bc:aa:4d:37:b2:96:96:ee:f0:c9:9f:c8:49:42:19: - 11:79:0c:35:8a:df:45:a7:fe:75:af:d3:07:cc:64: - d5:6a:b3:91:24:66:b6:a1:9d:20:88:d7:bc:2c:d8: - b3:0f:76:d0:d7:d8:44:be:3b:bd:9a:7d:31:f5:0e: - b6:43:b7:5a:a5:5a:5f:7f:0b -exponent1: - 00:b4:2d:53:3e:94:10:e7:d7:48:8c:44:68:9b:9b: - eb:c1:f7:d6:7d:34:c8:3b:fc:3c:1e:01:64:2b:1a: - e0:ca:e0:15:2c:94:82:26:d2:85:1f:a1:f5:f8:ce: - db:a4:aa:ec:cf:a6:43:0a:cf:a4:93:03:26:ef:a0: - 12:2d:fd:ed:94:3d:a1:de:63:64:a8:71:b8:4b:32: - e7:b8:10:fd:d2:30:7a:eb:aa:4e:d6:b7:22:e8:58: - d8:0d:aa:7b:8b:e0:c2:dc:5e:23:f7:eb:f1:61:85: - ed:f4:09:67:d3:d5:15:cf:de:21:c7:e2:da:03:fa: - e5:93:d3:87:63:71:32:07:a1 -exponent2: - 40:fd:c0:ad:34:d8:de:68:ab:f6:9b:13:3c:3d:b8: - c3:ff:6f:3d:77:ce:58:b9:e1:e0:7e:90:87:80:fb: - 37:89:98:aa:ed:6f:a2:c3:ca:e2:f8:52:b3:31:26: - 85:bf:0e:ba:ce:04:dc:3a:d5:0b:b7:1b:a5:be:2a: - 86:eb:29:bd:ba:69:36:f5:d3:90:46:0c:66:29:0a: - ac:82:77:24:ab:ab:86:b8:e8:f1:c0:91:ea:ae:56: - 47:8d:0c:3a:bc:b4:9a:4e:1c:48:93:e6:80:ce:ef: - a2:02:1f:94:f1:36:5b:5b:86:63:9a:b3:a2:19:d3: - 14:b8:99:43:a0:6a:72:1f -coefficient: - 00:91:8d:fa:90:8d:17:a5:61:5b:54:ee:3e:81:0f: - d0:43:6c:27:e6:8a:9c:1a:44:d3:f4:0a:e0:06:39: - 5b:52:f9:49:c9:b9:a2:21:b1:71:50:40:61:01:b8: - e9:df:16:28:c8:bd:be:d8:71:74:4b:e2:99:51:60: - f3:2f:0e:a6:11:34:e6:ee:23:a0:3e:d7:13:6d:af: - 4d:34:9f:16:bf:1d:47:f3:29:57:88:f5:3b:27:f4: - 02:8c:5a:75:d2:f1:97:1f:4a:db:15:7f:7a:03:15: - f7:d7:61:1a:82:69:67:ae:86:93:26:83:d4:f0:75: - 7a:00:bb:07:e9:13:91:82:9b -``` - -. To convert from DER encoded public certificate (`cert.der`) to PEM encoded (`cert.pem`): - * If the DER file contains both the public certificate and private key, remove the private key with this command: - ** `perl -pe 'BEGIN{undef $/;} s|-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----|Removed private key|gs' cert.der > cert.pem` - * If the DER file only contains the public certificate, use this command: - ** `openssl x509 -inform der -in cert.der -out cert.pem` -. To convert from a PKCS12 keystore (`keystore.p12`) containing both the public certificate and private key into PEM encoded files (`$PASSWORD` is the keystore password): - * `openssl pkcs12 -in keystore.p12 -out cert.der -nodes -password "pass:$PASSWORD"` - * `openssl pkcs12 -in keystore.p12 -nodes -nocerts -out key.key -password "pass:$PASSWORD"` - * Follow the steps above to convert `cert.der` to `cert.pem` -. To convert from a Java Keystore (`keystore.jks`) containing private key into PEM encoded files (`$P12_PASSWORD` is the PKCS12 keystore password, `$JKS_PASSWORD` is the Java keystore password you want to set, and `$ALIAS` can be any value -- the NiFi default is `nifi-key`): - * `keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -destkeypass "$P12_PASSWORD" -deststorepass "$P12_PASSWORD" -srcstorepass "$JKS_PASSWORD" -srcalias "$ALIAS" -destalias "$ALIAS"` - * Follow the steps above to convert from `keystore.p12` to `cert.pem` and `key.key` -. To convert from PKCS #8 PEM format to PKCS #1 PEM format: - * If the private key is provided in PKCS #8 format (the file begins with `-----BEGIN PRIVATE KEY-----` rather than `-----BEGIN RSA PRIVATE KEY-----`), the following command will convert it to PKCS #1 format, move the original to `nifi-key-pkcs8.key`, and rename the PKCS #1 version as `nifi-key.key`: - ** `openssl rsa -in nifi-key.key -out nifi-key-pkcs1.key && mv nifi-key.key nifi-key-pkcs8.key && mv nifi-key-pkcs1.key nifi-key.key` - -===== Signing with Externally-signed CA Certificates - -To sign generated certificates with a certificate authority (CA) generated outside of the TLS Toolkit, ensure the necessary files are in the right format and location (see above). For example, an organization *Large Organization* has an internal CA (`CN=ca.large.org, OU=Certificate Authority`). This *root CA* is offline and only used to sign other internal CAs. The Large IT team generates an *intermediate CA* (`CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority`) to be used to sign all NiFi node certificates (`CN=node1.nifi.large.org, OU=NiFi`, `CN=node2.nifi.large.org, OU=NiFi`, etc.). - -To use the toolkit to generate these certificates and sign them using the *intermediate CA*, ensure that the following files are present (see <> above): - -* `nifi-cert.pem` -- the public certificate of the *intermediate CA* in PEM format -* `nifi-key.key` -- the Base64-encoded private key of the *intermediate CA* in PKCS #1 PEM format - -If the *intermediate CA* was the *root CA*, it would be *self-signed* -- the signature over the certificate would be issued from the same key. In that case (the same as a toolkit-generated CA), no additional arguments are necessary. However, because the *intermediate CA* is signed by the *root CA*, the public certificate of the *root CA* needs to be provided as well to validate the signature. The `--additionalCACertificate` parameter is used to specify the path to the signing public certificate. The value should be the absolute path to the *root CA* public certificate. - -Example: - -``` -# Generate cert signed by intermediate CA (which is signed by root CA) -- WILL FAIL - -$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ --P passwordpassword \ --S passwordpassword \ --o /opt/certs/externalCA \ --O - -2018/08/02 18:48:11 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. -2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA -2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=Certificate Authority -2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority -2018/08/02 18:48:12 WARN [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority not signed by CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority [certificate does not verify with supplied key] -Error generating TLS configuration. (The signing certificate was not signed by any known certificates) - -# Provide additional CA certificate path for signature verification of intermediate CA - -$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ --P passwordpassword \ --S passwordpassword \ --o /opt/certs/externalCA \ ---additionalCACertificate /opt/certs/externalCA/root.pem \ --O - -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=ca.large.org, OU=Certificate Authority -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate was signed by CN=ca.large.org, OU=Certificate Authority -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Using existing CA certificate /opt/certs/externalCA/nifi-cert.pem and key /opt/certs/externalCA/nifi-key.key -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Writing new ssl configuration to /opt/certs/externalCA/node1.nifi.apache.org -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Successfully generated TLS configuration for node1.nifi.apache.org 1 in /opt/certs/externalCA/node1.nifi.apache.org -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: No clientCertDn specified, not generating any client certificates. -2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: tls-toolkit standalone completed successfully -``` [[user_authentication]] == User Authentication @@ -1906,412 +1499,29 @@ If it is not possible to install the unlimited strength jurisdiction policies, t It is preferable to request upstream/downstream systems to switch to link:https://cwiki.apache.org/confluence/display/NIFI/Encryption+Information[keyed encryption^] or use a "strong" link:https://cwiki.apache.org/confluence/display/NIFI/Key+Derivation+Function+Explanations[Key Derivation Function (KDF) supported by NiFi^]. +[[encrypt-config_tool]] == Encrypted Passwords in Configuration Files In order to facilitate the secure setup of NiFi, you can use the `encrypt-config` command line utility to encrypt raw configuration values that NiFi decrypts in memory on startup. This extensible protection scheme transparently allows NiFi to use raw values in operation, while protecting them at rest. In the future, hardware security modules (HSM) and external secure storage mechanisms will be integrated, but for now, an AES encryption provider is the default implementation. -This is a change in behavior; prior to 1.0, all configuration values were stored in plaintext on the file system. POSIX file permissions were recommended to limit unauthorized access to these files +This is a change in behavior; prior to 1.0, all configuration values were stored in plaintext on the file system. POSIX file permissions were recommended to limit unauthorized access to these files. If no administrator action is taken, the configuration values remain unencrypted. -[[encrypt-config_tool]] -=== Encrypt-Config Tool - -The `encrypt-config` command line tool (invoked as `./bin/encrypt-config.sh` or `bin\encrypt-config.bat`) reads from a _nifi.properties_ file with plaintext sensitive configuration values, prompts for a master password or raw hexadecimal key, and encrypts each value. It replaces the plain values with the protected value in the same file, or writes to a new _nifi.properties_ file if specified. - -The default encryption algorithm utilized is AES/GCM 128/256-bit. 128-bit is used if the JCE Unlimited Strength Cryptographic Jurisdiction Policy files are not installed, and 256-bit is used if they are installed. - -You can use the following command line options with the `encrypt-config` tool: - - * `-h`,`--help` Prints this usage message - * `-v`,`--verbose` Sets verbose mode (default false) - * `-n`,`--niFiProperties ` The _nifi.properties_ file containing unprotected config values (will be overwritten) - * `-l`,`--loginIdentityProviders ` The _login-identity-providers.xml_ file containing unprotected config values (will be overwritten) - * `-a`,`--authorizers ` The _authorizers.xml_ file containing unprotected config values (will be overwritten) - * `-f`,`--flowXml ` The _flow.xml.gz_ file currently protected with old password (will be overwritten) - * `-b`,`--bootstrapConf ` The _bootstrap.conf_ file to persist master key - * `-o`,`--outputNiFiProperties ` The destination _nifi.properties_ file containing protected config values (will not modify input _nifi.properties_) - * `-i`,`--outputLoginIdentityProviders ` The destination _login-identity-providers.xml_ file containing protected config values (will not modify input _login-identity-providers.xml_) - * `-u`,`--outputAuthorizers ` The destination _authorizers.xml_ file containing protected config values (will not modify input _authorizers.xml_) - * `-g`,`--outputFlowXml ` The destination _flow.xml.gz_ file containing protected config values (will not modify input _flow.xml.gz_) - * `-k`,`--key ` The raw hexadecimal key to use to encrypt the sensitive properties - * `-e`,`--oldKey ` The old raw hexadecimal key to use during key migration - * `-p`,`--password ` The password from which to derive the key to use to encrypt the sensitive properties - * `-w`,`--oldPassword ` The old password from which to derive the key during migration - * `-r`,`--useRawKey` If provided, the secure console will prompt for the raw key value in hexadecimal form - * `-m`,`--migrate` If provided, the _nifi.properties_ and/or _login-identity-providers.xml_ sensitive properties will be re-encrypted with a new key - * `-x`,`--encryptFlowXmlOnly` If provided, the properties in _flow.xml.gz_ will be re-encrypted with a new key but the _nifi.properties_ and/or _login-identity-providers.xml_ files will not be modified - * `-s`,`--propsKey ` The password or key to use to encrypt the sensitive processor properties in _flow.xml.gz_ - * `-A`,`--newFlowAlgorithm ` The algorithm to use to encrypt the sensitive processor properties in _flow.xml.gz_ - * `-P`,`--newFlowProvider ` The security provider to use to encrypt the sensitive processor properties in _flow.xml.gz_ - -As an example of how the tool works, assume that you have installed the tool on a machine supporting 256-bit encryption and with the following existing values in the _nifi.properties_ file: - ----- -# security properties # -nifi.sensitive.props.key=thisIsABadSensitiveKeyPassword -nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL -nifi.sensitive.props.provider=BC -nifi.sensitive.props.additional.keys= - -nifi.security.keystore=/path/to/keystore.jks -nifi.security.keystoreType=JKS -nifi.security.keystorePasswd=thisIsABadKeystorePassword -nifi.security.keyPasswd=thisIsABadKeyPassword -nifi.security.truststore= -nifi.security.truststoreType= -nifi.security.truststorePasswd= ----- - -Enter the following arguments when using the tool: - ----- -encrypt-config.sh --b bootstrap.conf --k 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 --n nifi.properties ----- - -As a result, the _nifi.properties_ file is overwritten with protected properties and sibling encryption identifiers (`aes/gcm/256`, the currently supported algorithm): - ----- -# security properties # -nifi.sensitive.props.key=n2z+tTTbHuZ4V4V2||uWhdasyDXD4ZG2lMAes/vqh6u4vaz4xgL4aEbF4Y/dXevqk3ulRcOwf1vc4RDQ== -nifi.sensitive.props.key.protected=aes/gcm/256 -nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL -nifi.sensitive.props.provider=BC -nifi.sensitive.props.additional.keys= - -nifi.security.keystore=/path/to/keystore.jks -nifi.security.keystoreType=JKS -nifi.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo -nifi.security.keystorePasswd.protected=aes/gcm/256 -nifi.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg== -nifi.security.keyPasswd.protected=aes/gcm/256 -nifi.security.truststore= -nifi.security.truststoreType= -nifi.security.truststorePasswd= ----- - -Additionally, the _bootstrap.conf_ file is updated with the encryption key as follows: - ----- -# Master key in hexadecimal format for encrypted sensitive configuration values -nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 ----- - -Sensitive configuration values are encrypted by the tool by default, however you can encrypt any additional properties, if desired. To encrypt additional properties, specify them as comma-separated values in the `nifi.sensitive.props.additional.keys` property. - -If the _nifi.properties_ file already has valid protected values, those property values are not modified by the tool. - -When applied to _login-identity-providers.xml_ and _authorizers.xml_, the property elements are updated with an `encryption` attribute: - -Example of protected _login-identity-providers.xml_: - ----- - - - ldap-provider - org.apache.nifi.ldap.LdapProvider - START_TLS - someuser - q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA - - Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g - - ... - ----- - -Example of protected _authorizers.xml_: - ---- - - - ldap-user-group-provider - org.apache.nifi.ldap.tenants.LdapUserGroupProvider - START_TLS - someuser - q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA - - Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g - - ... - ---- - -[encrypt_config_property_migration] -=== Sensitive Property Key Migration - -In order to change the key used to encrypt the sensitive values, indicate *migration mode* using the `-m` or `--migrate` flag, provide the new key or password using the `-k` or `-p` flags as usual, and provide the existing key or password using `-e` or `-w` respectively. This will allow the toolkit to decrypt the existing values and re-encrypt them, and update _bootstrap.conf_ with the new key. Only one of the key or password needs to be specified for each phase (old vs. new), and any combination is sufficient: - -* old key -> new key -* old key -> new password -* old password -> new key -* old password -> new password - -[encrypt_config_flow_migration] -=== Existing Flow Migration - -This tool can also be used to change the value of `nifi.sensitive.props.key` for an existing flow. The tool will read the existing _flow.xml.gz_ and decrypt any sensitive component properties using the original key, -then re-encrypt the sensitive properties with the new key, and write out a new version of the _flow.xml.gz_, or overwrite the existing one. - -The current sensitive properties key is not provided as a command-line argument, as it is read directly from _nifi.properties_. As this file is a required parameter, the `-x`/`--encryptFlowXmlOnly` flags tell the tool *not* to attempt to encrypt the properties in _nifi.properties_, but rather to *only* update the `nifi.sensitive.props.key` value with the new key. The exception to this is if the _nifi.properties_ is *already* encrypted, the new sensitive property key will also be encrypted before being written to _nifi.properties_. - -The following command would migrate the sensitive properties key in place, meaning it would overwrite the existing _flow.xml.gz_ and _nifi.properties_: ----- -./encrypt-config.sh -f /path/to/flow.xml.gz -n ./path/to/nifi.properties -s newpassword -x ----- - -The following command would migrate the sensitive properties key and write out a separate _flow.xml.gz_ and _nifi.properties_: ----- -./encrypt-config.sh -f ./path/to/src/flow.xml.gz -g /path/to/dest/flow.xml.gz -n /path/to/src/nifi.properties -o /path/to/dest/nifi.properties -s newpassword -x ----- - -[[encrypt-config_password]] -=== Password Key Derivation - -Instead of providing a 32 or 64 character raw hexadecimal key, you can provide a password from which the key will be derived. As of 1.0.0, the password must be at least 12 characters, and the key will be derived using `SCrypt` with the parameters: - -* `pw` -- the password bytes in `UTF-8` -* `salt` -- the fixed salt value (`NIFI_SCRYPT_SALT`) bytes in `UTF-8` -* `N` -- 2^16^ -* `r` -- 8 -* `p` -- 1 -* `dkLen` -- determined by the JCE policies available - -As of August 2016, these values are determined to be strong for this threat model but may change in future versions. - -NOTE: While fixed salts are counter to best practices, a static salt is necessary for deterministic key derivation without additional storage of the salt value. - -[[encrypt-config_secure_prompt]] -=== Secure Prompt - -If you prefer not to provide the password or raw key in the command-line invocation of the tool, leaving these arguments absent will prompt a secure console read of the password (by default) or raw key (if the `-r` flag is provided at invocation). +For more information, see the <> section in the link:toolkit-guide.html[NiFi Toolkit Guide]. [[admin-toolkit]] -== Administrative Tools -The admin toolkit contains command line utilities for administrators to support NiFi maintenance in standalone -and clustered environments. These utilities include: - -* Notify -- The notification tool allows administrators to send bulletins to the NiFi UI using the command line. -* Node Manager -- The node manager tool allows administrators to perform a status check on a node as well as to connect, disconnect, or remove nodes that are part of a cluster. -* File Manager -- The file manager tool allows administrators to backup, install or restore a NiFi installation from backup. - -The admin toolkit is bundled with the nifi-toolkit and can be executed with scripts found in the `bin` folder. - -=== Prerequisites for Running Admin Toolkit in a Secure Environment -For secured nodes and clusters, two policies should be configured in advance: - -* Access the controller – A user that will have access to these utilities should be authorized in NiFi by creating an “access -the controller” policy (`/controller`) with both view and modify rights. -* Proxy user request – If not previously set node’s identity (the DN value of the node’s certificate) should be authorized to proxy requests on behalf of a user - -When executing either the notify or node manager tools in a secured environment the `proxyDN` flag option should be used in -order to properly identify the user that was authorized to execute these commands. In non-secure environments, or if running -the status operation on the Node Manager tool, the flag is ignored. - -=== Notify -Notify allows administrators to send messages as bulletins to NiFi. Notify is supported on NiFi version 1.2.0 and higher. -The notification tool is also available in a _notify.bat_ file for use on Windows machines. - -To send notifications: - - notify.sh -d {$NIFI_HOME} –b {nifi bootstrap file path} -m {message} [-l {level}] [-v] +== NiFi Toolkit Administrative Tools +In addition to `tls-toolkit` and `encrypt-config`, the NiFi Toolkit also contains command line utilities for administrators to support NiFi maintenance in standalone and clustered environments. These utilities include: -To show help: - - notify.sh -h - -The following are available options: - -* `-b`,`--bootstrapConf ` Existing Bootstrap Configuration file (required) -* `-d`,`--nifiInstallDir ` NiFi Root Folder (required) -* `-h`,`--help` Help Text (optional) -* `-l`,`--level ` Status level of bulletin – `INFO`, `WARN`, `ERROR` -* `-m`,`--message ` Bulletin message (required) -* `-p`,`--proxyDN ` Proxy or User DN (required for secured nodes) -* `-v`,`--verbose` Verbose messaging (optional) - - -Example usage on Linux: - - ./notify.sh -d /usr/nifi/nifi_current -b /usr/nifi/nifi_current/conf/bootstrap.conf -m "Test Message Server 1" -l "WARN" –p “ydavis@nifi” -v - -Example usage on Windows: - - notify.bat -v -d "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT" -b "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT\\conf\\bootstrap.conf" -m "Test Message Server 1" -v - -Executing the above command line should result in a bulletin appearing in NiFi: - -image::nifi-notifications.png["NiFi Notifications"] - -=== Node Manager - -Node manager supports connecting, disconnecting and removing a node when in a cluster (an error message -displays if the node is not part of a cluster) as well as obtaining the status of a node. When nodes are disconnected -from a cluster and need to be -connected or removed, a list of urls of connected nodes should be provided to send the required command to -the active cluster. Node Manager supports NiFi version 1.0.0 and higher. Node Manager is also available in -_node-manager.bat_ file for use on Windows machines. - -To connect, disconnect, or remove a node from a cluster: - - node-manager.sh -d {$NIFI_HOME} –b { nifi bootstrap file path} - -o {remove|disconnect|connect|status} [-u {url list}] [-p {proxy name}] [-v] - -To show help: - - node-manager.sh -h - -The following are available options: - -* `-b`,`--bootstrapConf ` Existing Bootstrap Configuration file (required) -* `-d`,`--nifiInstallDir ` NiFi Root Folder (required) -* `-h`,`--help` Help Text (optional) -* `-o`, `--operation ` Operations supported: status, connect (cluster), disconnect (cluster), remove (cluster) -* `-p`,`--proxyDN ` Proxy or User DN (required for secured nodes doing connect, disconnect and remove operations) -* `-u`,`--clusterUrls ` Comma delimited list of active urls for cluster (optional). Not required for disconnecting a node yet will be needed when connecting or removing from a cluster -* `-v`,`--verbose` Verbose messaging (optional) - - -Example usage on Linux: - - # disconnect without cluster url list - ./node-manager.sh - -d /usr/nifi/nifi_current - -b /usr/nifi/nifi_current/conf/bootstrap.conf - -o disconnect - –p ydavis@nifi - -v - - #with url list - ./node-manager.sh - -d /usr/nifi/nifi_current - -b /usr/nifi/nifi_current/conf/bootstrap.conf - -o connect - -u 'http://nifi-server-1:8080,http://nifi-server-2:8080' - -v - -Example usage on Windows: - - node-manager.bat - -d "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT" - -b "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT\\conf\\bootstrap.conf" - -o disconnect - –v - -==== Expected behavior - -Status: - -To obtain information on UI availability of a node, the status operation can be used to determine if the node is running. -If the `–u (clusterUrls)` option is not provided the current node url is checked otherwise the urls provided will be checked. - -Disconnect: - -When a node is disconnected from the cluster, the node itself should appear as disconnected and the cluster -should have a bulletin indicating the disconnect request was received. The cluster should also show _n-1/n_ -nodes available in the cluster. For example, if 1 node is disconnected from a 3-node cluster, then "2 of 3" nodes -should show on the remaining nodes in the cluster. Changes to the flow should not be allowed on the cluster -with a disconnected node. - -Connect: - -When the connect command is executed to reconnect a node to a cluster, upon completion the node itself -should show that it has rejoined the cluster by showing _n/n_ nodes. Previously it would have shown Disconnected. -Other nodes in the cluster should receive a bulletin of the connect request and also show _n/n_ nodes allowing -for changes to be allowed to the flow. - -Remove: - -When the remove command is executed the node should show as disconnected from a cluster. The nodes remaining -in the cluster should show _n-1/n-1_ nodes. For example, if 1 node is removed from a 3-node cluster, then the remaining 2 nodes -should show "2 of 2" nodes. The cluster should allow a flow to be adjusted. The removed node can rejoin the -cluster if restarted and the flow for the cluster has not changed. If the flow was changed, the flow template of -the removed node should be deleted before restarting the node to allow it to obtain the cluster flow (otherwise -an uninheritable flow file exception may occur). - -=== File Manager - -The File Manager utility allows system administrators to take a backup of an existing NiFi installation, install a new version of NiFi -in a designated location (while migrating any previous configuration settings) or restore an installation from a previous backup. -File Manager supports NiFi version 1.0.0 and higher and is available in _file-manager.bat_ file for use on Windows machines. - -To show help: - - file-manager.sh -h - -The following are available options: - -* `-b`,`--backupDir ` Backup NiFi Directory (used with backup or restore operation) -* `-c`,`--nifiCurrentDir ` Current NiFi Installation Directory (used optionally with install or restore operation) -* `-d`,`--nifiInstallDir ` NiFi Installation Directory (used with install or restore operation) -* `-h`,`--help` Print help info (optional) -* `-i`,`--installFile ` NiFi Install File (used with install operation) -* `-m`,`--moveRepositories` Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation) -* `-o`,`--operation ` File operation (install | backup | restore) -* `-r`,`--nifiRollbackDir ` NiFi Installation Directory (used with install or restore operation) -* `-t`,`--bootstrapConf ` Current NiFi Bootstrap Configuration File (used optionally) -* `-v`,`--verbose` Verbose messaging (optional) -* `-x`,`--overwriteConfigs` Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation) - -Example usage on Linux: - - # backup NiFi installation - # option -t may be provided to ensure backup of external boostrap.conf file - ./file-manager.sh - -o backup - –b /tmp/nifi_bak - –c /usr/nifi_old - -v - - # install NiFi using compressed tar file into /usr/nifi directory (should install as /usr/nifi/nifi-1.3.0). - # migrate existing configurations with location determined by external bootstrap.conf and move over repositories from nifi_old - # options -t and -c should both be provided if migration of configurations, state and repositories are required - ./file-manager.sh - -o install - –i nifi-1.3.0.tar.gz - –d /usr/nifi - –c /usr/nifi/nifi_old - -t /usr/nifi/old_conf/bootstrap.conf - -v - -m - - # restore NiFi installation from backup directory and move back repositories - # option -t may be provided to ensure bootstrap.conf is restored to the file path provided, otherwise it is placed in the - # default directory under the rollback path (e.g. /usr/nifi_old/conf) - ./file-manager.sh - -o restore - –b /tmp/nifi_bak - –r /usr/nifi_old - –c /usr/nifi - -m - -v - -=== Expected Behavior - -Backup: - -During the backup operation a backup directory is created in a designated location for an existing NiFi installation. Backups will capture all critical files -(including any internal or external configurations, libraries, scripts and documents) however it excludes backing up repositories and logs due to potential size. -If configuration/library files are external from the existing installation folder the backup operation will capture those as well. - -Install: - -During the install operation File Manager will perform installation using the designated NiFi binary file (either tar.gz or zip file) -to create a new installation or migrate an existing nifi installation to a new one. Installation can optionally move repositories (if located within the configuration -folder of the current installation) to the new installation as well as migrate configuration files to the newer installation. - -Restore: - -The restore operation allows an existing installation to revert back to a previous installation. Using an existing backup directory (created from the backup operation) -the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. - -NOTE: If repositories were changed due to the installation -of a newer version of NiFi these may no longer be compatible during restore. In that scenario exclude the `-m` option to ensure new repositories will be created or, if repositories -live outside of the NiFi directory, remove them so they can be recreated on startup after restore. +* CLI -- The `cli` tool enables administrators to interact with NiFi and NiFi Registry instances to automate tasks such as deploying versioned flows and managing process groups and cluster nodes. +* File Manager -- The `file-manager` tool enables administrators to backup, install or restore a NiFi installation from backup. +* Flow Analyzer -- The `flow-analyzer` tool produces a report that helps administrators understand the max amount of data which can be stored in backpressure for a given flow. +* Node Manager -- The `node-manager` tool enables administrators to perform status checks on nodes as well as the ability to connect, disconnect, or remove nodes from the cluster. +* Notify -- The `notify` tool enables administrators to send bulletins to the NiFi UI. +* S2S -- The `s2s` tool enables administrators to send data into or out of NiFi flows over site-to-site. +For more information about each utility, see the link:toolkit-guide.html[NiFi Toolkit Guide]. [[clustering]] == Clustering Configuration @@ -2321,6 +1531,7 @@ In the future, we hope to provide supplemental documentation that covers the NiF image::zero-master-cluster-http-access.png["NiFi Cluster HTTP Access"] +=== Zero-Master Clustering NiFi employs a Zero-Master Clustering paradigm. Each node in the cluster performs the same tasks on the data, but each operates on a different set of data. One of the nodes is automatically elected (via Apache ZooKeeper) as the Cluster Coordinator. All nodes in the cluster will then send heartbeat/status information @@ -2332,9 +1543,9 @@ flow is provided to that node, and that node is able to join the cluster, assumi flow matches the copy provided by the Cluster Coordinator. If the node's version of the flow configuration differs from that of the Cluster Coordinator's, the node will not join the cluster. -*Why Cluster?* + +=== Why Cluster? -NiFi Administrators or Dataflow Managers (DFMs) may find that using one instance of NiFi on a single server is not +NiFi Administrators or DataFlow Managers (DFMs) may find that using one instance of NiFi on a single server is not enough to process the amount of data they have. So, one solution is to run the same dataflow on multiple NiFi servers. However, this creates a management problem, because each time DFMs want to change or update the dataflow, they must make those changes on each server and then monitor each server individually. By clustering the NiFi servers, it's possible to @@ -2342,10 +1553,10 @@ have that increased processing capability along with a single interface through the dataflow. Clustering allows the DFM to make each change only once, and that change is then replicated to all the nodes of the cluster. Through the single interface, the DFM may also monitor the health and status of all the nodes. -NiFi Clustering is unique and has its own terminology. It's important to understand the following terms before setting up a cluster. - [template="glossary", id="terminology"] -*Terminology* + +=== Terminology + +NiFi Clustering is unique and has its own terminology. It's important to understand the following terms before setting up a cluster: *NiFi Cluster Coordinator*: A NiFi Cluster Coordinator is the node in a NiFi cluster that is responsible for carrying out tasks to manage which nodes are allowed in the cluster and providing the most up-to-date flow to newly joining nodes. When a @@ -2359,21 +1570,22 @@ ZooKeeper is used to automatically elect a Primary Node. If that node disconnect Primary Node will automatically be elected. Users can determine which node is currently elected as the Primary Node by looking at the Cluster Management page of the User Interface. +image::primary-node-cluster-mgt.png["Primary Node in Cluster Management UI"] + *Isolated Processors*: In a NiFi cluster, the same dataflow runs on all the nodes. As a result, every component in the flow runs on every node. However, there may be cases when the DFM would not want every processor to run on every node. The most common case is when using a processor that communicates with an external service using a protocol that does not scale well. -For example, the GetSFTP processor pulls from a remote directory, and if the GetSFTP Processor runs on every node in the -cluster tries simultaneously to pull from the same remote directory, there could be race conditions. Therefore, the DFM could -configure the GetSFTP on the Primary Node to run in isolation, meaning that it only runs on that node. It could pull in data and - -with the proper dataflow configuration - load-balance it across the rest of the nodes in the cluster. Note that while this +For example, the GetSFTP processor pulls from a remote directory. If the GetSFTP Processor runs on every node in the +cluster and tries simultaneously to pull from the same remote directory, there could be race conditions. Therefore, the DFM could +configure the GetSFTP on the Primary Node to run in isolation, meaning that it only runs on that node. With the proper dataflow configuration, it could pull in data and load-balance it across the rest of the nodes in the cluster. Note that while this feature exists, it is also very common to simply use a standalone NiFi instance to pull data and feed it to the cluster. It just depends on the resources available and how the Administrator decides to configure the cluster. *Heartbeats*: The nodes communicate their health and status to the currently elected Cluster Coordinator via "heartbeats", which let the Coordinator know they are still connected to the cluster and working properly. By default, the nodes emit heartbeats every 5 seconds, and if the Cluster Coordinator does not receive a heartbeat from a node within 40 seconds, it -disconnects the node due to "lack of heartbeat". (The 5-second setting is configurable in the _nifi.properties_ file. -See the <> section of this document for more information.) The reason that the Cluster Coordinator +disconnects the node due to "lack of heartbeat". The 5-second setting is configurable in the _nifi.properties_ file (see +the <> section for more information). The reason that the Cluster Coordinator disconnects the node is because the Coordinator needs to ensure that every node in the cluster is in sync, and if a node is not heard from regularly, the Coordinator cannot be sure it is still in sync with the rest of the cluster. If, after 40 seconds, the node does send a new heartbeat, the Coordinator will automatically request that the node re-join the cluster, @@ -2381,7 +1593,7 @@ to include the re-validation of the node's flow. Both the disconnection due to lack of heartbeat and the reconnection once a heartbeat is received are reported to the DFM in the User Interface. -*Communication within the Cluster* + +=== Communication within the Cluster As noted, the nodes communicate with the Cluster Coordinator via heartbeats. When a Cluster Coordinator is elected, it updates a well-known ZNode in Apache ZooKeeper with its connection information so that nodes understand where to send heartbeats. If one @@ -2392,20 +1604,58 @@ happen automatically. When the DFM makes changes to the dataflow, the node that receives the request to change the flow communicates those changes to all nodes and waits for each node to respond, indicating that it has made the change on its local flow. +[[managing_nodes]] +=== Managing Nodes + +==== Disconnect Nodes + +A DFM may manually disconnect a node from the cluster. A node may also become disconnected for other reasons, such as due to a lack of heartbeat. The Cluster Coordinator will show a bulletin on the User Interface when a node is disconnected. The DFM will not be able to make any changes to the dataflow until the issue of the disconnected node is resolved. The DFM or the Administrator will need to troubleshoot the issue with the node and resolve it before any new changes can be made to the dataflow. However, it is worth noting that just because a node is disconnected does not mean that it is not working. This may happen for a few reasons, for example when the node is unable to communicate with the Cluster Coordinator due to network problems. + +To manually disconnect a node, select the "Disconnect" icon (image:iconDisconnect.png["Disconnect Icon"]) from the node's row. + +image::disconnected-node-cluster-mgt.png["Disconnected Node in Cluster Management UI"] + +A disconnected node can be connected (image:iconConnect.png["Connect Icon"]), offloaded (image:iconOffload.png["Offload Icon"]) or deleted (image:iconDelete.png["Delete Icon"]). + +NOTE: Not all nodes in a "Disconnected" state can be offloaded. If the node is disconnected and unreachable, the offload request can not be received by the node to start the offloading. Additionally, offloading may be interrupted or prevented due to firewall rules. + +==== Offload Nodes + +Flowfiles that remain on a disconnected node can be rebalanced to other active nodes in the cluster via offloading. In the Cluster Management dialog, select the "Offload" icon (image:iconOffload.png["Offload Icon"]) for a Disconnected node. This will stop all processors, terminate all processors, stop transmitting on all remote process groups and rebalance flowfiles to the other connected nodes in the cluster. + +image::offloading-node-cluster-mgt.png["Offloading Node in Cluster Management UI"] + +Nodes that remain in "Offloading" state due to errors encountered (out of memory, no network connection, etc.) can be reconnected to the cluster by restarting NiFi on the node. Offloaded nodes can be either reconnected to the cluster (by selecting Connect or restarting NiFi on the node) or deleted from the cluster. + +image::offloaded-node-cluster-mgt.png["Offloaded Node in Cluster Management UI"] + +==== Delete Nodes + +There are cases where a DFM may wish to continue making changes to the flow, even though a node is not connected to the cluster. In this case, the DFM may elect to delete the node from the cluster entirely. In the Cluster Management dialog, select the "Delete" icon (image:iconDelete.png["Delete Icon"]) for a Disconnected or Offloaded node. Once deleted, the node cannot be rejoined to the cluster until it has been restarted. + +==== Decommission Nodes -*Dealing with Disconnected Nodes* + +The steps to decommission a node and remove it from a cluster are as follows: -A DFM may manually disconnect a node from the cluster. But if a node becomes disconnected for any other reason (such as due to lack of heartbeat), -the Cluster Coordinator will show a bulletin on the User Interface. The DFM will not be able to make any changes to the dataflow until the issue -of the disconnected node is resolved. The DFM or the Administrator will need to troubleshoot the issue with the node and resolve it before any -new changes may be made to the dataflow. However, it is worth noting that just because a node is disconnected does not mean that it is not working; -this may happen for a few reasons, including that the node is unable to communicate with the Cluster Coordinator due to network problems. +1. Disconnect the node. +2. Once disconnect completes, offload the node. +3. Once offload completes, delete the node. +4. Once the delete request has finished, stop/remove the NiFi service on the host. -There are cases where a DFM may wish to continue making changes to the flow, even though a node is not connected to the cluster. -In this case, they DFM may elect to remove the node from the cluster entirely through the Cluster Management dialog. Once removed, -the node cannot be rejoined to the cluster until it has been restarted. +==== NiFi CLI Node Commands -*Flow Election* + +As an alternative to the UI, the following NiFi CLI commands can be used for retrieving a single node, retrieving a list of nodes, and connecting/disconnecting/offloading/deleting nodes: + +* `nifi get-node` +* `nifi get-nodes` +* `nifi connect-node` +* `nifi disconnect-node` +* `nifi offload-node` +* `nifi delete-node` + +For more information, see the <> section in the link:toolkit-guide.html[NiFi Toolkit Guide]. + +=== Flow Election When a cluster first starts up, NiFi must determine which of the nodes have the "correct" version of the flow. This is done by voting on the flows that each of the nodes has. When a node attempts to connect to a cluster, it provides a copy of its local flow to the Cluster Coordinator. If no flow @@ -2420,7 +1670,7 @@ the "popular vote" with the caveat that the winner will never be an "empty flow" allows an administrator to remove a node's _flow.xml.gz_ file and restart the node, knowing that the node's flow will not be voted to be the "correct" flow unless no other flow is found. -*Basic Cluster Setup* + +=== Basic Cluster Setup This section describes the setup for a simple three-node, non-secure cluster comprised of three instances of NiFi. @@ -2428,7 +1678,7 @@ For each instance, certain properties in the _nifi.properties_ file will need to should be evaluated for your situation and adjusted accordingly. All the properties are described in the <> section of this guide; however, in this section, we will focus on the minimum properties that must be set for a simple cluster. -For all three instances, the Cluster Common Properties can be left with the default settings. Note, however, that if you change these settings, +For all three instances, the <> can be left with the default settings. Note, however, that if you change these settings, they must be set the same on every instance in the cluster. For each Node, the minimum properties to configure are as follows: @@ -2471,7 +1721,7 @@ one of the nodes, and the User Interface should look similar to the following: image:ncm.png["Clustered User Interface"] -*Troubleshooting* +=== Troubleshooting If you encounter issues and your cluster does not work as described, investigate the _nifi-app.log_ and _nifi-user.log_ files on the nodes. If needed, you can change the logging level to DEBUG by editing the `conf/logback.xml` file. Specifically, @@ -2819,7 +2069,7 @@ This output can be rather verbose but provides extremely valuable information fo [[zookeeper_migrator]] === ZooKeeper Migrator -You can use the NiFi ZooKeeper Migrator to perform the following tasks: +You can use the `zk-migrator` tool to perform the following tasks: * Moving ZooKeeper information from one ZooKeeper cluster to another * Migrating ZooKeeper node ownership @@ -2831,86 +2081,7 @@ For example, you may want to use the ZooKeeper Migrator when you are: * Upgrading from NiFi 0.x with an external ZooKeeper to NiFi 1.x with the same external ZooKeeper * Migrating from an external ZooKeeper to an embedded ZooKeeper in NiFi 1.x -The NiFi ZooKeeper Migrator is part of the NiFi Toolkit and is downloaded separately from the -link:https://nifi.apache.org/download.html[Apache NiFi download page^]. - -[[zk_migrator_command_line_parameters]] -==== zk-migrator.sh Command Line Parameters - -You can use the following command line options with the ZooKeeper Migrator: - -* `-a`,`--auth ` Allows the specification of a username and password for authentication with ZooKeeper. This option is mutually exclusive with the `-k`,`--krb-conf` option. -* `-f`,`--file ` The file used for ZooKeeper data serialized as JSON. When used with the `-r`,`--receive` option, data read from ZooKeeper will be stored in the given filename. When used with the `-s`,`--send` option, the data in the file will be sent to ZooKeeper. -* `-h`,`--help` Prints help, displays available parameters with descriptions -* `--ignore-source` Allows the ZooKeeper Migrator to write to the ZooKeeper and path from which the data was obtained. -* `-k`,`--krb-conf ` Allows the specification of a JAAS configuration file to allow authentication with a ZooKeeper configured to use Kerberos. This option is mutually exclusive with the `-a`,`--auth` option. -* `-r`,`--receive` Receives data from ZooKeeper and writes to the given filename (if the `-f`,`--file` option is provided) or standard output. The data received will contain the full path to each node read from ZooKeeper. This option is mutually exclusive with the `-s`,`--send` option. -* `-s`,`--send` Sends data to ZooKeeper that is read from the given filename (if the `-f`,`--file` option is provided) or standard input. The paths for each node in the data being sent to ZooKeeper are absolute paths, and will be stored in ZooKeeper under the *path* portion of the `-z`,`--zookeeper` argument. Typically, the *path* portion of the argument can be omitted, which will store the nodes at their absolute paths. This option is mutually exclusive with the `-r`,`--receive` option. -* `--use-existing-acl` Allows the Zookeeper Migrator to write ACL values retrieved from the source Zookeeper server to destination server. Default action will apply Open rights for unsecured destinations or Creator Only rights for secured destinations. -* `-z`,`--zookeeper ` The ZooKeeper server(s) to use, specified by a connect string, comprised of one or more comma-separated host:port pairs followed by a path, in the format of _host:port[,host2:port...,hostn:port]/znode/path_. - -[[migrating_between_source_destination_zookeepers]] -==== Migrating Between Source and Destination ZooKeepers - -Before you begin, confirm that: - -* You have installed the destination ZooKeeper cluster. -* You have installed and configured a NiFi cluster to use the destination ZooKeeper cluster. -* If you are migrating ZooKeepers due to upgrading NiFi from 0.x to 1.x,, you have already followed appropriate NiFi upgrade steps. -* You have configured Kerberos as needed. -* You have not started processing any dataflow (to avoid duplicate data processing). -* If one of the ZooKeeper clusters you are using is configured with Kerberos, you are running the ZooKeeper Migrator from a host that has access to NiFi’s ZooKeeper client jaas configuration file (see <> for more information). - -===== ZooKeeper Migration Steps - -1. Collect the following information: -+ -|==== -|*Required Information*|*Description* -|Source ZooKeeper hostname (*sourceHostname*)|The hostname must be one of the hosts running in the ZooKeeper ensemble, which can be found in _/conf/zookeeper.properties_. Any of the hostnames declared in the `server.N` properties can be used. -|Destination ZooKeeper hostname (*destinationHostname*)|The hostname must be one of the hosts running in the ZooKeeper ensemble, which can be found in _/conf/zookeeper.properties_. Any of the hostnames declared in the `server.N` properties can be used. -|Source ZooKeeper port (*sourceClientPort*)|This can be found in _/conf/zookeeper.properties_. The port is specified in the `clientPort` property. -|Destination ZooKeeper port (*destinationClientPort*)|This can be found in _/conf/zookeeper.properties_. The port is specified in the `clientPort` property. -|Export data path|Determine the path that will store a json file containing the export of data from ZooKeeper. It must be readable and writable by the user running the zk-migrator tool. -|Source ZooKeeper Authentication Information|This information is in _/conf/state-management.xml_. For NiFi 0.x, if Creator Only is specified in _state-management.xml_, you need to supply authentication information using the `-a,--auth` argument with the values from the Username and Password properties in _state-management.xml_. For NiFi 1.x, supply authentication information using the `-k,--krb-conf` argument. - -If the _state-management.xml_ specifies Open, no authentication is required. -|Destination ZooKeeper Authentication Information|This information is in _/conf/state-management.xml_. For NiFi 0.x, if Creator Only is specified in _state-management.xml_, you need to supply authentication information using the `-a,--auth` argument with the values from the Username and Password properties in state-management.xml. For NiFi 1.x, supply authentication information using the `-k,--krb-conf` argument. - -If the _state-management.xml_ specifies Open, no authentication is required. -|Root path to which NiFi writes data in Source ZooKeeper (*sourceRootPath*)|This information can be found in `/conf/state-management.xml` under the Root Node property in the cluster-provider element. (default: `/nifi`) -|Root path to which NiFi writes data in Destination ZooKeeper (*destinationRootPath*)|This information can be found in _/conf/state-management.xml_ under the Root Node property in the cluster-provider element. -|==== -2. Stop all processors in the NiFi flow. If you are migrating between two NiFi installations, the flows on both must be stopped. -3. Export the NiFi component data from the source ZooKeeper. The following command reads from the specified ZooKeeper running on the given hostname:port, using the provided path to the data, and authenticates with ZooKeeper using the given username and password. The data read from ZooKeeper is written to the file provided. - -* For NiFi 0.x -** For an open ZooKeeper: -*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -f /path/to/export/zk-source-data.json` -** For a ZooKeeper using username:password for authentication: -*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -a -f /path/to/export/zk-source-data.json` - -* For NiFi 1.x -** For an open ZooKeeper: -*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -f /path/to/export/zk-source-data.json` -** For a ZooKeeper using Kerberos for authentication: -*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-source-data.json` - -4. (Optional) If you have used the new NiFi installation to do any processing, you can also export its ZooKeeper data as a backup prior to performing the migration. - -* For an open ZooKeeper: -** `zk-migrator.sh -r -z destinationHostname:destinationClientPort/destinationRootPath/components -f /path/to/export/zk-destination-backup-data.json` -* For a ZooKeeper using Kerberos for authentication: -** `zk-migrator.sh -r -z destinationHostname:destinationClientPort/destinationRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-destination-backup-data.json` - -5. Migrate the ZooKeeper data to the destination ZooKeeper. If the source and destination ZooKeepers are the same, the `--ignore-source` option can be added to the following examples. - -* For an open ZooKeeper: -** `zk-migrator.sh -s -z destinationHostname:destinationClientPort/destinationRootPath/components -f /path/to/export/zk-source-data.json` -* For a ZooKeeper using Kerberos for authentication: -** `zk-migrator.sh -s -z destinationHostname:destinationClientPort/destinationRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-source-data.json` - -6. Once the migration has completed successfully, start the processors in the NiFi flow. Processing should continue from the point at which it was stopped when the NiFi flow was stopped. +For more information, see the <> section in the link:toolkit-guide.html[NiFi Toolkit Guide]. [[bootstrap_properties]] == Bootstrap Properties @@ -3122,8 +2293,8 @@ host[:port] the expected values need to be configured. This may be required when separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. `localhost:18443, proxyhost:443`). IPv6 addresses are accepted. Please refer to RFC 5952 Sections link:https://tools.ietf.org/html/rfc5952#section-4[4] and link:https://tools.ietf.org/html/rfc5952#section-6[6] for additional details. -** NiFi will only accept HTTP requests with a X-ProxyContextPath or X-Forwarded-Context header if the value is whitelisted in the `nifi.web.proxy.context.path` property in -_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath or X-Forwarded-Context header value that is not +** NiFi will only accept HTTP requests with a X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header if the value is whitelisted in the `nifi.web.proxy.context.path` property in +_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header value that is not present in the whitelist, the "An unexpected error has occurred" page will be shown and an error will be written to the _nifi-app.log_. * Additional configurations at both proxy server and NiFi cluster are required to make NiFi Site-to-Site work behind reverse proxies. See <> for details. @@ -3452,7 +2623,7 @@ All of the properties defined above (see <> tool in NiFi Toolkit. + |`nifi.provenance.repository.encryption.key`|The key to use for `StaticKeyProvider`. The key format is hex-encoded (`0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210`) but can also be encrypted using the `./encrypt-config.sh` tool in NiFi Toolkit (see the <> section in the link:toolkit-guide.html[NiFi Toolkit Guide] for more information). |`nifi.provenance.repository.encryption.key.id.`*|Allows for additional keys to be specified for the `StaticKeyProvider`. For example, the line `nifi.provenance.repository.encryption.key.id.Key2=012...210` would provide an available key `Key2`. |==== @@ -3839,7 +3010,7 @@ Providing three total network interfaces, including `nifi.web.https.network.int |`nifi.web.proxy.host`|A comma separated list of allowed HTTP Host header values to consider when NiFi is running securely and will be receiving requests to a different host[:port] than it is bound to. For example, when running in a Docker container or behind a proxy (e.g. localhost:18443, proxyhost:443). By default, this value is blank meaning NiFi should only allow requests sent to the host[:port] that NiFi is bound to. -|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath or X-Forwarded-Context header values to consider. By default, this value is +|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header values to consider. By default, this value is blank meaning all requests containing a proxy context path are rejected. Configuring this property would allow requests where the proxy path is contained in this listing. |==== @@ -3862,7 +3033,6 @@ These properties pertain to various security features in NiFi. Many of these pro |`nifi.security.truststore`*|The full path and name of the truststore. It is blank by default. |`nifi.security.truststoreType`|The truststore type. It is blank by default. |`nifi.security.truststorePasswd`|The truststore password. It is blank by default. -|`nifi.security.needClientAuth`|This indicates whether client authentication in the cluster protocol. It is blank by default. |`nifi.security.user.authorizer`|Specifies which of the configured Authorizers in the _authorizers.xml_ file to use. By default, it is set to `file-provider`. |`nifi.security.user.login.identity.provider`|This indicates what type of login identity provider to use. The default value is blank, can be set to the identifier from a provider in the file specified in `nifi.login.identity.provider.configuration.file`. Setting this property will trigger NiFi to support username/password authentication. @@ -3901,6 +3071,7 @@ nifi.security.group.mapping.transform.anygroup=LOWER NOTE: These mappings are applied to any legacy groups referenced in the _authorizers.xml_ as well as groups imported from LDAP. +[[cluster_common_properties]] === Cluster Common Properties When setting up a NiFi cluster, these properties should be configured the same way on all nodes. @@ -3911,6 +3082,7 @@ When setting up a NiFi cluster, these properties should be configured the same w |`nifi.cluster.protocol.is.secure`|This indicates whether cluster communications are secure. The default value is `false`. |==== +[[cluster_node_properties]] === Cluster Node Properties Configure these properties for cluster nodes. @@ -3939,8 +3111,10 @@ to the cluster. It provides an additional layer of security. This value is blank |`nifi.cluster.flow.election.max.candidates`|Specifies the number of Nodes required in the cluster to cause early election of Flows. This allows the Nodes in the cluster to avoid having to wait a long time before starting processing if we reach at least this number of nodes in the cluster. |`nifi.cluster.load.balance.port`|Specifies the port to listen on for incoming connections for load balancing data across the cluster. The default value is `6342`. -|`nifi.cluster.load.balance.host`|Specifies the hostname to listen on for incoming connections for load balancing data across the cluster. If not specified, will default to the value used by the `nifi -.cluster.node.address` property. +|`nifi.cluster.load.balance.host`|Specifies the hostname to listen on for incoming connections for load balancing data across the cluster. If not specified, will default to the value used by the `nifi.cluster.node.address` property. +|`nifi.cluster.load.balance.connections.per.node`|The maximum number of connections to create between this node and each other node in the cluster. For example, if there are 5 nodes in the cluster and this value is set to 4, there will be up to 20 socket connections established for load-balancing purposes (5 x 4 = 20). The default value is `4`. +|`nifi.cluster.load.balance.max.thread.count`|The maximum number of threads to use for transferring data from this node to other nodes in the cluster. If this value is set to 8, for example, there will be up to 8 threads responsible for transferring data to other nodes, regardless of how many nodes are in the cluster. While a given thread can only write to a single socket at a time, a single thread is capable of servicing multiple connections simultaneously because a given connection may not be available for reading/writing at any given time. The default value is `8`. +|`nifi.cluster.load.balance.comms.timeout`|When communicating with another node, if this amount of time elapses without making any progress when reading from or writing to a socket, then a TimeoutException will be thrown. This will then result in the data either being retried or sent to another node in the cluster, depending on the configured Load Balancing Strategy. The default value is `30 sec`. |==== [[claim_management]] diff --git a/nifi-docs/src/main/asciidoc/developer-guide.adoc b/nifi-docs/src/main/asciidoc/developer-guide.adoc index f35feacd1e97..d357822bda2b 100644 --- a/nifi-docs/src/main/asciidoc/developer-guide.adoc +++ b/nifi-docs/src/main/asciidoc/developer-guide.adoc @@ -132,7 +132,7 @@ attribute within the `CoreAttributes` enum. - Filename ("filename"): The filename of the FlowFile. The filename should not contain any directory structure. -- UUID ("uuid"): A unique universally unique identifier (UUID) assigned to this FlowFile. +- UUID ("uuid"): A Universally Unique Identifier assigned to this FlowFile that distinguishes the FlowFile from other FlowFiles in the system. - Path ("path"): The FlowFile's path indicates the relative directory to which a FlowFile belongs and does not contain the filename. diff --git a/nifi-docs/src/main/asciidoc/images/cluster_connection_summary.png b/nifi-docs/src/main/asciidoc/images/cluster_connection_summary.png new file mode 100644 index 0000000000000000000000000000000000000000..e1df9cc405a3bb54709d9b8f03a620ea9ba3f37c GIT binary patch literal 106868 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≪agEMA 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 KzN}C*5Bq=ornCS6 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/connection-settings.png b/nifi-docs/src/main/asciidoc/images/connection-settings.png index 60b18f5b08a9408c3544946ea2e1900779ddf74e..40f57e1a04737c886223453b6f46c102db259f14 100644 GIT binary patch literal 73259 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 DrP8HR diff --git a/nifi-docs/src/main/asciidoc/images/disconnected-node-cluster-mgt.png b/nifi-docs/src/main/asciidoc/images/disconnected-node-cluster-mgt.png new file mode 100644 index 0000000000000000000000000000000000000000..41e20796e4740ff53e10730a5fefd1565a6ffb94 GIT binary patch literal 92532 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/iconConnect.png b/nifi-docs/src/main/asciidoc/images/iconConnect.png new file mode 100644 index 0000000000000000000000000000000000000000..8b613a15456f08ea690c0eeae5ca3ad4669cd177 GIT binary patch literal 595 zcmV-Z0<8UsP)Px%4M{{nR7ef&l1(UVVHn5%#|$D8x|W2gOWiRNWhf#BvoXoSQlqfAD~htQv>*$t zZui#pF}@adHk5Oe1>L*RZDujb*Iihke2ptJb6(FmIK6YtyiLlAr_MRg`#kUSJMa0Q zzr-l;Mv?GFTpjxk;m4<^2;AKXRehB#9z{WUcMrx_R)lb~u*)?=;V{f56Etww=omoj zlKcX0$ICP>_{h>S{u~~nzSM@VlT(n$y*~yA(Q2>MiGR<|ke!~6)a2yIH)uW=#e$+R zQ&TfTLnDKuiDk69*u1P5Bc2VJWl?Q-cFw3hC&H7TUtMG9=QJr02F$cI7=+bgL0eUo ze!_WK)+HhsN()~~B!vIo^W*5`JW536?*jvH+3orZ`I``a zaT9aVsIbvZx4UmO`@Ke{c;TN!a%Ju;uk~xX4r}%i`uHeESCkq9OX38c+|CDwV7^ z;&#JXQ4vYxR%BWFz9>Oa13DIi2XJtuc-wkt|Qb7&TE6Qx?%sygi{? h;)TT8_W_Tp%mc~kD8tAv1vdZy002ovPDHLkV1mN15bpp0 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/iconDetails.png b/nifi-docs/src/main/asciidoc/images/iconDetails.png index f4fb4d48b52aa6face9148eb412069304e9f866d..28166deac7541314264fba3052f82ce6458031d2 100644 GIT binary patch literal 704 zcmV;x0zdtUP)Px%dPzhQ%h(QQ4~EhX_GV`G#|9smI@*r(M9Y=u|Z7Ig-GhcDzxaX{=iSW zaibgERY534F!i%^BgSHhl;A=vYK0amqM#tQDUp&k!8DfqljnVtw#=AlvdDqVdzp9d zx%ZuOM_>fh5n>&=WU9IoHvF%{#pPwZnw!I?AM+>_i`d)Vj_&;q>~3lKXPQt+!(5?& zk@0cdedb3b7K5v^6Ac=X$UnknG~j~QiyQs@Fz9sU#GFtG>VysX#u!{~22VHJdh!&@ z<@0h#$G}+zw};_~i=DoGho!SwWvaX{yYln5?=a%`16r+IJ-(X2wMUPkYivX$9tUXU zcoGcapd{h)@gDjA#zSQab%N1}7XazSVbJR_aO@}&$t1$jDDotQ3_$=y5%&WDtff+= zck548haocI^Fj#FXaE{dtI3RqR|g@80{F1*Id>l4LhI*5_9kg)Pyr` z4?yN{W42kWcs)FfEk+~s;h!jyQM*XvUS}6hIGr$)Je5$f!@mQ$yL@1{In_`T mVKoxvdG(UY=oPx#oKQ?uMF2=i07*&$PEQ9?Qw>yA4^~$bSz8!fT_0d!Az@-JYiu%ZZ#QvrJalzE zb$3H~dP;qNO@M(+9<9@aypK_xbty{QUd;{QUj> z{{H^{|NsC0|2B7bxBvhEd`Uz>R4C8Q(MJ}7KoErCA9cjRoB^|g2?Gv&|GRmB>E~8! z`z`8q7xE?i80+V8_wYKt7LEQq>fJP~X0Z2QO9j_Jy910BoI)mVpjm}zV0aY(7h)GU z2*5qWB_xH6AW=HchTQuLY_&U2re$p$ZS)V7tH~gBe>7Q;5BA$=1Bp!&0ssI207*qo IM6N<$g6vqUSpWb4 diff --git a/nifi-docs/src/main/asciidoc/images/iconDisconnect.png b/nifi-docs/src/main/asciidoc/images/iconDisconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..d9a5eab2802d166c03f3386edbe7bf43f5ffdbc5 GIT binary patch literal 618 zcmV-w0+s!VP)Px%BuPX;R7ef&lut<0VHn3hn?twRhEB+tB?Lw^FA^R4qcE{UR3H_oM0m+V$8ez` zoxBKn3U38#{Xsek9U`lV7fVLaify1~+eilrv)XhU`}6Dl4Yq^({pKIoslM>zeR!Vt z`8+Sr^Lt;7Bt(Uztq?bl{RiOjM1n{(y00$oxhu zNM|$TM1gEB%V=*eJr}$A=JWA-VPSvpuRntP+Ss7Gy`9#krhIa9LjxAGnOcJZtyarT zyB%RLAMmTfbxM(VG6|Us*82Klcw%sn`+a>h*45>GM?^8i41RBI75D5k6yePl3mA=f zgF%pA9t|88Q28t9^U^Y6x*WG!3$Bt?vFE2xv~tFFidm0`S95bENg*+lN7K{j%_e%z zo-2tL9rCNFB=;AuRCDRFWV~`!dhqPIq=t;Zq<$Lfq80 zV@K)l>dOB~YS4mzS9oFLK~}|q9t5P6^B=C%6)M`6zgHglC&UTWTmS$707*qoM6N<$ Ef|vFo&;S4c literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/iconDownloadTemplate.png b/nifi-docs/src/main/asciidoc/images/iconDownloadTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..1bfc1cf3f10c4d6b4e6a9135260915aecd3f47bd GIT binary patch literal 929 zcmV;S177@zP)Px&TS-JgR7efomRU$tVHC&zv$~cV4l1dnCZc36g=vCdVGtxeL{`#+FA)_AiM_Nu z*OQdJm>^bfK@@}(NDC%rwzz~#+1tF8X6KR7%fvUnk_3| zrD`8A8l{0A^1Nqw81oBuXfz6mr%p zqCl(NhLqS??2Frj_bn}mPfJ79 zIY6Vup`;{%DGF3zX@1oDmT{~z`Cor zTl5-NS7c}?qM~Px%(n&-?R7efAmHSTBM#iR)CGVVN4h&xFL|;cMD!d+yeV)a$*W<8h znqU~Gc=IU)r_+H4H+#)&Sm3|*!gvv!q!8pNmSuP{JcPEUI=R5`R8Ek>;jr5@j84wr zVqGm-8*1fiOSZ7r;blUzFkw@VTGDkWlW@g1*7$y>)NTu+VvVx`Qd<}X6 zEkZB!OCyICSJ%O(H$=ksBQZqchhjd^R4?Od82)cm$@^F$iQR*!G?nl;z2H3NORDmF zr9m+Yr)W4DlS?FAQB+Dg5+x@tR9Y0NsKhZuYHmt9M`?*Dm@QM>48uZ3zSwfA2}2~@ z?{mZNanrnl`K47Vpd-Vkpph%sORb=as@4z*FSXmz-PSBpX>Dr<6JO>Gn}Wt8Y7v(y z5%R*B(A(aMa}_>hX$vm@>A8i1z+BBP3hS+_?G}6KInQ#u9uEzO&X-K5@$^-Yp0`-t z+@?%Qi-u)cO+Pq8xN?UFdeKPrVV1m&Ycli=&x7v~jVGzRfik-2D!&gIx`&-`M3g*t zMP1in*KD}g-z^@_b+hD!v%&Jl7DmQDVCm;3{`^Vg1XNWO;Vn0>3pJIv-`|7ImPRvE z>c_&_f{*-z;G8|%`v+9P3Am~IRZ?bMYN!)U>hCry$)ubJHyfTtw_NCH04J?EQ}}N> W{~d6WgK{PS0000Px%2T4RhR7ef&Q%f%cQ562>k(6pnwbDgNZ1lxW*wjLTO@Dv|(TIf=^$%Ex=YkCz z8)E4ZJCBeiVu_?(VkaSCAwf#p=}etF(J^#pgv8ECZsy!`&-vy%=YE$flHrex|07%# z_8;7Ex}st9J$QO~g^TNJbr)=CKwB_im0o_v)M8|^q7++4MvI7csH<%>QOrq_QL1Uo zoH;2}DinaqN|jMTmO&^QyF8tVJP)3hnVFwZ1;=q(9RvT^0V}(^=$)Fz)60vN^VP8g z*OKWc(dGyQL4ZQ+SqBf#&)D8Sz|g`X{oqwg)jQ^6DP<9mug;6%SPXr$b9l^TAkZkT zZf^lc6}FQ}>X14~jKS@6l&aM4_uz^GkJF`sZk(I|*({qEAW|uH0-Ft6)Io^UG#QWo z5S*B2a#^_Sb~O5Ycp~QP5;M6RUTGnpMN)mynPX&Yhle;$rHL$$XiEzQiLL%&8P(`xmf-rt-}she`3<1H<{w-? b4b6W8Sat0im%Gw!00000NkvXXu0mjfQI`$Z literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/load_balance_active_connection.png b/nifi-docs/src/main/asciidoc/images/load_balance_active_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..cb4750d592f79b2075b42066981f9457f180fd07 GIT binary patch literal 35045 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

JaPJM2|s3stq?2;%@m@k<)F0k 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/scheduling-tab.png b/nifi-docs/src/main/asciidoc/images/scheduling-tab.png index 0ee2280a0e466e27994805738df2a37df954cd71..d82c8511e00ef9ee4d9063a2ebd2490df5682aa9 100644 GIT binary patch literal 54320 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 diff --git a/nifi-docs/src/main/asciidoc/images/summary_connections.png b/nifi-docs/src/main/asciidoc/images/summary_connections.png new file mode 100644 index 0000000000000000000000000000000000000000..267c64943ccfe1d41b9eba02a60ae2c35badd56d GIT binary patch literal 113698 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 z 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

~Qx+|Fh7JV9ZAG3bY&u{p?3Mu5-CVa{CL z^0+)DLn%~ zTxJaDC0>SLPnNw01#hOX^C*089QSQM@q_sltH5JUOPc^P<_hD;5Je-r5_JNtk-Z%o zt!4bqbf^tGTY!#bGoGR4eBja3cQT^yCq)z!;^4S)>SG?OI+OThQH7Q!D2dOz?{hZF zG$|jOVz<>33Q4)8E6PLu7hh)`R7dbUc--Axf(LiE2SFe1?m>dPy99Ul;1K-b?(Xgo z+#P~ExP0&KcU32Ke{SvW>`YBhZ+B16r)3pi?gU!X=ylbz^u0=8nDu}9ZG*C|EgJtW zRS0st;sC)SK83D!bJ`KSHiwSi()AMQV_sqD2oGkkD|E@P4)|M>UYttdGw3N;IGrd^ zr_8fmixZXph&^hct;g)Nt4Rb-BA-*Av%(^~`qZoHbf+}l$@;cUMG^@Q=IFwS0&`=R zYTQ(-^$E+*Ry3x_Do?Xy%WL%8BG1RO=5bwl;#{x&2i#T{4CR#C3LIm?Q+0S>;wZgl z_)Pp)eBo$g;QYNpd7mfL>h=*3bwml$6wh(a5-Zdg$bIa~OT;MQwT1+_y5;uwZS2T^ zGTsuG8GevUzvP3*ijhDaTXoYmUEh{71Ub^eBab=h8ktz$Fe@=RF~BJ*ynf+yqI7Q= zs)~blR=aT_F>ZFNM!ynFr!;N5Tw~V%B2I@Z+3FC6%=0Ge)=8nd?STrq zOz8s5=@Lr!EJanznTs=l(oZgvRCZpob5~MApo$X@$YCl|$--d8_CW6XL+T0PIa6@q z0(LY-r(PWSIoQND$kdC!lwAmXa$?yXFN76)qvzI?1|V4W;UqN8;%4(#EocI?EVB8< z0zTOd*T3y8M>8O&Rl?0k0S&K%b6FKXn5 zlr>UR9c!%fbMZm!DV$y4llWn;;<%6=jRvO`UKq}BDno1`hhYQ*WG8VwhQeORg+pbp zIdOngSi@foyaa_rMm=unEFi}XWc(lW{}^*bNJOhzn9!tJvv4xd zPimlACysvXqj)`zQ!A|yp4p5Al(UrW8l)%xz)+c)a=w74A)_RI{ zpPkG6Uu|qk*vmL&wRlnc8Sgr2x+U8qs8m9D0#?``2EFn1lZyw&q=mGFY2??L4sqw3 z%g2h*Yx|xGAAygoLZKU7RNVF~n<`dQ%znf5LcYL9b_d@T6hi`%$))>LKM;a8j57t4 zCZ99f&tb*fT)}wkO_TxEk=c4R3KfW8bRU13_a!c%hck~rerS>lT&B{?Gm6Dw+t9nAh*%dx1+ao!hlv}tp^DWVeLg7C3E9A~4|Q;!SGEFqU>39rzq z!h?GZVKYv1L#aTEk;#tP=ubU&(zmqOp~h!?(yl+E4&+nAKC-#!D5b*o`ZD4^-g@MUkb=En*#Yvw8UGTX^hc{^aHu-|$#74}8{lq3R$%L`$^A zzXjj1HYn)8i%|tsYPRZ~OVFPL5glZAc`-ar1qER|Y}%g;yTxa0gz{AHoKEIX`nB-mNYG_=JGaK$f|l5v-Tu|?NVbnU@goK;(u*rIy-TwVpokW zel=-}ll*o{=Je!dUgy=1s^!%3tJ|@m*PSRLKhu#-1PQb@;#`vHRz^@)0I{@zpTVUd zvLKqxi}vKJN>uD!W_)lrj@R=0$0nNGC7-nVj~VeIf~*NPoZ;o=ay6Hqn2sGkRXO;cv0YsgLE}NQc;G#C;^=XfxU1k ztx;9gnGLyqs~F^sekz5~T17OF%L*t+ctPHVU>sxhro>&%Iw1v#ak-&lC9?;AueiT$ zyYQo6um5szFq((I6q0S}a7Ge@ z))d+1IN}sNd@+WtMnBa4T?#?67R-GrV@G7e=29Pbc#uZ7Oe28QmjS1}ZrrlhLh-n|N*#!WbF&3#@2PjBnNFhTLdLwdxgx;WE-_b#i*0=2;u;Bxf~G-TPGsmog2J?6oK;z$0%wwl@k1<+?24s+bAQ*b_)e#^ zq`|>OvimaW_rOIm1qokGvh6eMD=AAF{=+HF~H|K0o47X!r=Es43~=TUUQB;9=&|pc;lQYL0-$7o_}3n8A_bh$ zI@Hy6^;fZ<2GWltUKy;BsQV zE%=W<9$4=yB^ajVGE>)SDbkQlHN}AOiQ?|;8I0I6TsJuw2+at$a z#aDR5i!yR+`z1!vIko3=iq|IhS1Ei;heCv3+MlWV63Ag}SqH_mpMrEP>hZ@JQg-ys zut>**4oqyOokMlP5yRSE>#!RXBkKDZmc#c;`JApl%!ya$_RhFEL9xo(<7)gMVan^x zxPR*nTPpm}Johs4se|@cjia8;bsVpih^IH=eHOutY>_Pcu_435f$2zDot9MRn&K*B zLU%EAi?@nj zRH-gs$n)>d`SfPM>-M7VIShVit3rkz>FRe+09FeS+~^tB?9iA2D0bU~tG1fVTz4Uz z+i6f#?)ce>;e}%F;dYWuk&20qpGUxo8k5wG zNwUw`Ts+D6WHP*rdEd)sdwk_%I%$`JtwP>*m^ge-7#^)66cihNF9_vhzhS1mtYmX@ z14DBZ`&V^VS|s6iHhx_JNB6t?5B$@yr^m5cHJsXw4Ze3;{7U+F8Y6(o5pZ|vHpvV( zY?O(70zgR#i}>+z8t6LJ{HCxsfi>$>Li z@Wa?e#g+S8x4IwbdBGjLJC{gUjQbmP$dJX2XW6&D}I)~f6c~0)5 zS3zu5e?-!66I6-}Ft1`u#DcLL$Real^CapjX@ckWi^Iyu97U+5LT~_T zMIpml6{&r=Dgy{<19=)(dWUDRHKkMvPy0CfjtPfyBwCeYvvmGx`S-8R&S5qNW6i4lnaZ>Wy^~SP=HynL(W> z{zaHClFGokGww3vcHSvz3>Sw3Zt=t9+24NOA+WjaMu`YzD&y_@pI^Iv$sO!#D3zM<@#ndpGbgoGvc!J zl^42nR}`e$@K}};6CK@G^Mlcg%;kZHOu9efqBXrP@|bocn8?c#l{`Ok{>EWDdUz4^ zbO_fOC^uzhs2Go9SDqZ()zTV`ba-zNal=70{>ivQvsP{lj04%|esVmJZ|mokUJC#4wX3*I%jxpy4a z`2@RIyGZS178D7h;~>Y(Q z*9`A6E|}y4V{x8LO5Yu@p`6j#BJ3gf6P$QJfgimcuoUj&5&~KRYab`m2X!~kpW4P$`B^1E~%0Fc( z(N4`hbv(Rd{!N7tF|3IGsxz__L7H2L6Pxgn!iI?bPfpT%iB~7!_gtFll)Qq(iVh~a zflN#A?8n41LPB^>)hxZEW}BHdaI4ub;vj6>?OS2<#pZ7VIcC>F+Z~t@5S+Pw7rw+?}~En_VM0n(y-Bp z8&MhxP%&R;$wogCe^uP-5E2X^a0Gwz#3q$Uc_SiF70XD z&o6TIdKl_{JhiA%dK|Z4)J>4jU?eA!2t5_SOGSGN)ZemhyMC#7&h)?FR9z^G4>%6p z>j1gZbQTv|x1b%W?mOhsXzz&%V;fi`+PqN+MihEHB3?YVyBV%Q1n*l2e!f6E+UJX!;ilxsXJB zv*pO-(%Pr|3nkg2edvq$Ze4l5hb64N?Mub5?rom)xRpiRYBkS3%jYgxK?Q@m2=ez$ z-&|~Xm-7kHKNcic@7b-KJ_;p`n$}ZQZ+h1RrYF{SHFm?&IEGd;(}d5P^~F8BJNw8b z6@4)uuD;7`c}m>Ee;E|U()Xrw;guZA@?NbR<{cxP%A^B6XB5dEN$*NcXh&$9!U*?py$oU#H@5`u`6J`#9?n6{T~10z0%UjL8g$W*e z5b&2^XjyrQj_61E=wzV^2)|FKt0gB+#&mAocR%jSWr4?h7W9`eK{e06nwdEjo;Yzc z6y**1<8Mg->#r2x0ladY_W8_>;I(`6HHGQaetQE9{4K1WpNtR4x?$1vUij0vVWG}T zFubMNxlPE&l3M55PQb^Q!Q`8*IU=zyfu?$Ug<$Syl4*ZYlk}Z zOB}|XfYN{eEz?Ya>)q3XWdr-yo3(T4fM;Hj;CVd*sht}dy4cLz9CJy_M~@^qDz00v z68*UJ_!P0W-C+hgeE&a$|e&T>9l~uJp5Tk5fm? z-4)t^Pk@src$1**T0x(%BsDR>hNJ?V!>fSp@x4}2moP$Po8XT^HkWDs<=Hw%3||pP z`~D&u;a~yRQhP>JSWm&nS%_ecpztgC^*_hV@A_KMgXrnwT|96#fnT2Z7k#e#XX`LQ>4b`XC%9yi8<%n+Lddbgawbx- zJX5I*T?Hh1*R2|bp%$~^Ln9+SM}_+9ENF2y z<~Y_h2l@#h5n6n`3@CtDvqyz)mgJDQaS4TNj6XgajKiotl$H2TaFY8~ zsLMOso^+~HOO9$+5Eqx@L3>q+Lk*SnE7Tf0moRPH(#X63XNucVS|+0PK#l`~xzC=A zl}titM3@X)9M^a~8dzObwSj1x65TmRE!o8u(?xYSnZn5>E(Zh}WpHienqWwg%T|(a z0eK!Qmf;8-EkdpOlH)_l@9g&{-y6ODRg^!@Q5wfi>z#iM#mIy}lnufP=Pz}`gJgwT zGvu3ZbTg0zmS6oA^LEKs%bU}wUMHa`iH?vD^0oTu4tbTMYENUO^3gAp#Eb5ps-v5u zrfW{?WD{S#>Z|_MAO}BKRlaf3IE}PF0n@0SL9Q}&jFGZ1&3-M2wk#=PaL`<*jYiHU zWr9vz{GhXXLIGirj(Xy(y1>C}mUniZmv@e*go)yZt!>ip4NPY4iDRv>?i7zFS`SJH zU1@UpB!v!A-Q${m3n%MVWg?ukp~UqQXXyWQ8&Hjjr~tLh?>Q^AJTUNw#wbtu9ilBa zA%CIS%qo#L9#=hA4PegW1WLcD;{)ZgF_QdaNUTwiXlUj(<9!dKS_>pA%YsFcMj@?s zJWyAe?A}q~1H2wde-c^!yP2+Bmdtrm&=(|I;1WE~PuI9m20h?)%WOqT5h?fM4!BH; z4#}1dSvZ1hypvn@pD9B?`FFS^;l2cs!Ceg`bZl)mK|9n88l?>#HRs7;q0q5I=Ld_^ zih}y48K6pYZ*5N96=j`tBg;zmt-o1_PKP6CzkZkal@r9&cA68CPBa6-(Z(t#alXms zA<($Ex5;rl!#cwxb!>UDX2j?0KR=YV{(mm8|1P>c2r%Lk*;rkY7hgtUqYsZ+DP0uD zT&`8R#p!j~nh#(kEWQ$OP&9>RxgeOi*!)lZ@V~8>24<*#j_!-5rT~%z0UZn#BadCO z7vu7tF*1VmiRH12P9!LH0v)PN$%PsNfXS5;E9gGa$xVFd=o+KW=vT;9t}EVue}pM3 za|0dnC~%GdBY|aG(rZcTo^8w{9>_?k54mSt``r3RlJQ&-3i9dk-J)5ktTzV(`@ZpT zbd<()vcAedC*D)B@5%h!dYQ_?41|#51v7c(4=_EN7ueV*qgw=WHtv5rk~5JIXTx&3 zse5m6ssfe!Sf6Q+d^?p~3{xX~6RV=0ME@}CRPW9Z&}+UFr7z6j45gf|iLFg}LOqk& zc3f5CPi>un8k%gTyU`eds!*;hPGMF7h6g;vsxxZ<3{RF6h>Q%Wj$Pv}^kH(_5;Eb<@^J#(3EyIVPK(oW z=Bl#Aiq}77$dX?tQ&ZCFHal$7;v1-|_BS}er>opn2cU@sy0H=_tD;TooPrdb0c7bs znhx2!oCa{0=5NcFe9R2-ILw-o&4tW3U)7b7@V+LPc+zpq1UO{nt?=I^GAajgB^;gK zV+A#eqRg#GlL=A(Shi1Lto^-@APco>)KJ^U3hDQcOkE``_$-!&ML#5uBb@+5l2H_s z_Q0~fx;60C&za0`RIUMh1Zz9Ir&3Kxx1EU7$rVSC3#eu%@9RWK2y$cNAqlxo8ow0= zz+K|s&g60{*{Iht!A9c@)75}B_lujgx;fEa*zI^Ne5VSgZ?NzEu+9p#%1ga0j4;k6 z^EkFMK?woA*!6S(_lr&D^py@2lf87}90oz_RE%WZFSsy?mO*C36Skid?szck#Or+Q ze91Z`)fMP0?_A8w_(r6huDnhq53n^3z|N;Eei(XXG>TynvT1p8Y~=N^IOdJR*}?(U zMosQAQAO*0TNi8VUj7mHP*!y|(c!M(%xhw$B2f_%FG9ab%#^6YNrZ0FVUF)9oK zYGQZk(I29pG|Xo!ihb^9e-549nxe4q;0z}>Jo4yqAFktlZ<;2x+jYY?=cNR=T1m{T z5+*~RMo%JpM$;FL$5!IelucyaiM6m*mPb#lBBhfN2may`J$@txwa(f>6?h)u$!~nOfROz?bG2MLdAM_GTUrlyaL{n-%-r)%m61uzE z$1hj7Q`*smXk5~(6XTS6XhntCWwH)nAQ6ccDi5nH?(!E7PxgvboAs?vXS+*qzEEZ8 zgzuvyP$E`sL~0|VB06#AoMOvKlJ(^MZMs-%oGF8Ze_Ok$Qm^RL_Zm8!cj$E_d>&U* zEsWM{T=^};vT`V?&USwp+qrPzC>&Z z6BbQX6RD`gqjA-G2OZY|$yCyDa{I%m#VY&Z)^D=2CYyibX@rE-rG(f~_$=f0WMI^0 zUL}u?%ng)f%7OXpN_Z^#Uqj%YCHyJsZl~B4d1^>-$+WM7kB-nqKWN{cNE0eG7{?Vz z9TH||*(TGmE#_x>#wwT2n`E4<=up{p+PbZ=QPyNR;GyCxv>7ImnP=ii^<-b<$4g46 zdV(D-?utbZH?HZ%R@3uR=-E<5-*2*QH(S5Sr7^LzxY{#*W2J}s9b!H`*~a0r3}qKh z67W@DRa3yjCgjENr>%C2ivQ~idSrBD>h~$)e<7EKwvpvt3>~&-Bb~Y%(YB@^@C;Dm z-;t8zIX)go)YnkluJeQ)#M_b%qm{Z7Wq`dWcZU;k``O-hYv^0foXXtYJ!`<%seyrk+$XfnX500z@@cU6pzy{p4yT&vbIVof)STe1~EQWHvcJq`n3<3kz) z`?gzlK9tY`Yj6N9R&%R`v2V~Z;t@CggV<&ViXd2|7A+<{+GJXA^<$CSd8uj@4;~FF zSlqz|R_N~J{sdoF_LwDC3_m?^inQhULvp{8&NEB;X(T`3HsoTnNm{_&CW1>CnZz2f z5#fqZAW}R#d!OX1gwWJ?N^$T`2=+|39G2f{RyaKD-lE9>@F2-xpdE(^-kNjaaiH{1 z8+9O#U`mggKAdY5g7bNq)Q^7a(nZ7%jc!XwAKWBbF&KiBUtdoS{^Wqs0itU34u&x% zUK(sTNPEgs{g~6$`;=mlaj~hv1e~35?`E#l?n2rg%a~%!Q#35}+TCxK-M){{Oh3;7 zG+ahxUi&LOB~xm(oUv+)5M-IT0J1>oo z?>_c;@KN69SM+#ESM*HOoBct;4=~3&@jR4E@So5S<{K`1 z(k19|Fb#CjtTv=WNAn_j>F+fakN|Jv)9sJ6AX?oxbb&K%H=5P%4kjXG5#vmTSgEUawJh+$XClrp@3kBf5S6DC_G_Efov^@^3X321SdYq{Fcq zy`1F3!IK{`eEb~P-LGkv`-1o|LZf+V3Wn$*2uZyW@sCqquKp1#VJsUfGHE z@GfIo80M_*x=?T_5ge5MbN$n7^~mh~YV zgu&9l4@!)J5+mG-J)T?#w=iSxTbk9G` zoWvV>zt~+SH;M8jT6>aTkFw9V;sh)*{5@t&(1P3cXz{(7Tbb2e3vSHCVoZijMYxrATLn7%KR<+nBLt=|0Ya;}WfZ24yJKtz^X?GVl?fW~`yVYu!G=U={<;Ff_u+%aB3y+5=oK1QrWO%70SrbK*~A)r zV!8XN!A1LEf^e21wj;l0hE1=eL{OJT9E)p%FOOY~8WfxH^VXuA)aJ6z7qp9H9;NQD z=Pj2|N-?PgOT^}5;nnju^Ob8ZxR9tcB0N%t{)Yx_m&)%;i_KOfML2PSv-$MqI*d>KNS#)Wasx*>bXBU+#nBk%k zWTrmz1d#8Fl&#kvh^&6$Xnc2BII31{bu`h5UIJ+{XBp4RiM28aApVZV{kxvNlH}*} ze0DbN%SLT3u+ii|35$fAHVO@YaLvH!_Gj+lpTEr4JTV(q=yJY!DrLQTA|RBx)Bv&8 z=GN#=gi^$b2K;WRn9qF9Ynzy*oZS6K?f@SQyHqNl+K!>iY0(!$mR2&Md1m87pE2Wx zF;5aMrO@@tq=#n>$44mlM$VTsmnY!Yejyp*|6;LJf~NW2mLzO>BxF$kOA|f+IGB<# z>{8ESB30!*9Z>Cf8^CzbpM*d9*h`b5_fxFJFCjcR^LRWUd00!fq5(f-kRJL^2D@9S zZ9h-6fc9mjpO-?39u~pRfYKSrqE?O5wVSWyVGR}7l(NTS zu6cq;e}ef}eMm!*Ff%|3*>T-ajQEQj^pOIkj8VNV?3$73wc5}ED-g99)Ubt5dhIAD z&{BRw^M}qjC{*XY*8#wE!K+TxQJrK>S9`bZ$iZ(mP5z0(E5|Bn98EI{CL6J}=XIO_-k)kH4jgcHAwpy-`RzBv#mQ(NWj33!4Xop`lC5d&5i0rj^ ze@FZcu0!%hxOC^AXDUtpW1ubEWjd~I4W$rB@lc=5YpZJFR{txkgkfD@n~d#f^pgc4 zxa^l9JL`(LOUc0y%^VqPLfWj{Zqy;J)))U~JtO}HQq+bo5*aA0+I8x0MJh_DkkIxw=;P00k%?TYokD5IjJ}7}l z2V8Ak$9A6AdOG2OeS=OW``|G+)DKX z8BpejpO4JOlW>mfVC3;gKHYiqbYc7^`lnC>xzBn}cyD!|#_WY7{}bd=0nx_>`9kqO zY<;CW!Pg*Q_7lDg83lfMc+@>))3DGgiOdslgSZ6$OuqZK&2MTE6-`eXI|wQ0QnD$Sj+E0HgP)njnU8w+pW&!@r$~LnJS>-G77~ zt%$$;4M{IVQPoB|L{Evhf{U1yIGO{V>>5gVtna-8Mg9-{Y47?()2H<;td|P z*CoMLLa#WXO^VqnzLjL;-zj%s-n(G44B23-M-vXCjw&9rHVhX0xP@EqXTkhM@AJ9% z0^4E-;q0Iuyhy!%*xh`f9$&%-$HP)|?+*jj?iiaNDr(Vg=FBj4E);`|F?NC%Wym-z z^te@;bM0gGuiZg@D)`eB*C$_pGE7}@l(ooky0N^GpgCRD#Afh^vz3HCF*3= zqezV}5d08JY!EiPI+H`fTIh9WGS6mM)AouKYjS4vISQS@XS>}1U1gO&adNzqU@l|# z8GJP9XDrtptb6(eK^1OO6z~Diu3>TLgIGXcr^~7PMC=Jd7?CB+j0_6NPqEA=r<<7F z8-$n$3XvW^&i{lk8KbqTi%Age>m(UC+u#A7VX?m$_Be40EJ?0Kq&B?Wf3+9Xzr)w(U`D(q{hWS@q%%r-u3wkUfkPpoH2M{iUclycn%d$P zCU+lL=X+GF4Rt^2vGe|AAUr;;nqXD*<=v+%UP)%&uNi#hCkaa=pH`D%2o4{$vEeI- ziz9r)Q~wkVZk}SXZSs_&HJN7?-)k4(eOmGmJHJ%^=BH>lVnYMQH%@)Xdm~Futy(rq z1#7i+%wUugNmOS~2DzPLLJYLPLPd9j>wh8lK{c_wK9JL9FFVXX-EI^!0K)xWqMF*< zUC}jr+GzS`p97S!yMKLH7=Jh0osW^7KT}nk=SDEDh!^`BOAS)VE|c$r!1xgJB2oUhL4i7L+ucvP{i z1iAJnEV@a_AVT2&a4_HE?@xU7t4Z&nb`S=jpkw00xA$7Gqzk@H5x765;&1$!sy7l!(+VCfJu|DwK3sPfy% zoRtB!r^N;R%U(w$R=`EM)R5gjb0MW)_*~<+zOyVLsH7_^ele5Wq7?CKj*vIC+{Zwx z>{OCDaQ*Xn1IK#%K^GUN?vhyAWU}5Yf{jwY>~XoG=mOAR!V>yGT~+X3EPySwdbUJC zTJy++;||)iK?pybw~>EA45_>Tr(osPze8H0W2xG`g<+LdxYO@ zy~&D>;u^lQy|L{0SYNf;CA(kl{%shFvYG-&p|8_L14IlG+McprcTtbVkY{33O;?ec zoR@U@!(~0;kxHyc*4lv-81&gZcInY|tJvj4{c~1S7H&QYIL#uZ4vu`}&b$73q<2Hrn0?W;ci3ndmve49 z@XwX%V)--`j{`8&BEzD;J;wYiN5}puizIb2YpFKyo|^xWQbq17H$1?C__83nmR?y+ zfm-sp8vPLqyVWt=%^2_hUj1OdBx (uKpS4}oMx$q}&iz3uX4#r*xVRPLHMYgCK# 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�-wEyC-1nB+Ywh+4Vb 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/toolkit-guide.adoc b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc new file mode 100644 index 000000000000..304f79ca624e --- /dev/null +++ b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc @@ -0,0 +1,1279 @@ +// +// 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. +// += Apache NiFi Toolkit Guide +Apache NiFi Team +:homepage: http://nifi.apache.org +:linkattrs: + +== Overview +The NiFi Toolkit contains several command line utilities to setup and support NiFi in standalone and clustered environments. The utilities include: + +* CLI -- The `cli` tool enables administrators to interact with NiFi and NiFi Registry instances to automate tasks such as deploying versioned flows and managing process groups and cluster nodes. +* Encrypt Config -- The `encrypt-config` tool encrypts the sensitive keys in the _nifi.properties_ file to facilitate the setup of a secure NiFi instance. +* File Manager -- The `file-manager` tool enables administrators to backup, install or restore a NiFi installation from backup. +* Flow Analyzer -- The `flow-analyzer` tool produces a report that helps administrators understand the max amount of data which can be stored in backpressure for a given flow. +* Node Manager -- The `node-manager` tool enables administrators to perform status checks on nodes as well as the ability to connect, disconnect, or remove nodes from the cluster. +* Notify -- The `notify` tool enables administrators to send bulletins to the NiFi UI. +* S2S -- The `s2s` tool enables administrators to send data into or out of NiFi flows over site-to-site. +* TLS Toolkit -- The `tls-toolkit` utility generates the required keystores, truststore, and relevant configuration files to facilitate the setup of a secure NiFi instance. +* ZooKeeper Migrator -- The `zk-migrator` tool enables administrators to: +** move ZooKeeper information from one ZooKeeper cluster to another +** migrate ZooKeeper node ownership + +The utilities are executed with scripts found in the `bin` folder of your NiFi Toolkit installation. + +NOTE: The NiFi Toolkit is downloaded separately from NiFi (see the link:https://nifi.apache.org/download.html[Apache NiFi Downloads^] page). + +=== Prerequisites for Running in a Secure Environment +For secured nodes and clusters, two policies should be configured in advance: + +* Access the controller – A user that will have access to these utilities should be authorized in NiFi by creating an “access the controller” policy (`/controller`) with both view and modify rights +* Proxy user request – If not previously set, node’s identity (the DN value of the node’s certificate) should be authorized to proxy requests on behalf of a user + +When executing either the Notify or Node Manager tools in a secured environment the `proxyDN` flag option should be used in order to properly identify the user that was authorized to execute these commands. In non-secure environments, or if running the status operation on the Node Manager tool, the flag is ignored. + +[[nifi_CLI]] +== NiFi CLI +This tool offers a CLI focused on interacting with NiFi and NiFi Registry in order to automate tasks, such as deploying flows from a NIFi Registy to a NiFi instance or managing process groups and cluster nodes. + +=== Usage +The CLI toolkit can be executed in standalone mode to execute a single command, or interactive mode to enter an interactive shell. + +To execute a single command: + + ./bin/cli.sh + +To launch the interactive shell: + + ./bin/cli.sh + +To show help: + + ./bin/cli.sh -h + +The following are available commands: + + demo quick-import + nifi current-user + nifi cluster-summary + nifi connect-node + nifi delete-node + nifi disconnect-node + nifi get-root-id + nifi get-node + nifi get-nodes + nifi offload-node + nifi list-reg-clients + nifi create-reg-client + nifi update-reg-client + nifi get-reg-client-id + nifi pg-import + nifi pg-start + nifi pg-stop + nifi pg-get-vars + nifi pg-set-var + nifi pg-get-version + nifi pg-change-version + nifi pg-get-all-versions + nifi pg-list + nifi pg-status + nifi pg-get-services + nifi pg-enable-services + nifi pg-disable-services + registry current-user + registry list-buckets + registry create-bucket + registry delete-bucket + registry list-flows + registry create-flow + registry delete-flow + registry list-flow-versions + registry export-flow-version + registry import-flow-version + registry sync-flow-versions + registry transfer-flow-version + session keys + session show + session get + session set + session remove + session clear + exit + help + +To display extensive help for a specific command: + + ./bin/cli.sh -h + +=== Property/Argument Handling +Most commands will require specifying a baseUrl for the NiFi or NiFi Registry instance. + +An example command to list the buckets in a NiFi Registry instance would be the following: + + ./bin/cli.sh registry list-buckets -u http://localhost:18080 + +In order to avoid specifying the URL (and possibly other optional arguments for TLS) on every command, you can define a properties file containing the repetitive arguments. + +An example properties file for a local NiFi Registry instance would look like the following: + +``` + baseUrl=http://localhost:18080 + keystore= + keystoreType= + keystorePasswd= + keyPasswd= + truststore= + truststoreType= + truststorePasswd= + proxiedEntity= +``` + +This properties file can then be used on a command by specifying `-p`: + + ./bin/cli.sh registry list-buckets -p /path/to/local-nifi-registry.properties + +You could then maintain a properties file for each environment you plan to interact with, such as Dev, QA, and Prod. + +In addition to specifying a properties file on each command, you can setup a default properties file to be used in the event that no properties file is specified. + +The default properties file is specified using the `session` concept, which persists to the users home directory in a file called _.nifi-cli.config_. + +An example of setting the default property files for NiFi would be the following: + + ./bin/cli.sh session set nifi.props /path/to/local-nifi.properties + +An example for NiFi Registry would be the following: + + ./bin/cli.sh session set nifi.reg.props /path/to/local-nifi-registry.properties + +This will write the above properties into the _.nifi-cli.config_ in the user's home directory and will allow commands to be executed without specifying a URL or properties file: + + ./bin/cli.sh registry list-buckets + +The above command will now use the `baseUrl` from _local-nifi-registry.properties_. + +The order of resolving an argument is the following: + +* A direct argument overrides anything in a properties file or session +* A properties file argument (`-p`) overrides the session +* The session is used when nothing else is specified + +=== Security Configuration +If NiFi and NiFi Registry are secured, then commands executed from the CLI will need to make a TLS connection and authenticate as a user with permissions to perform the desired action. + +Currently the CLI supports authenticating with a client certificate and an optional proxied-entity. A common scenario would be running the CLI from one of the nodes where NiFi or NiFi Registry is installed, which allows the CLI to use the same keystore and truststore as the NiFi/NiFi Registry instance. + +The security configuration can be specified per-command, or in one of the properties files described in the previous section. + +The examples below are for NiFi Registry, but the same concept applies for NiFi commands. + +==== Example - Secure NiFi Registry without Proxied-Entity +Assuming we have a keystore containing the certificate for "CN=user1, OU=NIFI", an example properties file would be the following: + +``` + baseUrl=https://localhost:18443 + keystore=/path/to/keystore.jks + keystoreType=JKS + keystorePasswd=changeme + keyPasswd=changeme + truststore=/path/to/truststore.jks + truststoreType=JKS + truststorePasswd=changeme +``` + +In this example, commands will be executed as "CN=user1, OU=NIFI". This user would need to be a user in NiFi Registry, and commands accessing buckets would be restricted to buckets this user has access to. + +==== Example - Secure NiFi Registry with Proxied-Entity +Assuming we have access to the keystore of NiFi Registry itself, and that NiFi Registry is also configured to allow Kerberos or LDAP authentication, an example properties file would be the following: + +``` + baseUrl=https://localhost:18443 + keystore=/path/to/keystore.jks + keystoreType=JKS + keystorePasswd=changeme + keyPasswd=changeme + truststore=/path/to/truststore.jks + truststoreType=JKS + truststorePasswd=changeme + proxiedEntity=user1@NIFI.COM +``` + +In this example, the certificate in _keystore.jks_ would be for the NiFi Registry server, for example "CN=localhost, OU=NIFI". This identity would need to be defined as a user in NiFi Registry and given permissions to 'Proxy'. + +"CN=localhost, OU=NIFI" would be proxying commands to be executed as user1@NIFI.COM. + +=== Interactive Usage +In interactive mode the tab key can be used to perform auto-completion. + +For example, typing tab at an empty prompt should display possible commands for the first argument: + + #> + demo exit help nifi registry session + +Typing "nifi " and then a tab will show the sub-commands for NiFi: + + #> nifi + cluster-summary get-nodes pg-enable-services pg-set-var + connect-node get-reg-client-id pg-get-all-versions pg-start + create-reg-client get-root-id pg-get-services pg-status + current-user list-reg-clients pg-get-vars pg-stop + delete-node offload-node pg-get-version update-reg-client + disconnect-node pg-change-version pg-import + get-node pg-disable-services pg-list + +Arguments that represent a path to a file, such as `-p` or when setting a properties file in the session, will auto-complete the path being typed: + + #> session set nifi.props /tmp/ + dir1/ dir2/ dir3/ + +=== Output +Most commands support the ability to specify an `--outputType` argument, or `-ot` for short. + +Currently the output type may be simple or json. + +The default output type in interactive mode is simple, and the default output type in standalone mode is json. + +Example of simple output for `list-buckets`: + + #> registry list-buckets -ot simple + My Bucket - 3c7b7467-0012-4d8f-a918-6aa42b6b9d39 + +Example of json output for `list-buckets`: + + #> registry list-buckets -ot json + [ { + "identifier" : "3c7b7467-0012-4d8f-a918-6aa42b6b9d39", + "name" : "My Bucket", + "createdTimestamp" : 1516718733854, + "permissions" : { + "canRead" : true, + "canWrite" : true, + "canDelete" : true + }, + "link" : { + "params" : { + "rel" : "self" + }, + "href" : "buckets/3c7b7467-0012-4d8f-a918-6aa42b6b9d39" + } + } ] + +=== Back-Referencing +When using the interactive CLI, a common scenario will be using an id from a previous result as the input to the next command. Back-referencing provides a shortcut for referencing a result from the previous command via a positional reference. + +NOTE: Not every command produces back-references. To determine if a command supports back-referencing, check the usage. + + #> registry list-buckets help + Lists the buckets that the current user has access to. + PRODUCES BACK-REFERENCES + +A common scenario for utilizing back-references would be the following: + +1. User starts by exploring the available buckets in a registry instance + + #> registry list-buckets + # Name Id Description + - ------------ ------------------------------------ ----------- + 1 My Bucket 3c7b7467-0012-4d8f-a918-6aa42b6b9d39 (empty) + 2 Other Bucket 175fb557-43a2-4abb-871f-81a354f47bc2 (empty) + +2. User then views the flows in one of the buckets using a back-reference to the bucket id from the previous result in position 1 + + #> registry list-flows -b &1 + Using a positional back-reference for 'My Bucket' + # Name Id Description + - ------- ------------------------------------ ---------------- + 1 My Flow 06acb207-d2f1-447f-85ed-9b8672fe6d30 This is my flow. + +3. User then views the version of the flow using a back-reference to the flow id from the previous result in position 1 + + #> registry list-flow-versions -f &1 + Using a positional back-reference for 'My Flow' + Ver Date Author Message + --- -------------------------- ------------------------ ------------------------------------- + 1 Tue, Jan 23 2018 09:48 EST anonymous This is the first version of my flow. + +4. User deploys version 1 of the flow using back-references to the bucket and flow id from step 2 + + #> nifi pg-import -b &1 -f &1 -fv 1 + Using a positional back-reference for 'My Bucket' + Using a positional back-reference for 'My Flow' + 9bd157d4-0161-1000-b946-c1f9b1832efd + +The reason step 4 was able to reference the results from step 2, is because the `list-flow-versions` command in step 3 does not produce back-references, so the results from step 2 are still available. + +=== Adding Commands +To add a NiFi command, create a new class that extends `AbstractNiFiCommand`: + +``` +public class MyCommand extends AbstractNiFiCommand { + + public MyCommand() { + super("my-command"); + } + + @Override + protected void doExecute(NiFiClient client, Properties properties) + throws NiFiClientException, IOException, MissingOptionException, CommandException { + // TODO implement + } + + @Override + public String getDescription() { + return "This is my new command"; + } +} +``` + +Add the new command to `NiFiCommandGroup`: + +``` +commands.add(new MyCommand()); +``` + +To add a NiFi Registry command, perform the same steps, but extend from `AbstractNiFiRegistryCommand`, and add the command to `NiFiRegistryCommandGroup`. + +[[encrypt_config_tool]] +== Encrypt-Config Tool +The `encrypt-config` command line tool (invoked as `./bin/encrypt-config.sh` or `bin\encrypt-config.bat`) reads from a _nifi.properties_ file with plaintext sensitive configuration values, prompts for a master password or raw hexadecimal key, and encrypts each value. It replaces the plain values with the protected value in the same file, or writes to a new _nifi.properties_ file if specified. + +The default encryption algorithm utilized is AES/GCM 128/256-bit. 128-bit is used if the JCE Unlimited Strength Cryptographic Jurisdiction Policy files are not installed, and 256-bit is used if they are installed. + +=== Usage +To show help: + + ./bin/encrypt-config.sh -h + +The following are available options: + + * `-h`,`--help` Prints this usage message + * `-v`,`--verbose` Sets verbose mode (default false) + * `-n`,`--niFiProperties ` The _nifi.properties_ file containing unprotected config values (will be overwritten) + * `-l`,`--loginIdentityProviders ` The _login-identity-providers.xml_ file containing unprotected config values (will be overwritten) + * `-a`,`--authorizers ` The _authorizers.xml_ file containing unprotected config values (will be overwritten) + * `-f`,`--flowXml ` The _flow.xml.gz_ file currently protected with old password (will be overwritten) + * `-b`,`--bootstrapConf ` The _bootstrap.conf_ file to persist master key + * `-o`,`--outputNiFiProperties ` The destination _nifi.properties_ file containing protected config values (will not modify input _nifi.properties_) + * `-i`,`--outputLoginIdentityProviders ` The destination _login-identity-providers.xml_ file containing protected config values (will not modify input _login-identity-providers.xml_) + * `-u`,`--outputAuthorizers ` The destination _authorizers.xml_ file containing protected config values (will not modify input _authorizers.xml_) + * `-g`,`--outputFlowXml ` The destination _flow.xml.gz_ file containing protected config values (will not modify input _flow.xml.gz_) + * `-k`,`--key ` The raw hexadecimal key to use to encrypt the sensitive properties + * `-e`,`--oldKey ` The old raw hexadecimal key to use during key migration + * `-p`,`--password ` The password from which to derive the key to use to encrypt the sensitive properties + * `-w`,`--oldPassword ` The old password from which to derive the key during migration + * `-r`,`--useRawKey` If provided, the secure console will prompt for the raw key value in hexadecimal form + * `-m`,`--migrate` If provided, the _nifi.properties_ and/or _login-identity-providers.xml_ sensitive properties will be re-encrypted with a new key + * `-x`,`--encryptFlowXmlOnly` If provided, the properties in _flow.xml.gz_ will be re-encrypted with a new key but the _nifi.properties_ and/or _login-identity-providers.xml_ files will not be modified + * `-s`,`--propsKey ` The password or key to use to encrypt the sensitive processor properties in _flow.xml.gz_ + * `-A`,`--newFlowAlgorithm ` The algorithm to use to encrypt the sensitive processor properties in _flow.xml.gz_ + * `-P`,`--newFlowProvider ` The security provider to use to encrypt the sensitive processor properties in _flow.xml.gz_ + +As an example of how the tool works, assume that you have installed the tool on a machine supporting 256-bit encryption and with the following existing values in the _nifi.properties_ file: + +``` +# security properties # +nifi.sensitive.props.key=thisIsABadSensitiveKeyPassword +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC +nifi.sensitive.props.additional.keys= + +nifi.security.keystore=/path/to/keystore.jks +nifi.security.keystoreType=JKS +nifi.security.keystorePasswd=thisIsABadKeystorePassword +nifi.security.keyPasswd=thisIsABadKeyPassword +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +``` + +Enter the following arguments when using the tool: + +---- +encrypt-config.sh +-b bootstrap.conf +-k 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 +-n nifi.properties +---- + +As a result, the _nifi.properties_ file is overwritten with protected properties and sibling encryption identifiers (`aes/gcm/256`, the currently supported algorithm): + +``` +# security properties # +nifi.sensitive.props.key=n2z+tTTbHuZ4V4V2||uWhdasyDXD4ZG2lMAes/vqh6u4vaz4xgL4aEbF4Y/dXevqk3ulRcOwf1vc4RDQ== +nifi.sensitive.props.key.protected=aes/gcm/256 +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC +nifi.sensitive.props.additional.keys= + +nifi.security.keystore=/path/to/keystore.jks +nifi.security.keystoreType=JKS +nifi.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo +nifi.security.keystorePasswd.protected=aes/gcm/256 +nifi.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg== +nifi.security.keyPasswd.protected=aes/gcm/256 +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +``` + +Additionally, the _bootstrap.conf_ file is updated with the encryption key as follows: + +``` +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 +``` + +Sensitive configuration values are encrypted by the tool by default, however you can encrypt any additional properties, if desired. To encrypt additional properties, specify them as comma-separated values in the `nifi.sensitive.props.additional.keys` property. + +If the _nifi.properties_ file already has valid protected values, those property values are not modified by the tool. + +When applied to _login-identity-providers.xml_ and _authorizers.xml_, the property elements are updated with an `encryption` attribute: + +Example of protected _login-identity-providers.xml_: + +``` + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + ... + +``` + +Example of protected _authorizers.xml_: + +``` + + + ldap-user-group-provider + org.apache.nifi.ldap.tenants.LdapUserGroupProvider + START_TLS + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + ... + +``` + +== File Manager +The File Manager utility (invoked as `./bin/file-manager.sh` or `bin\file-manager.bat`) allows system administrators to take a backup of an existing NiFi installation, install a new version of NiFi in a designated location (while migrating any previous configuration settings) or restore an installation from a previous backup. File Manager supports NiFi version 1.0.0 and higher. + +=== Usage +To show help: + + ./bin/file-manager.sh -h + +The following are available options: + +* `-b`,`--backupDir ` Backup NiFi Directory (used with backup or restore operation) +* `-c`,`--nifiCurrentDir ` Current NiFi Installation Directory (used optionally with install or restore operation) +* `-d`,`--nifiInstallDir ` NiFi Installation Directory (used with install or restore operation) +* `-h`,`--help` Print help info (optional) +* `-i`,`--installFile ` NiFi Install File (used with install operation) +* `-m`,`--moveRepositories` Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation) +* `-o`,`--operation ` File operation (install | backup | restore) +* `-r`,`--nifiRollbackDir ` NiFi Installation Directory (used with install or restore operation) +* `-t`,`--bootstrapConf ` Current NiFi Bootstrap Configuration File (used optionally) +* `-v`,`--verbose` Verbose messaging (optional) +* `-x`,`--overwriteConfigs` Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation) + +Example usage on Linux: + + # backup NiFi installation + # option -t may be provided to ensure backup of external boostrap.conf file + ./file-manager.sh + -o backup + –b /tmp/nifi_bak + –c /usr/nifi_old + -v + + # install NiFi using compressed tar file into /usr/nifi directory (should install as /usr/nifi/nifi-1.3.0). + # migrate existing configurations with location determined by external bootstrap.conf and move over repositories from nifi_old + # options -t and -c should both be provided if migration of configurations, state and repositories are required + ./file-manager.sh + -o install + –i nifi-1.3.0.tar.gz + –d /usr/nifi + –c /usr/nifi/nifi_old + -t /usr/nifi/old_conf/bootstrap.conf + -v + -m + + # restore NiFi installation from backup directory and move back repositories + # option -t may be provided to ensure bootstrap.conf is restored to the file path provided, otherwise it is placed in the + # default directory under the rollback path (e.g. /usr/nifi_old/conf) + ./file-manager.sh + -o restore + –b /tmp/nifi_bak + –r /usr/nifi_old + –c /usr/nifi + -m + -v + +=== Expected Behavior + +==== Backup +During the backup operation a backup directory is created in a designated location for an existing NiFi installation. Backups will capture all critical files (including any internal or external configurations, libraries, scripts and documents) however it excludes backing up repositories and logs due to potential size. If configuration/library files are external from the existing installation folder the backup operation will capture those as well. + +==== Install +During the install operation File Manager will perform installation using the designated NiFi binary file (either tar.gz or zip file) to create a new installation or migrate an existing nifi installation to a new one. Installation can optionally move repositories (if located within the configuration folder of the current installation) to the new installation as well as migrate configuration files to the newer installation. + +==== Restore +The restore operation allows an existing installation to revert back to a previous installation. Using an existing backup directory (created from the backup operation) the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. + +NOTE: If repositories were changed due to the installation of a newer version of NiFi these may no longer be compatible during restore. In that scenario exclude the `-m` option to ensure new repositories will be created or, if repositories live outside of the NiFi directory, remove them so they can be recreated on startup after restore. + +== Flow Analyzer +The `flow-analyzer` tool (invoked as `./bin/flow-analyzer.sh` or `bin\flow-analyzer.bat`) analyzes the _flow.xml.gz_ file and reports: + +* Total Bytes Utilized by the System +* Min/Max Back Pressure Size +* Average Back Pressure Size +* Min/Max Flowfile Queue Size +* Average Flowfile Queue Size + +=== Usage +To execute the `flow-analyzer` tool: + + flow-analyzer.sh + +Example: + + $ ./flow-analyzer.sh /Users/nifiuser/nifi-1.8.0/conf/flow.xml.gz + Using flow=/Users/nifiuser/nifi-1.8.0/conf/flow.xml.gz + Total Bytes Utilized by System=1518 GB + Max Back Pressure Size=1 GB + Min Back Pressure Size=1 GB + Average Back Pressure Size=2.504950495 GB + Max Flowfile Queue Size=10000 + Min Flowfile Queue Size=10000 + Avg Flowfile Queue Size=10000.000000000 + +== Node Manager +Node manager (invoked as `./bin/node-manager.sh` or `bin\node-manager.bat`) supports connecting, disconnecting and removing a node when in a cluster (an error message displays if the node is not part of a cluster) as well as obtaining the status of a node. When nodes are disconnected from a cluster and need to be connected or removed, a list of urls of connected nodes should be provided to send the required command to the active cluster. Node Manager supports NiFi version 1.0.0 and higher. + +=== Usage +To show help: + + ./bin/node-manager.sh -h + +The following are available options: + +* `-b`,`--bootstrapConf ` Existing Bootstrap Configuration file (required) +* `-d`,`--nifiInstallDir ` NiFi Root Folder (required) +* `-h`,`--help` Help Text (optional) +* `-o`, `--operation ` Operations supported: status, connect (cluster), disconnect (cluster), remove (cluster) +* `-p`,`--proxyDN ` Proxy or User DN (required for secured nodes doing connect, disconnect and remove operations) +* `-u`,`--clusterUrls ` Comma delimited list of active urls for cluster (optional). Not required for disconnecting a node yet will be needed when connecting or removing from a cluster +* `-v`,`--verbose` Verbose messaging (optional) + + +To connect, disconnect, or remove a node from a cluster: + + node-manager.sh -d {$NIFI_HOME} –b { nifi bootstrap file path} + -o {remove|disconnect|connect|status} [-u {url list}] [-p {proxy name}] [-v] + +Example usage on Linux: + + # disconnect without cluster url list + ./node-manager.sh + -d /usr/nifi/nifi_current + -b /usr/nifi/nifi_current/conf/bootstrap.conf + -o disconnect + –p ydavis@nifi + -v + + #with url list + ./node-manager.sh + -d /usr/nifi/nifi_current + -b /usr/nifi/nifi_current/conf/bootstrap.conf + -o connect + -u 'http://nifi-server-1:8080,http://nifi-server-2:8080' + -v + +Example usage on Windows: + + node-manager.bat + -d "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT" + -b "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT\\conf\\bootstrap.conf" + -o disconnect + –v + +=== Expected Behavior + +==== Status +To obtain information on UI availability of a node, the status operation can be used to determine if the node is running. If the `–u (clusterUrls)` option is not provided the current node url is checked otherwise the urls provided will be checked. + +==== Disconnect +When a node is disconnected from the cluster, the node itself should appear as disconnected and the cluster should have a bulletin indicating the disconnect request was received. The cluster should also show _n-1/n_ nodes available in the cluster. For example, if 1 node is disconnected from a 3-node cluster, then "2 of 3" nodes should show on the remaining nodes in the cluster. Changes to the flow should not be allowed on the cluster with a disconnected node. + +==== Connect +When the connect command is executed to reconnect a node to a cluster, upon completion the node itself should show that it has rejoined the cluster by showing _n/n_ nodes. Previously it would have shown Disconnected. Other nodes in the cluster should receive a bulletin of the connect request and also show _n/n_ nodes allowing for changes to be allowed to the flow. + +==== Remove +When the remove command is executed the node should show as disconnected from a cluster. The nodes remaining in the cluster should show _n-1/n-1_ nodes. For example, if 1 node is removed from a 3-node cluster, then the remaining 2 nodes should show "2 of 2" nodes. The cluster should allow a flow to be adjusted. The removed node can rejoin the cluster if restarted and the flow for the cluster has not changed. If the flow was changed, the flow template of the removed node should be deleted before restarting the node to allow it to obtain the cluster flow (otherwise an uninheritable flow file exception may occur). + +== Notify +Notify (invoked as `./bin/notify.sh` or `bin\notify.bat`) allows administrators to send messages as bulletins to NiFi. Notify is supported on NiFi version 1.2.0 and higher. + +=== Usage +To show help: + + ./bin/notify.sh -h + +The following are available options: + +* `-b`,`--bootstrapConf ` Existing Bootstrap Configuration file (required) +* `-d`,`--nifiInstallDir ` NiFi Root Folder (required) +* `-h`,`--help` Help Text (optional) +* `-l`,`--level ` Status level of bulletin – `INFO`, `WARN`, `ERROR` +* `-m`,`--message ` Bulletin message (required) +* `-p`,`--proxyDN ` Proxy or User DN (required for secured nodes) +* `-v`,`--verbose` Verbose messaging (optional) + +To send notifications: + + notify.sh -d {$NIFI_HOME} –b {nifi bootstrap file path} -m {message} [-l {level}] [-v] + +Example usage on Linux: + + ./notify.sh -d /usr/nifi/nifi_current -b /usr/nifi/nifi_current/conf/bootstrap.conf -m "Test Message Server 1" -l "WARN" –p “ydavis@nifi” -v + +Example usage on Windows: + + notify.bat -v -d "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT" -b "C:\\Program Files\\nifi\\nifi-1.2.0-SNAPSHOT\\conf\\bootstrap.conf" -m "Test Message Server 1" -v + +Executing the above command line should result in a bulletin appearing in NiFi: + +image::nifi-notifications.png["NiFi Notifications"] + +== S2S +S2S is a command line tool (invoked as `./bin/s2s.sh` or `bin\s2s.bat`) that can either read a list of DataPackets from stdin to send over site-to-site or write the received DataPackets to stdout. + +=== Usage +To show help: + + ./bin/s2s.sh -h + +The following are available options: + +* `--batchCount ` Number of flow files in a batch +* `--batchDuration ` Duration of a batch +* `--batchSize ` Size of flow files in a batch +* `-c`,`--compression` Use compression +* `-d`,`--direction` Direction (valid directions: `SEND`, `RECEIVE`) (default: `SEND`) +* `-h`,`--help` Help Text (optional) +* `-i`,`--portIdentifier ` Port id +* `--keystore ` Keystore +* `--keyStorePassword ` Keystore password +* `--keyStoreType ` Keystore type (default: `JKS`) +* `-n`,`--portName` Port name +* `-p`,`--transportProtocol` Site to site transport protocol (default: `RAW`) +* `--peerPersistenceFile ` File to write peer information to so it can be recovered on restart +* `--penalization ` Penalization period +* `--proxyHost ` Proxy hostname +* `--proxyPassword ` Proxy password +* `--proxyPort ` Proxy port +* `--proxyUsername ` Proxy username +* `--timeout ` Timeout +* `--trustStore ` Truststore +* `--trustStorePassword ` Truststore password +* `--trustStoreType ` Truststore type (default: `JKS`) +* `-u,--url ` NiFI URL to connect to (default: `http://localhost:8080/nifi`) + +The s2s cli input/output format is a JSON list of DataPackets. They can have the following formats: + + [{"attributes":{"key":"value"},"data":"aGVsbG8gbmlmaQ=="}] + +where data is the base64 encoded value of the FlowFile content (always used for received data) or: + + [{"attributes":{"key":"value"},"dataFile":"/Users/pvillard/Documents/GitHub/nifi/nifi-toolkit/nifi-toolkit-assembly/target/nifi-toolkit-1.9.0-SNAPSHOT-bin/nifi-toolkit-1.9.0-SNAPSHOT/bin/EXAMPLE"}] + +where dataFile is a file to read the FlowFile content from. + +Example usage to send a FlowFile with the contents of "hey nifi" to a local unsecured NiFi over http with an input port named "input": + + echo '[{"data":"aGV5IG5pZmk="}]' | bin/s2s.sh -n input -p http + +[[tls_toolkit]] +== TLS Toolkit +In order to facilitate the secure setup of NiFi, you can use the `tls-toolkit` command line utility to automatically generate the required keystores, truststore, and relevant configuration files. This is especially useful for securing multiple NiFi nodes, which can be a tedious and error-prone process. + +[[wildcard_certificates]] +=== Wildcard Certificates +Wildcard certificates (i.e. two nodes `node1.nifi.apache.org` and `node2.nifi.apache.org` being assigned the same certificate with a CN or SAN entry of `+*.nifi.apache.org+`) are *not officially supported* and *not recommended*. There are numerous disadvantages to using wildcard certificates, and a cluster working with wildcard certificates has occurred in previous versions out of lucky accidents, not intentional support. Wildcard SAN entries are acceptable *if* each cert maintains an additional unique SAN entry and CN entry. + +==== Potential issues with wildcard certificates +* In many places throughout the codebase, cluster communications use certificate identities many times to identify a node, and if the certificate simply presents a wildcard DN, that doesn’t resolve to a specific node +* Admins may need to provide a custom node identity in _authorizers.xml_ for `*.nifi.apache.org` because all proxy actions only resolve to the cert DN (see the <> section in the System Administrator's Guide for more information). +* Admins have no traceability into which node performed an action because they all resolve to the same DN +* Admins running multiple instances on the same machine using different ports to identify them can accidentally put `node1` hostname with `node2` port, and the address will resolve fine because it’s using the same certificate, but the host header handler will block it because the `node1` hostname is (correctly) not listed as an acceptable host for `node2` instance +* If the wildcard certificate is compromised, all nodes are compromised + +NOTE: JKS keystores and truststores are recommended for NiFi. This tool allows the specification of other keystore types on the command line but will ignore a type of PKCS12 for use as the truststore because that format has some compatibility issues between BouncyCastle and Oracle implementations. + +[[tls_operation_modes]] +=== Operation Modes +The `tls-toolkit` command line tool has two primary modes of operation: + +1. Standalone -- generates the certificate authority, keystores, truststores, and _nifi.properties_ files in one command. +2. Client/Server -- uses a Certificate Authority Server that accepts Certificate Signing Requests from clients, signs them, and sends the resulting certificates back. Both client and server validate the other’s identity through a shared secret. + +==== Standalone +Standalone mode is invoked by running `./bin/tls-toolkit.sh standalone` or `bin\tls-toolkit.sh standalone`. + +===== Usage +To show help: + + ./bin/tls-toolkit.sh standlone -h + +The following are available options: + +* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) +* `--additionalCACertificate ` Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary +* `-B`,`--clientCertPassword ` Password for client certificate. Must either be one value or one for each client DN (auto-generate if not specified) +* `-c`,`--certificateAuthorityHostname ` Hostname of NiFi Certificate Authority (default: `localhost`) +* `-C`,`--clientCertDn ` Generate client certificate suitable for use in browser with specified DN (Can be specified multiple times) +* `-d`,`--days ` Number of days issued certificate should be valid for (default: `1095`) +* `-f`,`--nifiPropertiesFile ` Base _nifi.properties_ file to update (Embedded file identical to the one in a default NiFi install will be used if not specified) +* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore +* `-G`,`--globalPortSequence ` Use sequential ports that are calculated for all hosts according to the provided hostname expressions (Can be specified multiple times, MUST BE SAME FROM RUN TO RUN) +* `-h`,`--help` Print help and exit +* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) +* `-K`,`--keyPassword ` Key password to use. Must either be one value or one for each host (auto-generate if not specified) +* `-n`,`--hostnames ` Comma separated list of hostnames +* `--nifiDnPrefix ` String to prepend to hostname(s) when determining DN (default: `CN=`) +* `--nifiDnSuffix ` String to append to hostname(s) when determining DN (default: `, OU=NIFI`) +* `-o`,`--outputDirectory ` The directory to output keystores, truststore, config files (default: `../bin`) +* `-O`,`--isOverwrite` Overwrite existing host output +* `-P`,`--trustStorePassword ` Keystore password to use. Must either be one value or one for each host (auto-generate if not specified) +* `-s`,`--signingAlgorithm ` Algorithm to use for signing certificates (default: `SHA256WITHRSA`) +* `-S`,`--keyStorePassword ` Keystore password to use. Must either be one value or one for each host (auto-generate if not specified) +* `--subjectAlternativeNames ` Comma-separated list of domains to use as Subject Alternative Names in the certificate +* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) + + +Hostname Patterns: + +* Square brackets can be used in order to easily specify a range of hostnames. Example: `[01-20]` +* Parentheses can be used in order to specify that more than one NiFi instance will run on the given host(s). Example: `(5)` + +Examples: + +Create 4 sets of keystore, truststore, _nifi.properties_ for localhost along with a client certificate with the given DN: +---- +bin/tls-toolkit.sh standalone -n 'localhost(4)' -C 'CN=username,OU=NIFI' +---- + +Create keystore, truststore, _nifi.properties_ for 10 NiFi hostnames in each of 4 subdomains: +---- +bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain' +---- + +Create 2 sets of keystore, truststore, _nifi.properties_ for 10 NiFi hostnames in each of 4 subdomains along with a client certificate with the given DN: +---- +bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain(2)' -C 'CN=username,OU=NIFI' +---- + +==== Client/Server +Client/Server mode relies on a long-running Certificate Authority (CA) to issue certificates. The CA can be stopped when you’re not bringing nodes online. + +===== Server +CA server mode is invoked by running `./bin/tls-toolkit.sh server` or `bin\tls-toolkit.sh server`. + +====== Usage +To show help: + + ./bin/tls-toolkit.sh server -h + +The following are available options: + +* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) +* `--configJsonIn ` The place to read configuration info from (defaults to the value of configJson), implies useConfigJson if set (default: `configJson` value) +* `-d`,`--days ` Number of days issued certificate should be valid for (default: `1095`) +* `-D`,`--dn ` The dn to use for the CA certificate (default: `CN=YOUR_CA_HOSTNAME,OU=NIFI`) +* `-f`,`--configJson ` The place to write configuration info (default: `config.json`) +* `-F`,`--useConfigJson` Flag specifying that all configuration is read from `configJson` to facilitate automated use (otherwise `configJson` will only be written to) +* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore +* `-h`,`--help` Print help and exit +* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) +* `-p`,`--PORT ` The port for the Certificate Authority to listen on (default: `8443`) +* `-s`,`--signingAlgorithm ` Algorithm to use for signing certificates (default: `SHA256WITHRSA`) +* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) +* `-t`,`--token ` The token to use to prevent MITM (required and must be same as one used by clients) + +===== Client +The client can be used to request new Certificates from the CA. The client utility generates a keypair and Certificate Signing Request (CSR) and sends the CSR to the Certificate Authority. CA client mode is invoked by running `./bin/tls-toolkit.sh client` or `bin\tls-toolkit.sh client`. + +====== Usage +To show help: + + ./bin/tls-toolkit.sh client -h + +The following are available options: + +* `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) +* `-c`,`--certificateAuthorityHostname ` Hostname of NiFi Certificate Authority (default: `localhost`) +* `-C`,`--certificateDirectory ` The directory to write the CA certificate (default: `.`) +* `--configJsonIn ` The place to read configuration info from, implies `useConfigJson` if set (default: `configJson` value) +* `-D`,`--dn ` The DN to use for the client certificate (default: `CN=,OU=NIFI`) (this is auto-populated by the tool) +* `-f`,`--configJson ` The place to write configuration info (default: `config.json`) +* `-F`,`--useConfigJson` Flag specifying that all configuration is read from `configJson` to facilitate automated use (otherwise `configJson` will only be written to) +* `-g`,`--differentKeyAndKeystorePasswords` Use different generated password for the key and the keystore +* `-h`,`--help` Print help and exit +* `-k`,`--keySize ` Number of bits for generated keys (default: `2048`) +* `-p`,`--PORT ` The port to use to communicate with the Certificate Authority (default: `8443`) +* `--subjectAlternativeNames ` Comma-separated list of domains to use as Subject Alternative Names in the certificate +* `-T`,`--keyStoreType ` The type of keystores to generate (default: `jks`) +* `-t`,`--token ` The token to use to prevent MITM (required and must be same as one used by CA) + +After running the client you will have the CA’s certificate, a keystore, a truststore, and a `config.json` with information about them as well as their passwords. + +For a client certificate that can be easily imported into the browser, specify: `-T PKCS12`. + +[[tls_intermediate_ca]] +=== Using An Existing Intermediate Certificate Authority (CA) +In some enterprise scenarios, a security/IT team may provide a signing certificate that has already been signed by the organization's certificate authority (CA). This *intermediate CA* can be used to sign the *node* (sometimes referred to as *leaf*) certificates that will be installed on each NiFi node, or the *client certificates* used to identify users. In order to inject the existing signing certificate into the toolkit process, follow these steps: + +. Generate or obtain the signed intermediate CA keys in the following format (see additional commands below): + * Public certificate in PEM format: `nifi-cert.pem` + * Private key in PEM format: `nifi-key.key` +. Place the files in the *toolkit working directory*. This is the directory where the tool is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. + * For example, given the following scenario, the toolkit command can be run from its location as long as the output directory `-o` is `../hardcoded/`, and the existing `nifi-cert.pem` and `nifi-key.key` will be used. + ** e.g. `$ ./toolkit/bin/tls-toolkit.sh standalone -o ./hardcoded/ -n 'node4.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` will result in a new directory at `./hardcoded/node4.nifi.apache.org` with a keystore and truststore containing a certificate signed by `./hardcoded/nifi-key.key` + * If the `-o` argument is not provided, the default working directory (`.`) must contain `nifi-cert.pem` and `nifi-key.key` + ** e.g. `$ cd ./hardcoded/ && ../toolkit/bin/tls-toolkit.sh standalone -n 'node5.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` + +``` +# Example directory structure *before* commands above are run + +🔓 0s @ 18:07:58 $ tree -L 2 +. +├── hardcoded +│   ├── CN=myusername.hardcoded_OU=NiFi.p12 +│   ├── CN=myusername.hardcoded_OU=NiFi.password +│   ├── nifi-cert.pem +│   ├── nifi-key.key +│   ├── node1.nifi.apache.org +│   ├── node2.nifi.apache.org +│   └── node3.nifi.apache.org +└── toolkit +    ├── LICENSE +    ├── NOTICE +    ├── README +    ├── bin +    ├── conf +    ├── docs +    └── lib +``` + +The `nifi-cert.pem` and `nifi-key.key` files should be ASCII-armored (Base64-encoded ASCII) files containing the CA public certificate and private key respectively. Here are sample files of each to show the expected format: + +==== nifi-cert.pem + +``` +# The first command shows the actual content of the encoded file, and the second parses it and shows the internal values + +.../certs $ more nifi-cert.pem +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIKAWTeM3kDAAAAADANBgkqhkiG9w0BAQsFADAxMQ0wCwYD +VQQLDAROSUZJMSAwHgYDVQQDDBduaWZpLWNhLm5pZmkuYXBhY2hlLm9yZzAeFw0x +ODA3MjgwMDA0MzJaFw0yMTA3MjcwMDA0MzJaMDExDTALBgNVBAsMBE5JRkkxIDAe +BgNVBAMMF25pZmktY2EubmlmaS5hcGFjaGUub3JnMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAqkVrrC+AkFbjnCpupSy84tTFDsRVUIWYj/k2pVwC145M +3bpr0pRCzLuzovAjFCmT5L+isTvNjhionsqif07Ebd/M2psYE/Rih2MULsX6KgRe +1nRUiBeKF08hlmSBMGDFPj39yDzE/V9edxV/KGjRqVgw/Qy0vwaS5uWdXnLDhzoV +4/Mz7lGmYoMasZ1uexlH93jjBl1+EFL2Xoa06oLbEojJ9TKaWhpG8ietEedf7WM0 +zqBEz2kHo9ddFk9yxiCkT4SUKnDWkhwc/o6us1vEXoSw+tmufHY/A3gVihjWPIGz +qyLFl9JuN7CyJepkVVqTdskBG7S85G/kBlizUj5jOwIDAQABo38wfTAOBgNVHQ8B +Af8EBAMCAf4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUKiWBKbMMQ1zUabD4gI7L +VOWOcy0wHwYDVR0jBBgwFoAUKiWBKbMMQ1zUabD4gI7LVOWOcy0wHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAxfHFIZLOw +mwIqnSI/ir8f/uzDMq06APHGdhdeIKV0HR74BtK95KFg42zeXxAEFeic98PC/FPV +tKpm2WUa1slMB+oP27cRx5Znr2+pktaqnM7f2JgMeJ8bduNH3RUkr9jwgkcJRwyC +I4fwHC9k18aizNdOf2q2UgQXxNXaLYPe17deuNVwwrflMgeFfVrwbT2uPJTMRi1D +FQyc6haF4vsOSSRzE6OyDoc+/1PpyPW75OeSXeVCbc3AEAvRuTZMBQvBQUqVM51e +MDG+K3rCeieSBPOnGNrEC/PiA/CvaMXBEog+xPAw1SgYfuCz4rlM3BdRa54z3+oO +lc8xbzd7w8Q3 +-----END CERTIFICATE----- +.../certs $ openssl x509 -in nifi-cert.pem -text -noout +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 01:64:de:33:79:03:00:00:00:00 + Signature Algorithm: sha256WithRSAEncryption + Issuer: OU=NIFI, CN=nifi-ca.nifi.apache.org + Validity + Not Before: Jul 28 00:04:32 2018 GMT + Not After : Jul 27 00:04:32 2021 GMT + Subject: OU=NIFI, CN=nifi-ca.nifi.apache.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:aa:45:6b:ac:2f:80:90:56:e3:9c:2a:6e:a5:2c: + bc:e2:d4:c5:0e:c4:55:50:85:98:8f:f9:36:a5:5c: + 02:d7:8e:4c:dd:ba:6b:d2:94:42:cc:bb:b3:a2:f0: + 23:14:29:93:e4:bf:a2:b1:3b:cd:8e:18:a8:9e:ca: + a2:7f:4e:c4:6d:df:cc:da:9b:18:13:f4:62:87:63: + 14:2e:c5:fa:2a:04:5e:d6:74:54:88:17:8a:17:4f: + 21:96:64:81:30:60:c5:3e:3d:fd:c8:3c:c4:fd:5f: + 5e:77:15:7f:28:68:d1:a9:58:30:fd:0c:b4:bf:06: + 92:e6:e5:9d:5e:72:c3:87:3a:15:e3:f3:33:ee:51: + a6:62:83:1a:b1:9d:6e:7b:19:47:f7:78:e3:06:5d: + 7e:10:52:f6:5e:86:b4:ea:82:db:12:88:c9:f5:32: + 9a:5a:1a:46:f2:27:ad:11:e7:5f:ed:63:34:ce:a0: + 44:cf:69:07:a3:d7:5d:16:4f:72:c6:20:a4:4f:84: + 94:2a:70:d6:92:1c:1c:fe:8e:ae:b3:5b:c4:5e:84: + b0:fa:d9:ae:7c:76:3f:03:78:15:8a:18:d6:3c:81: + b3:ab:22:c5:97:d2:6e:37:b0:b2:25:ea:64:55:5a: + 93:76:c9:01:1b:b4:bc:e4:6f:e4:06:58:b3:52:3e: + 63:3b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment, Key Agreement, Certificate Sign, CRL Sign + X509v3 Basic Constraints: + CA:TRUE + X509v3 Subject Key Identifier: + 2A:25:81:29:B3:0C:43:5C:D4:69:B0:F8:80:8E:CB:54:E5:8E:73:2D + X509v3 Authority Key Identifier: + keyid:2A:25:81:29:B3:0C:43:5C:D4:69:B0:F8:80:8E:CB:54:E5:8E:73:2D + + X509v3 Extended Key Usage: + TLS Web Client Authentication, TLS Web Server Authentication + Signature Algorithm: sha256WithRSAEncryption + 31:7c:71:48:64:b3:b0:9b:02:2a:9d:22:3f:8a:bf:1f:fe:ec: + c3:32:ad:3a:00:f1:c6:76:17:5e:20:a5:74:1d:1e:f8:06:d2: + bd:e4:a1:60:e3:6c:de:5f:10:04:15:e8:9c:f7:c3:c2:fc:53: + d5:b4:aa:66:d9:65:1a:d6:c9:4c:07:ea:0f:db:b7:11:c7:96: + 67:af:6f:a9:92:d6:aa:9c:ce:df:d8:98:0c:78:9f:1b:76:e3: + 47:dd:15:24:af:d8:f0:82:47:09:47:0c:82:23:87:f0:1c:2f: + 64:d7:c6:a2:cc:d7:4e:7f:6a:b6:52:04:17:c4:d5:da:2d:83: + de:d7:b7:5e:b8:d5:70:c2:b7:e5:32:07:85:7d:5a:f0:6d:3d: + ae:3c:94:cc:46:2d:43:15:0c:9c:ea:16:85:e2:fb:0e:49:24: + 73:13:a3:b2:0e:87:3e:ff:53:e9:c8:f5:bb:e4:e7:92:5d:e5: + 42:6d:cd:c0:10:0b:d1:b9:36:4c:05:0b:c1:41:4a:95:33:9d: + 5e:30:31:be:2b:7a:c2:7a:27:92:04:f3:a7:18:da:c4:0b:f3: + e2:03:f0:af:68:c5:c1:12:88:3e:c4:f0:30:d5:28:18:7e:e0: + b3:e2:b9:4c:dc:17:51:6b:9e:33:df:ea:0e:95:cf:31:6f:37: + 7b:c3:c4:37 +``` + +==== nifi-key.key + +``` +# The first command shows the actual content of the encoded file, and the second parses it and shows the internal values + +.../certs $ more nifi-key.key +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqkVrrC+AkFbjnCpupSy84tTFDsRVUIWYj/k2pVwC145M3bpr +0pRCzLuzovAjFCmT5L+isTvNjhionsqif07Ebd/M2psYE/Rih2MULsX6KgRe1nRU +iBeKF08hlmSBMGDFPj39yDzE/V9edxV/KGjRqVgw/Qy0vwaS5uWdXnLDhzoV4/Mz +7lGmYoMasZ1uexlH93jjBl1+EFL2Xoa06oLbEojJ9TKaWhpG8ietEedf7WM0zqBE +z2kHo9ddFk9yxiCkT4SUKnDWkhwc/o6us1vEXoSw+tmufHY/A3gVihjWPIGzqyLF +l9JuN7CyJepkVVqTdskBG7S85G/kBlizUj5jOwIDAQABAoIBAAdWRnV89oVBuT0Z +dvsXGmyLzpH8U9DMcO6DRp+Jf3XaY+WKCutgCCDaVbtHrbtIr17EAzav5QOifGGb +SbVCp6Q0aJdi5360oSpEUrJRRZ5Z4dxL1vimSwUGG+RnIEn9YYJ1GWJve+2PFnr7 +KieLnL03V6UPzxoMJnhcnJNdTp+dBwzSazVQwye2csSJlVMk49t2lxBwce7ohuh+ +9fL7G3HU5S9d08QT1brknMHahcw1SYyJd0KSjRJCB6wAxnAZmJYJ1jQCI8YICq0j +RX2rhxEXuEMXQcaiFQXzCrmQEXreKUISDvNeu/h7YU9UvJWPZSFGnEGgnMP2XvQm +EjK3rQECgYEA5+OkpLsiLNMHGzj72PiBkq82sTLQJ2+8udYp6PheOGkhjjXoBse5 +YynyHlQt6CnVpJQ33mQUkJ+3ils0SMFtmI3rz3udzleek1so2L2J3+CI4kt7fFCb +FFbVXv+dLNrm+tOw68J48asyad8kEnHYq9Us+/3MLDmFJYTthkgzCpECgYEAu/ml +lQaWaZAQcQ8UuVeasxMYoN8zMmzfrkxc8AfNwKxF9nc44ywo4nJr+u/UVRGYpRgM +rdll5vz0Iq68qk03spaW7vDJn8hJQhkReQw1it9Fp/51r9MHzGTVarORJGa2oZ0g +iNe8LNizD3bQ19hEvju9mn0x9Q62Q7dapVpffwsCgYEAtC1TPpQQ59dIjERom5vr +wffWfTTIO/w8HgFkKxrgyuAVLJSCJtKFH6H1+M7bpKrsz6ZDCs+kkwMm76ASLf3t +lD2h3mNkqHG4SzLnuBD90jB666pO1rci6FjYDap7i+DC3F4j9+vxYYXt9Aln09UV +z94hx+LaA/rlk9OHY3EyB6ECgYBA/cCtNNjeaKv2mxM8PbjD/289d85YueHgfpCH +gPs3iZiq7W+iw8ri+FKzMSaFvw66zgTcOtULtxulviqG6ym9umk29dOQRgxmKQqs +gnckq6uGuOjxwJHqrlZHjQw6vLSaThxIk+aAzu+iAh+U8TZbW4ZjmrOiGdMUuJlD +oGpyHwKBgQCRjfqQjRelYVtU7j6BD9BDbCfmipwaRNP0CuAGOVtS+UnJuaIhsXFQ +QGEBuOnfFijIvb7YcXRL4plRYPMvDqYRNObuI6A+1xNtr000nxa/HUfzKVeI9Tsn +9AKMWnXS8ZcfStsVf3oDFffXYRqCaWeuhpMmg9TwdXoAuwfpE5GCmw== +-----END RSA PRIVATE KEY----- +.../certs $ openssl rsa -in nifi-key.key -text -noout +Private-Key: (2048 bit) +modulus: + 00:aa:45:6b:ac:2f:80:90:56:e3:9c:2a:6e:a5:2c: + bc:e2:d4:c5:0e:c4:55:50:85:98:8f:f9:36:a5:5c: + 02:d7:8e:4c:dd:ba:6b:d2:94:42:cc:bb:b3:a2:f0: + 23:14:29:93:e4:bf:a2:b1:3b:cd:8e:18:a8:9e:ca: + a2:7f:4e:c4:6d:df:cc:da:9b:18:13:f4:62:87:63: + 14:2e:c5:fa:2a:04:5e:d6:74:54:88:17:8a:17:4f: + 21:96:64:81:30:60:c5:3e:3d:fd:c8:3c:c4:fd:5f: + 5e:77:15:7f:28:68:d1:a9:58:30:fd:0c:b4:bf:06: + 92:e6:e5:9d:5e:72:c3:87:3a:15:e3:f3:33:ee:51: + a6:62:83:1a:b1:9d:6e:7b:19:47:f7:78:e3:06:5d: + 7e:10:52:f6:5e:86:b4:ea:82:db:12:88:c9:f5:32: + 9a:5a:1a:46:f2:27:ad:11:e7:5f:ed:63:34:ce:a0: + 44:cf:69:07:a3:d7:5d:16:4f:72:c6:20:a4:4f:84: + 94:2a:70:d6:92:1c:1c:fe:8e:ae:b3:5b:c4:5e:84: + b0:fa:d9:ae:7c:76:3f:03:78:15:8a:18:d6:3c:81: + b3:ab:22:c5:97:d2:6e:37:b0:b2:25:ea:64:55:5a: + 93:76:c9:01:1b:b4:bc:e4:6f:e4:06:58:b3:52:3e: + 63:3b +publicExponent: 65537 (0x10001) +privateExponent: + 07:56:46:75:7c:f6:85:41:b9:3d:19:76:fb:17:1a: + 6c:8b:ce:91:fc:53:d0:cc:70:ee:83:46:9f:89:7f: + 75:da:63:e5:8a:0a:eb:60:08:20:da:55:bb:47:ad: + bb:48:af:5e:c4:03:36:af:e5:03:a2:7c:61:9b:49: + b5:42:a7:a4:34:68:97:62:e7:7e:b4:a1:2a:44:52: + b2:51:45:9e:59:e1:dc:4b:d6:f8:a6:4b:05:06:1b: + e4:67:20:49:fd:61:82:75:19:62:6f:7b:ed:8f:16: + 7a:fb:2a:27:8b:9c:bd:37:57:a5:0f:cf:1a:0c:26: + 78:5c:9c:93:5d:4e:9f:9d:07:0c:d2:6b:35:50:c3: + 27:b6:72:c4:89:95:53:24:e3:db:76:97:10:70:71: + ee:e8:86:e8:7e:f5:f2:fb:1b:71:d4:e5:2f:5d:d3: + c4:13:d5:ba:e4:9c:c1:da:85:cc:35:49:8c:89:77: + 42:92:8d:12:42:07:ac:00:c6:70:19:98:96:09:d6: + 34:02:23:c6:08:0a:ad:23:45:7d:ab:87:11:17:b8: + 43:17:41:c6:a2:15:05:f3:0a:b9:90:11:7a:de:29: + 42:12:0e:f3:5e:bb:f8:7b:61:4f:54:bc:95:8f:65: + 21:46:9c:41:a0:9c:c3:f6:5e:f4:26:12:32:b7:ad: + 01 +prime1: + 00:e7:e3:a4:a4:bb:22:2c:d3:07:1b:38:fb:d8:f8: + 81:92:af:36:b1:32:d0:27:6f:bc:b9:d6:29:e8:f8: + 5e:38:69:21:8e:35:e8:06:c7:b9:63:29:f2:1e:54: + 2d:e8:29:d5:a4:94:37:de:64:14:90:9f:b7:8a:5b: + 34:48:c1:6d:98:8d:eb:cf:7b:9d:ce:57:9e:93:5b: + 28:d8:bd:89:df:e0:88:e2:4b:7b:7c:50:9b:14:56: + d5:5e:ff:9d:2c:da:e6:fa:d3:b0:eb:c2:78:f1:ab: + 32:69:df:24:12:71:d8:ab:d5:2c:fb:fd:cc:2c:39: + 85:25:84:ed:86:48:33:0a:91 +prime2: + 00:bb:f9:a5:95:06:96:69:90:10:71:0f:14:b9:57: + 9a:b3:13:18:a0:df:33:32:6c:df:ae:4c:5c:f0:07: + cd:c0:ac:45:f6:77:38:e3:2c:28:e2:72:6b:fa:ef: + d4:55:11:98:a5:18:0c:ad:d9:65:e6:fc:f4:22:ae: + bc:aa:4d:37:b2:96:96:ee:f0:c9:9f:c8:49:42:19: + 11:79:0c:35:8a:df:45:a7:fe:75:af:d3:07:cc:64: + d5:6a:b3:91:24:66:b6:a1:9d:20:88:d7:bc:2c:d8: + b3:0f:76:d0:d7:d8:44:be:3b:bd:9a:7d:31:f5:0e: + b6:43:b7:5a:a5:5a:5f:7f:0b +exponent1: + 00:b4:2d:53:3e:94:10:e7:d7:48:8c:44:68:9b:9b: + eb:c1:f7:d6:7d:34:c8:3b:fc:3c:1e:01:64:2b:1a: + e0:ca:e0:15:2c:94:82:26:d2:85:1f:a1:f5:f8:ce: + db:a4:aa:ec:cf:a6:43:0a:cf:a4:93:03:26:ef:a0: + 12:2d:fd:ed:94:3d:a1:de:63:64:a8:71:b8:4b:32: + e7:b8:10:fd:d2:30:7a:eb:aa:4e:d6:b7:22:e8:58: + d8:0d:aa:7b:8b:e0:c2:dc:5e:23:f7:eb:f1:61:85: + ed:f4:09:67:d3:d5:15:cf:de:21:c7:e2:da:03:fa: + e5:93:d3:87:63:71:32:07:a1 +exponent2: + 40:fd:c0:ad:34:d8:de:68:ab:f6:9b:13:3c:3d:b8: + c3:ff:6f:3d:77:ce:58:b9:e1:e0:7e:90:87:80:fb: + 37:89:98:aa:ed:6f:a2:c3:ca:e2:f8:52:b3:31:26: + 85:bf:0e:ba:ce:04:dc:3a:d5:0b:b7:1b:a5:be:2a: + 86:eb:29:bd:ba:69:36:f5:d3:90:46:0c:66:29:0a: + ac:82:77:24:ab:ab:86:b8:e8:f1:c0:91:ea:ae:56: + 47:8d:0c:3a:bc:b4:9a:4e:1c:48:93:e6:80:ce:ef: + a2:02:1f:94:f1:36:5b:5b:86:63:9a:b3:a2:19:d3: + 14:b8:99:43:a0:6a:72:1f +coefficient: + 00:91:8d:fa:90:8d:17:a5:61:5b:54:ee:3e:81:0f: + d0:43:6c:27:e6:8a:9c:1a:44:d3:f4:0a:e0:06:39: + 5b:52:f9:49:c9:b9:a2:21:b1:71:50:40:61:01:b8: + e9:df:16:28:c8:bd:be:d8:71:74:4b:e2:99:51:60: + f3:2f:0e:a6:11:34:e6:ee:23:a0:3e:d7:13:6d:af: + 4d:34:9f:16:bf:1d:47:f3:29:57:88:f5:3b:27:f4: + 02:8c:5a:75:d2:f1:97:1f:4a:db:15:7f:7a:03:15: + f7:d7:61:1a:82:69:67:ae:86:93:26:83:d4:f0:75: + 7a:00:bb:07:e9:13:91:82:9b +``` + +[[tls_external-signed_ca]] +==== Signing with Externally-signed CA Certificates +To sign generated certificates with a certificate authority (CA) generated outside of the TLS Toolkit, ensure the necessary files are in the right format and location (see <>). For example, an organization *Large Organization* has an internal CA (`CN=ca.large.org, OU=Certificate Authority`). This *root CA* is offline and only used to sign other internal CAs. The Large IT team generates an *intermediate CA* (`CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority`) to be used to sign all NiFi node certificates (`CN=node1.nifi.large.org, OU=NiFi`, `CN=node2.nifi.large.org, OU=NiFi`, etc.). + +To use the toolkit to generate these certificates and sign them using the *intermediate CA*, ensure that the following files are present (see <>): + +* `nifi-cert.pem` -- the public certificate of the *intermediate CA* in PEM format +* `nifi-key.key` -- the Base64-encoded private key of the *intermediate CA* in PKCS #1 PEM format + +If the *intermediate CA* was the *root CA*, it would be *self-signed* -- the signature over the certificate would be issued from the same key. In that case (the same as a toolkit-generated CA), no additional arguments are necessary. However, because the *intermediate CA* is signed by the *root CA*, the public certificate of the *root CA* needs to be provided as well to validate the signature. The `--additionalCACertificate` parameter is used to specify the path to the signing public certificate. The value should be the absolute path to the *root CA* public certificate. + +Example: + +---- +# Generate cert signed by intermediate CA (which is signed by root CA) -- WILL FAIL + +$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ +-P passwordpassword \ +-S passwordpassword \ +-o /opt/certs/externalCA \ +-O + +2018/08/02 18:48:11 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=Certificate Authority +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority +2018/08/02 18:48:12 WARN [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority not signed by CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority [certificate does not verify with supplied key] +Error generating TLS configuration. (The signing certificate was not signed by any known certificates) + +# Provide additional CA certificate path for signature verification of intermediate CA + +$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ +-P passwordpassword \ +-S passwordpassword \ +-o /opt/certs/externalCA \ +--additionalCACertificate /opt/certs/externalCA/root.pem \ +-O + +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=ca.large.org, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate was signed by CN=ca.large.org, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Using existing CA certificate /opt/certs/externalCA/nifi-cert.pem and key /opt/certs/externalCA/nifi-key.key +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Writing new ssl configuration to /opt/certs/externalCA/node1.nifi.apache.org +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Successfully generated TLS configuration for node1.nifi.apache.org 1 in /opt/certs/externalCA/node1.nifi.apache.org +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: No clientCertDn specified, not generating any client certificates. +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: tls-toolkit standalone completed successfully +---- + +[[additional_certificate_commands]] +=== Additional Certificate Commands + +. To convert from DER encoded public certificate (`cert.der`) to PEM encoded (`cert.pem`): + * If the DER file contains both the public certificate and private key, remove the private key with this command: + ** `perl -pe 'BEGIN{undef $/;} s|-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----|Removed private key|gs' cert.der > cert.pem` + * If the DER file only contains the public certificate, use this command: + ** `openssl x509 -inform der -in cert.der -out cert.pem` +. To convert from a PKCS12 keystore (`keystore.p12`) containing both the public certificate and private key into PEM encoded files (`$PASSWORD` is the keystore password): + * `openssl pkcs12 -in keystore.p12 -out cert.der -nodes -password "pass:$PASSWORD"` + * `openssl pkcs12 -in keystore.p12 -nodes -nocerts -out key.key -password "pass:$PASSWORD"` + * Follow the steps above to convert `cert.der` to `cert.pem` +. To convert from a Java Keystore (`keystore.jks`) containing private key into PEM encoded files (`$P12_PASSWORD` is the PKCS12 keystore password, `$JKS_PASSWORD` is the Java keystore password you want to set, and `$ALIAS` can be any value -- the NiFi default is `nifi-key`): + * `keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -destkeypass "$P12_PASSWORD" -deststorepass "$P12_PASSWORD" -srcstorepass "$JKS_PASSWORD" -srcalias "$ALIAS" -destalias "$ALIAS"` + * Follow the steps above to convert from `keystore.p12` to `cert.pem` and `key.key` +. To convert from PKCS #8 PEM format to PKCS #1 PEM format: + * If the private key is provided in PKCS #8 format (the file begins with `-----BEGIN PRIVATE KEY-----` rather than `-----BEGIN RSA PRIVATE KEY-----`), the following command will convert it to PKCS #1 format, move the original to `nifi-key-pkcs8.key`, and rename the PKCS #1 version as `nifi-key.key`: + ** `openssl rsa -in nifi-key.key -out nifi-key-pkcs1.key && mv nifi-key.key nifi-key-pkcs8.key && mv nifi-key-pkcs1.key nifi-key.key` +. To combine a private key in PEM format (`private.key`) and public certificate in PEM format (`certificate.pem`) into PKCS12 keystore: + * The following command will create the PKCS12 keystore (`keystore.p12`) from the two independent files. A Java keystore (JKS) cannot be formed directly from the PEM files: + ** `openssl pkcs12 -export -out keystore.p12 -inkey private.key -in certificate.pem` +. To convert a PKCS12 keystore (`keystore.p12`) to JKS keystore (`keystore.jks`): + * The following command will create the JKS keystore (`keystore.jks`). The `-destalias` flag is optional, as NiFi does not currently read from a specific alias in the keystore. The user will be prompted for a keystore password, which must be set and have minimum 8 characters, and a key password, which can be the same as the keystore password or different: + ** `keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -destkeystore keystore.jks + -deststoretype jks -destalias nifi-key` + +[[zookeeper_migrator]] +== ZooKeeper Migrator +You can use the `zk-migrator` tool to perform the following tasks: + +* Moving ZooKeeper information from one ZooKeeper cluster to another +* Migrating ZooKeeper node ownership + +For example, you may want to use the ZooKeeper Migrator when you are: + +* Upgrading from NiFi 0.x to NiFi 1.x in which embedded ZooKeepers are used +* Migrating from an embedded ZooKeeper in NiFi 0.x or 1.x to an external ZooKeeper +* Upgrading from NiFi 0.x with an external ZooKeeper to NiFi 1.x with the same external ZooKeeper +* Migrating from an external ZooKeeper to an embedded ZooKeeper in NiFi 1.x + +=== Usage +The `zk-migrator` tool is invoked as `./bin/zk-migrator.sh` or `bin\zk-migrator.bat`. + +To show help: + + ./bin/zk-migrator.sh -h + +The following are available options: + +* `-a`,`--auth ` Allows the specification of a username and password for authentication with ZooKeeper. This option is mutually exclusive with the `-k`,`--krb-conf` option. +* `-f`,`--file ` The file used for ZooKeeper data serialized as JSON. When used with the `-r`,`--receive` option, data read from ZooKeeper will be stored in the given filename. When used with the `-s`,`--send` option, the data in the file will be sent to ZooKeeper. +* `-h`,`--help` Prints help, displays available parameters with descriptions +* `--ignore-source` Allows the ZooKeeper Migrator to write to the ZooKeeper and path from which the data was obtained. +* `-k`,`--krb-conf ` Allows the specification of a JAAS configuration file to allow authentication with a ZooKeeper configured to use Kerberos. This option is mutually exclusive with the `-a`,`--auth` option. +* `-r`,`--receive` Receives data from ZooKeeper and writes to the given filename (if the `-f`,`--file` option is provided) or standard output. The data received will contain the full path to each node read from ZooKeeper. This option is mutually exclusive with the `-s`,`--send` option. +* `-s`,`--send` Sends data to ZooKeeper that is read from the given filename (if the `-f`,`--file` option is provided) or standard input. The paths for each node in the data being sent to ZooKeeper are absolute paths, and will be stored in ZooKeeper under the *path* portion of the `-z`,`--zookeeper` argument. Typically, the *path* portion of the argument can be omitted, which will store the nodes at their absolute paths. This option is mutually exclusive with the `-r`,`--receive` option. +* `--use-existing-acl` Allows the Zookeeper Migrator to write ACL values retrieved from the source Zookeeper server to destination server. Default action will apply Open rights for unsecured destinations or Creator Only rights for secured destinations. +* `-z`,`--zookeeper ` The ZooKeeper server(s) to use, specified by a connect string, comprised of one or more comma-separated host:port pairs followed by a path, in the format of _host:port[,host2:port...,hostn:port]/znode/path_. + +=== Migrating Between Source and Destination ZooKeepers +Before you begin, confirm that: + +* You have installed the destination ZooKeeper cluster. +* You have installed and configured a NiFi cluster to use the destination ZooKeeper cluster. +* If you are migrating ZooKeepers due to upgrading NiFi from 0.x to 1.x,, you have already followed appropriate NiFi upgrade steps. +* You have configured Kerberos as needed. +* You have not started processing any dataflow (to avoid duplicate data processing). +* If one of the ZooKeeper clusters you are using is configured with Kerberos, you are running the ZooKeeper Migrator from a host that has access to NiFi’s ZooKeeper client jaas configuration file (see the <> section in the System Administrator's Guide for more information). + +==== ZooKeeper Migration Steps +1. Collect the following information: ++ +|==== +|*Required Information*|*Description* +|Source ZooKeeper hostname (*sourceHostname*)|The hostname must be one of the hosts running in the ZooKeeper ensemble, which can be found in _/conf/zookeeper.properties_. Any of the hostnames declared in the `server.N` properties can be used. +|Destination ZooKeeper hostname (*destinationHostname*)|The hostname must be one of the hosts running in the ZooKeeper ensemble, which can be found in _/conf/zookeeper.properties_. Any of the hostnames declared in the `server.N` properties can be used. +|Source ZooKeeper port (*sourceClientPort*)|This can be found in _/conf/zookeeper.properties_. The port is specified in the `clientPort` property. +|Destination ZooKeeper port (*destinationClientPort*)|This can be found in _/conf/zookeeper.properties_. The port is specified in the `clientPort` property. +|Export data path|Determine the path that will store a json file containing the export of data from ZooKeeper. It must be readable and writable by the user running the zk-migrator tool. +|Source ZooKeeper Authentication Information|This information is in _/conf/state-management.xml_. For NiFi 0.x, if Creator Only is specified in _state-management.xml_, you need to supply authentication information using the `-a,--auth` argument with the values from the Username and Password properties in _state-management.xml_. For NiFi 1.x, supply authentication information using the `-k,--krb-conf` argument. + +If the _state-management.xml_ specifies Open, no authentication is required. +|Destination ZooKeeper Authentication Information|This information is in _/conf/state-management.xml_. For NiFi 0.x, if Creator Only is specified in _state-management.xml_, you need to supply authentication information using the `-a,--auth` argument with the values from the Username and Password properties in state-management.xml. For NiFi 1.x, supply authentication information using the `-k,--krb-conf` argument. + +If the _state-management.xml_ specifies Open, no authentication is required. +|Root path to which NiFi writes data in Source ZooKeeper (*sourceRootPath*)|This information can be found in `/conf/state-management.xml` under the Root Node property in the cluster-provider element. (default: `/nifi`) +|Root path to which NiFi writes data in Destination ZooKeeper (*destinationRootPath*)|This information can be found in _/conf/state-management.xml_ under the Root Node property in the cluster-provider element. +|==== +2. Stop all processors in the NiFi flow. If you are migrating between two NiFi installations, the flows on both must be stopped. +3. Export the NiFi component data from the source ZooKeeper. The following command reads from the specified ZooKeeper running on the given hostname:port, using the provided path to the data, and authenticates with ZooKeeper using the given username and password. The data read from ZooKeeper is written to the file provided. + +* For NiFi 0.x +** For an open ZooKeeper: +*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -f /path/to/export/zk-source-data.json` +** For a ZooKeeper using username:password for authentication: +*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -a -f /path/to/export/zk-source-data.json` + +* For NiFi 1.x +** For an open ZooKeeper: +*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -f /path/to/export/zk-source-data.json` +** For a ZooKeeper using Kerberos for authentication: +*** `zk-migrator.sh -r -z sourceHostname:sourceClientPort/sourceRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-source-data.json` + +4. (Optional) If you have used the new NiFi installation to do any processing, you can also export its ZooKeeper data as a backup prior to performing the migration. + +* For an open ZooKeeper: +** `zk-migrator.sh -r -z destinationHostname:destinationClientPort/destinationRootPath/components -f /path/to/export/zk-destination-backup-data.json` +* For a ZooKeeper using Kerberos for authentication: +** `zk-migrator.sh -r -z destinationHostname:destinationClientPort/destinationRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-destination-backup-data.json` + +5. Migrate the ZooKeeper data to the destination ZooKeeper. If the source and destination ZooKeepers are the same, the `--ignore-source` option can be added to the following examples. + +* For an open ZooKeeper: +** `zk-migrator.sh -s -z destinationHostname:destinationClientPort/destinationRootPath/components -f /path/to/export/zk-source-data.json` +* For a ZooKeeper using Kerberos for authentication: +** `zk-migrator.sh -s -z destinationHostname:destinationClientPort/destinationRootPath/components -k /path/to/jaasconfig/jaas-config.conf -f /path/to/export/zk-source-data.json` + +6. Once the migration has completed successfully, start the processors in the NiFi flow. Processing should continue from the point at which it was stopped when the NiFi flow was stopped. diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc index 294fd3ba6677..16863e57968b 100644 --- a/nifi-docs/src/main/asciidoc/user-guide.adoc +++ b/nifi-docs/src/main/asciidoc/user-guide.adoc @@ -71,7 +71,7 @@ UI may become unavailable. context about the data; they are made up of key-value pairs. All FlowFiles have the following Standard Attributes: -- *uuid*: A unique identifier for the FlowFile +- *uuid*: A Universally Unique Identifier that distinguishes the FlowFile from other FlowFiles in the system. - *filename*: A human-readable filename that may be used when storing the data to disk or in an external service - *path*: A hierarchically structured value that can be used when storing data to disk or an external service so that the data is not stored in a single directory @@ -119,13 +119,13 @@ UI may become unavailable. As a result, several components may be combined together to make a larger building block from which to create a dataflow. These templates can also be exported as XML and imported into another NiFi instance, allowing these building blocks to be shared. -*flow.xml.gz*: Everything the DFM puts onto the NiFi User Interface canvas is written, in real time, to one file called the flow.xml.gz. This file is located in the nifi/conf directory by default. - Any change made on the canvas is automatically saved to this file, without the user needing to click a "save" button. +*flow.xml.gz*: Everything the DFM puts onto the NiFi User Interface canvas is written, in real time, to one file called the _flow.xml.gz_. This file is located in the `nifi/conf` directory by default. + Any change made on the canvas is automatically saved to this file, without the user needing to click a "Save" button. In addition, NiFi automatically creates a backup copy of this file in the archive directory when it is updated. - You can use these archived files to rollback flow configuration. To do so, stop NiFi, replace flow.xml.gz with a desired backup copy, then restart NiFi. - In a clustered environment, stop the entire NiFi cluster, replace the flow.xml.gz of one of nodes, and restart the node. Remove flow.xml.gz from other nodes. + You can use these archived files to rollback flow configuration. To do so, stop NiFi, replace _flow.xml.gz_ with a desired backup copy, then restart NiFi. + In a clustered environment, stop the entire NiFi cluster, replace the _flow.xml.gz_ of one of nodes, and restart the node. Remove _flow.xml.gz_ from other nodes. Once you confirmed the node starts up as a one-node cluster, start the other nodes. The replaced flow configuration will be synchronized across the cluster. - The name and location of flow.xml.gz, and auto archive behavior are configurable. See the link:administration-guide.html#core-properties-br[System Administrator’s Guide] for further details. + The name and location of _flow.xml.gz_, and auto archive behavior are configurable. See the link:administration-guide.html#core-properties-br[System Administrator’s Guide] for further details. @@ -441,7 +441,7 @@ image:iconLabel.png["Label"] *Label*: Labels are used to provide documentation to parts of a dataflow. When a Label is dropped onto the canvas, it is created with a default size. The Label can then be resized by dragging the handle in the bottom-right corner. The Label has no text when initially created. The text of the Label can be added by right-clicking on the Label and -choosing `Configure` +choosing `Configure`. [[component-versioning]] @@ -540,17 +540,17 @@ data may be processable at a later time. When this occurs, the Processor may cho prevent the FlowFile from being Processed for some period of time. For example, if the Processor is to push the data to a remote service, but the remote service already has a file with the same name as the filename that the Processor is specifying, the Processor may penalize the FlowFile. The 'Penalty Duration' allows the DFM to specify how long the -FlowFile should be penalized. The default value is 30 seconds. +FlowFile should be penalized. The default value is `30 seconds`. Similarly, the Processor may determine that some situation exists such that the Processor can no longer make any progress, regardless of the data that it is processing. For example, if a Processor is to push data to a remote service and that service is not responding, the Processor cannot make any progress. As a result, the Processor should 'yield', which will prevent the Processor from being scheduled to run for some period of time. That period of time is specified by setting -the 'Yield Duration'. The default value is 1 second. +the 'Yield Duration'. The default value is `1 second`. The last configurable option on the left-hand side of the Settings tab is the Bulletin level. Whenever the Processor writes to its log, the Processor also will generate a Bulletin. This setting indicates the lowest level of Bulletin that should be -shown in the User Interface. By default, the Bulletin level is set to WARN, which means it will display all warning and error-level +shown in the User Interface. By default, the Bulletin level is set to `WARN`, which means it will display all warning and error-level bulletins. The right-hand side of the Settings tab contains an 'Automatically Terminate Relationships' section. Each of the Relationships that is @@ -570,6 +570,7 @@ The second tab in the Processor Configuration dialog is the Scheduling Tab: image::scheduling-tab.png["Scheduling Tab"] +===== Scheduling Strategy The first configuration option is the Scheduling Strategy. There are three possible options for scheduling components: *Timer driven*: This is the default mode. The Processor will be scheduled to run on a regular interval. The interval @@ -636,13 +637,15 @@ For example: For additional information and examples, see the link:http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html[Chron Trigger Tutorial^] in the Quartz documentation. -Next, the Scheduling Tab provides a configuration option named 'Concurrent Tasks'. This controls how many threads the Processor +===== Concurrent Tasks +Next, the Scheduling tab provides a configuration option named 'Concurrent Tasks'. This controls how many threads the Processor will use. Said a different way, this controls how many FlowFiles should be processed by this Processor at the same time. Increasing this value will typically allow the Processor to handle more data in the same amount of time. However, it does this by using system resources that then are not usable by other Processors. This essentially provides a relative weighting of Processors -- it controls how much of the system's resources should be allocated to this Processor instead of other Processors. This field is available for most Processors. There are, however, some types of Processors that can only be scheduled with a single Concurrent task. +===== Run Schedule The 'Run Schedule' dictates how often the Processor should be scheduled to run. The valid values for this field depend on the selected Scheduling Strategy (see above). If using the Event driven Scheduling Strategy, this field is not available. When using the Timer driven Scheduling Strategy, this value is a time duration specified by a number followed by a time unit. For example, `1 second` or `5 mins`. @@ -650,7 +653,8 @@ The default value of `0 sec` means that the Processor should run as often as pos for any time duration of 0, regardless of the time unit (i.e., `0 sec`, `0 mins`, `0 days`). For an explanation of values that are applicable for the CRON driven Scheduling Strategy, see the description of the CRON driven Scheduling Strategy itself. -When configured for clustering, an Execution setting will be available. This setting is used to determine which node(s) the Processor will be +===== Execution +The Execution setting is used to determine on which node(s) the Processor will be scheduled to execute. Selecting 'All Nodes' will result in this Processor being scheduled on every node in the cluster. Selecting 'Primary Node' will result in this Processor being scheduled on the Primary Node only. Processors that have been configured for 'Primary Node' execution are identified by a "P" next to the processor icon: @@ -660,6 +664,7 @@ To quickly identify 'Primary Node' processors, the "P" icon is also shown in the image::primary-node-processors-summary.png["Primary Node Processors in Summary Page"] +===== Run Duration The right-hand side of the Scheduling tab contains a slider for choosing the 'Run Duration'. This controls how long the Processor should be scheduled to run each time that it is triggered. On the left-hand side of the slider, it is marked 'Lower latency' while the right-hand side is marked 'Higher throughput'. When a Processor finishes running, it must update the repository in order to transfer the FlowFiles to @@ -672,8 +677,8 @@ Lower Latency or Higher Throughput. ==== Properties Tab -The Properties Tab provides a mechanism to configure Processor-specific behavior. There are no default properties. Each type of Processor -must define which Properties make sense for its use case. Below, we see the Properties Tab for a RouteOnAttribute Processor: +The Properties tab provides a mechanism to configure Processor-specific behavior. There are no default properties. Each type of Processor +must define which Properties make sense for its use case. Below, we see the Properties tab for a RouteOnAttribute Processor: image::properties-tab.png["Properties Tab"] @@ -743,7 +748,7 @@ so that it is not overridden by existing environment properties, system properti There are two ways to use and manage custom properties: * In the NiFi UI via the Variables window -* Referencing custom properties via 'nifi.properties' +* Referencing custom properties via _nifi.properties_ [[Variables_Window]] ==== Variables Window @@ -764,7 +769,7 @@ image::variables-context_menu-pg.png["Variables in Context Menu for PG"] ===== Creating a Variable -In the Variables window, click the "+" button to create a new variable. Add a name: +In the Variables window, click the `+` button to create a new variable. Add a name: image::variable-name.png["Variable Name Creation"] @@ -834,7 +839,7 @@ image::variable-unauthorized-ref-processor-canvas.png["Unauthorized Referencing Identify one or more sets of key/value pairs, and give them to your system administrator. Once the new custom properties have been added, ensure that the `nifi.variable.registry.properties` -field in the 'nifi.properties' file is updated with the custom properties location. +field in the _nifi.properties_ file is updated with the custom properties location. NOTE: NiFi must be restarted for these updates to be picked up. @@ -860,7 +865,7 @@ This displays the NiFi Settings window. The window has four tabs: General, Repor image:settings-general-tab.png["Controller Settings General Tab"] -To the right of the General tab is the Reporting Task Controller Services tab. From this tab, the DFM may click the "+" button in the upper-right corner to create a new Controller Service. +To the right of the General tab is the Reporting Task Controller Services tab. From this tab, the DFM may click the `+` button in the upper-right corner to create a new Controller Service. image:controller-services-tab.png["Controller Services Tab"] @@ -868,16 +873,16 @@ The Add Controller Service window opens. This window is similar to the Add Proce image:add-controller-service-window.png["Add Controller Service Window"] -Once you have added a Controller Service, you can configure it by clicking the Configure button in the -far-right column. Other buttons in this column include Enable, Remove and Access Policies. +Once you have added a Controller Service, you can configure it by clicking the `Configure` button in the +far-right column. Other buttons in this column include `Enable`, `Remove` and `Access Policies`. image:controller-services-configure-buttons.png["Controller Services Buttons"] -You can obtain information about Controller Services by clicking the Usage and Alerts buttons in the left-hand column. +You can obtain information about Controller Services by clicking the `Usage` and `Alerts` buttons in the left-hand column. image:controller-services-info-buttons.png["Controller Services Information Buttons"] -When the DFM clicks the Configure button, a Configure Controller Service window opens. It has three tabs: Settings, Properties,and Comments. This window is similar to the Configure Processor window. The Settings tab provides a place for the DFM to give the Controller Service a unique name (if desired). It also lists the UUID, Type, Bundle and Support information for the service and provides a list of other components (reporting tasks or other controller services) that reference the service. +When the DFM clicks the `Configure` button, a Configure Controller Service window opens. It has three tabs: Settings, Properties,and Comments. This window is similar to the Configure Processor window. The Settings tab provides a place for the DFM to give the Controller Service a unique name (if desired). It also lists the UUID, Type, Bundle and Support information for the service and provides a list of other components (reporting tasks or other controller services) that reference the service. image:configure-controller-service-settings.png["Configure Controller Service Settings"] @@ -885,7 +890,7 @@ The Properties tab lists the various properties that apply to the particular con image:configure-controller-service-properties.png["Configure Controller Service Properties"] -The Comments tab is just an open-text field, where the DFM may include comments about the service. After configuring a Controller Service, click the Apply button to apply the configuration and close the window, or click the Cancel button to cancel the changes and close the window. +The Comments tab is just an open-text field, where the DFM may include comments about the service. After configuring a Controller Service, click the `Apply` button to apply the configuration and close the window, or click the `Cancel` button to cancel the changes and close the window. [[Controller_Services_for_Dataflows]] @@ -905,7 +910,7 @@ Use the following steps to add a Controller Service: + image::process-group-configuration-window.png["Process Group Configuration Window"] 2. From the Process Group Configuration page, select the Controller Services tab. -3. Click the "+" button to display the Add Controller Service dialog. +3. Click the `+` button to display the Add Controller Service dialog. 4. Select the Controller Service desired, and click Add. 5. Perform any necessary Controller Service configuration tasks by clicking the Configure icon (image:iconConfigure.png["Configure"]) in the right-hand column. @@ -913,7 +918,7 @@ image::process-group-configuration-window.png["Process Group Configuration Windo [[Enabling_Disabling_Controller_Services]] ==== Enabling/Disabling Controller Services -After a Controller Service has been configured, it must be enabled in order to run. Do this using the Enable button (image:iconEnable.png["Enable Button"]) in the far-right column of the Controller Services tab. In order to modify an existing/running controller service, the DFM needs to stop/disable it (as well as all referencing reporting tasks and controller services). Do this using the Disable button (image:iconDisable.png["Disable Button"]). Rather than having to hunt down each component that is referenced by that controller service, the DFM has the ability to stop/disable them when disabling the controller service in question. When enabling a controller service, the DFM has the option to either start/enable the controller service and all referencing components or start/enable only the controller service itself. +After a Controller Service has been configured, it must be enabled in order to run. Do this using the `Enable` button (image:iconEnable.png["Enable Button"]) in the far-right column of the Controller Services tab. In order to modify an existing/running controller service, the DFM needs to stop/disable it (as well as all referencing reporting tasks and controller services). Do this using the `Disable` button (image:iconDisable.png["Disable Button"]). Rather than having to hunt down each component that is referenced by that controller service, the DFM has the ability to stop/disable them when disabling the controller service in question. When enabling a controller service, the DFM has the option to either start/enable the controller service and all referencing components or start/enable only the controller service itself. image:enable-controller-service-scope.png["Enable Controller Service Scope"] @@ -924,7 +929,7 @@ Reporting Tasks run in the background to provide statistical reports about what image:controller-settings-selection.png["Global Menu - Controller Settings"] -This displays the NiFi Settings window. Select the Reporting Tasks tab and click the "+" button in the upper-right corner to create a new Reporting Task. +This displays the NiFi Settings window. Select the Reporting Tasks tab and click the `+` button in the upper-right corner to create a new Reporting Task. image:reporting-tasks-tab.png["Reporting Tasks Tab"] @@ -932,15 +937,15 @@ The Add Reporting Task window opens. This window is similar to the Add Processor image:add-reporting-task-window.png["Add Reporting Task Window"] -Once a Reporting Task has been added, the DFM may configure it by clicking the Edit button in the far-right column. Other buttons in this column include Start, Remove, State and Access Policies. +Once a Reporting Task has been added, the DFM may configure it by clicking the `Edit` button in the far-right column. Other buttons in this column include `Start`, `Remove`, `State` and `Access Policies`. image:reporting-tasks-edit-buttons.png["Reporting Tasks Edit Buttons"] -You can obtain information about Reporting Tasks by clicking the View Details, Usage, and Alerts buttons in the left-hand column. +You can obtain information about Reporting Tasks by clicking the `View Details`, `Usage`, and `Alerts` buttons in the left-hand column. image:reporting-tasks-info-buttons.png["Reporting Tasks Information Buttons"] -When the DFM clicks the Edit button, a Configure Reporting Task window opens. It has three tabs: Settings, Properties, and Comments. This window is similar to the Configure Processor window. The Settings tab provides a place for the DFM to give the Reporting Task a unique name (if desired). It also lists the UUID, Type, and Bundle information for the task and provides settings for the task's Scheduling Strategy and Run Schedule (similar to the same settings in a processor). The DFM may hover the mouse over the question mark icons to see more information about each setting. +When the DFM clicks the `Edit` button, a Configure Reporting Task window opens. It has three tabs: Settings, Properties, and Comments. This window is similar to the Configure Processor window. The Settings tab provides a place for the DFM to give the Reporting Task a unique name (if desired). It also lists the UUID, Type, and Bundle information for the task and provides settings for the task's Scheduling Strategy and Run Schedule (similar to the same settings in a processor). The DFM may hover the mouse over the question mark icons to see more information about each setting. image:configure-reporting-task-settings.png["Configure Reporting Task Settings"] @@ -948,9 +953,9 @@ The Properties tab lists the various properties that may be configured for the t image:configure-reporting-task-properties.png["Configure Reporting Task Properties"] -The Comments tab is just an open-text field, where the DFM may include comments about the task. After configuring the Reporting Task, click the Apply button to apply the configuration and close the window, or click the Cancel button to cancel the changes and close the window. +The Comments tab is just an open-text field, where the DFM may include comments about the task. After configuring the Reporting Task, click the `Apply` button to apply the configuration and close the window, or click the `Cancel` button to cancel the changes and close the window. -When you want to run the Reporting Task, click the Start button (image:iconStart.png["Start Button"]). +When you want to run the Reporting Task, click the `Start` button (image:iconStart.png["Start Button"]). [[Connecting_Components]] @@ -973,7 +978,7 @@ and the same 'Create Connection' dialog appears. ==== Details Tab -The Details Tab of the 'Create Connection' dialog provides information about the source and destination components, including the component name, the +The Details tab of the 'Create Connection' dialog provides information about the source and destination components, including the component name, the component type, and the Process Group in which the component lives: image::create-connection.png["Create Connection"] @@ -986,13 +991,11 @@ automatically be 'cloned', and a copy will be sent to each of those Connections. ==== Settings -The Settings Tab provides the ability to configure the Connection's name, FlowFile expiration, Back Pressure thresholds, and -Prioritization: +The Settings tab provides the ability to configure the Connection's Name, FlowFile Expiration, Back Pressure Thresholds, Load Balance Strategy and Prioritization: image:connection-settings.png["Connection Settings"] -The Connection name is optional. If not specified, the name shown for the Connection will be names of the Relationships -that are active for the Connection. +The Connection name is optional. If not specified, the name shown for the Connection will be names of the Relationships that are active for the Connection. ===== FlowFile Expiration FlowFile expiration is a concept by which data that cannot be processed in a timely fashion can be automatically removed from the flow. @@ -1012,8 +1015,8 @@ is the "Back pressure data size threshold." This specifies the maximum amount of applying back pressure. This value is configured by entering a number followed by a data size (`B` for bytes, `KB` for kilobytes, `MB` for megabytes, `GB` for gigabytes, or `TB` for terabytes). -NOTE: By default each new connection added will have a default Back Pressure Object Threshold of 10,000 objects and Back Pressure Data Size Threshold of 1 GB. -These defaults can be changed by modifying the appropriate properties in the `nifi.properties` file. +NOTE: By default each new connection added will have a default Back Pressure Object Threshold of `10,000 objects` and Back Pressure Data Size Threshold of `1 GB`. +These defaults can be changed by modifying the appropriate properties in the _nifi.properties_ file. When back pressure is enabled, small progress bars appear on the connection label, so the DFM can see it at-a-glance when looking at a flow on the canvas. The progress bars change color based on the queue percentage: Green (0-60%), Yellow (61-85%) and Red (86-100%). @@ -1027,6 +1030,54 @@ When the queue is completely full, the Connection is highlighted in red. image:back_pressure_full.png["Back Pressure Queue Full"] +===== Load Balancing + +[[load_balance_strategy]] +====== Load Balance Strategy +To distribute the data in a flow across the nodes in the cluster, NiFi offers the following load balance strategies: + +- *Do not load balance*: Do not load balance FlowFiles between nodes in the cluster. This is the default. +- *Partition by attribute*: Determines which node to send a given FlowFile to based on the value of a user-specified FlowFile Attribute. All FlowFiles that have the same value for the Attribute will be sent to the same node in the cluster. If the destination node is disconnected from the cluster or if unable to communicate, the data does not fail over to another node. The data will queue, waiting for the node to be available again. Additionally, if a node joins or leaves the cluster necessitating a rebalance of the data, consistent hashing is applied to avoid having to redistribute all of the data. +- *Round robin*: FlowFiles will be distributed to nodes in the cluster in a round-robin fashion. If a node is disconnected from the cluster or if unable to communicate with a node, the data that is queued for that node will be automatically redistributed to another node(s). +- *Single node*: All FlowFiles will be sent to a single node in the cluster. Which node they are sent to is not configurable. If the node is disconnected from the cluster or if unable to communicate with the node, the data that is queued for that node will remain queued until the node is available again. + +NOTE: In addition to the UI settings, there are <> related to load balancing that must also be configured in _nifi.properties_. + +NOTE: NiFi persists the nodes that are in a cluster across restarts. This prevents the redistribution of data until all of the nodes have connected. If the cluster is shutdown and a node is not intended to be brought back up, the user is responsible for removing the node from the cluster via the "Cluster" dialog in the UI (see <> for more information). + +====== Load Balance Compression +After selecting the load balance strategy, the user can configure whether or not data should be compressed when being transferred between nodes in the cluster. + +image:load_balance_compression_options.png["Load Balance Compression Options"] + +The following compression options are available: + +- *Do not compress*: FlowFiles will not be compressed. This is the default. +- *Compress attributes only*: FlowFile attributes will be compressed, but FlowFile contents will not. +- *Compress attributes and content*: FlowFile attributes and contents will be compressed. + +====== Load Balance Indicator +When a load balance strategy has been implemented for a connection, a load balance indicator (image:iconLoadBalance.png["Load Balance Icon"]) will appear on the connection: + +image:load_balance_configured_connection.png["Connection Configured with Load Balance Strategy"] + +Hovering over the icon will display the connection's load balance strategy and compression configuration. The icon in this state also indicates that all data in the connection has been distributed across the cluster. + +image:load_balance_distributed_connection.png["Distributed Load Balance Connection"] + +When data is actively being transferred between the nodes in the cluster, the load balance indicator will change orientation and color: + +image:load_balance_active_connection.png["Active Load Balance Connection"] + +====== Cluster Connection Summary +To see where data has been distributed among the cluster nodes, select Summary from the Global Menu. Then select the "Connections" tab and the "View Connection Details" icon for a source: + +image:summary_connections.png["NiFi Summary Connections"] + +This will open the Cluster Connection Summary dialog, which shows the data on each node in the cluster: + +image:cluster_connection_summary.png["Cluster Connection Summary Dialog"] + ===== Prioritization The right-hand side of the tab provides the ability to prioritize the data in the queue so that higher priority data is processed first. Prioritizers can be dragged from the top ('Available prioritizers') to the bottom ('Selected prioritizers'). @@ -1040,9 +1091,15 @@ The following prioritizers are available: - *FirstInFirstOutPrioritizer*: Given two FlowFiles, the one that reached the connection first will be processed first. - *NewestFlowFileFirstPrioritizer*: Given two FlowFiles, the one that is newest in the dataflow will be processed first. - *OldestFlowFileFirstPrioritizer*: Given two FlowFiles, the one that is oldest in the dataflow will be processed first. 'This is the default scheme that is used if no prioritizers are selected'. -- *PriorityAttributePrioritizer*: Given two FlowFiles that both have a "priority" attribute, the one that has the highest priority value will be processed first. Note that an UpdateAttribute processor should be used to add the "priority" attribute to the FlowFiles before they reach a connection that has this prioritizer set. Values for the "priority" attribute may be alphanumeric, where "a" is a higher priority than "z", and "1" is a higher priority than "9", for example. +- *PriorityAttributePrioritizer*: Given two FlowFiles, an attribute called “priority” will be extracted. The one that has the lowest priority value will be processed first. +** Note that an UpdateAttribute processor should be used to add the "priority" attribute to the FlowFiles before they reach a connection that has this prioritizer set. +** If only one has that attribute it will go first. +** Values for the "priority" attribute can be alphanumeric, where "a" will come before "z" and "1" before "9" +** If "priority" attribute cannot be parsed as a long, unicode string ordering will be used. For example: "99" and "100" will be ordered so the flowfile with "99" comes first, but "A-99" and "A-100" will sort so the flowfile with "A-100" comes first. -===== Changing Configuration and Context Menu Options +NOTE: With a <> configured, the connection has a queue per node in addition to the local queue. The prioritizer will sort the data in each queue independently. + +==== Changing Configuration and Context Menu Options After a connection has been drawn between two components, the connection's configuration may be changed, and the connection may be moved to a new destination; however, the processors on either side of the connection must be stopped before a configuration or destination change may be made. image:nifi-connection.png["Connection"] @@ -1207,7 +1264,7 @@ After you drag the GenerateFlowFile and LogAttribute processors to the canvas an * Generate FlowFile ** On the Scheduling tab, set Run schedule to: 5 sec. Note that the GenerateFlowFile processor can create many FlowFiles very quickly; that's why setting the Run schedule is important so that this flow does not overwhelm the system NiFi is running on. -** On the Properties tab, set File Size to: 10 kb +** On the Properties tab, set File Size to: 10 KB * Log Attribute ** On the Settings tab, under Auto-terminate relationships, select the checkbox next to Success. This will terminate FlowFiles after this processor has successfully processed them. @@ -1243,7 +1300,7 @@ In order to start a component, the following conditions must be met: - The component must have no active tasks. For more information about active tasks, see the "Anatomy of ..." sections under <> (<>, <>, <>). -Components can be started by selecting all of the components to start and then clicking the Start button ( +Components can be started by selecting all of the components to start and then clicking the `Start` button ( image:buttonStart.png["Start"] ) in the Operate Palette or by right-clicking a single component and choosing Start from the context menu. @@ -1259,7 +1316,7 @@ image:iconRun.png["Run"] === Stopping a Component A component can be stopped any time that it is running. A component is stopped by right-clicking on the component -and clicking Stop from the context menu, or by selecting the component and clicking the Stop button ( +and clicking Stop from the context menu, or by selecting the component and clicking the `Stop` button ( image:buttonStop.png["Stop"] ) in the Operate Palette. @@ -1283,7 +1340,7 @@ intentionally not running and those that may have been stopped temporarily (for configuration) and inadvertently were never restarted. When it is desirable to re-enable a component, it can be enabled by selecting the component and -clicking the Enable button ( +clicking the `Enable` button ( image:buttonEnable.png["Enable"] ) in the Operate Palette. This is available only when the selected component or components are disabled. Alternatively, a component can be enabled by checking the checkbox next to the "Enabled" option in @@ -1295,7 +1352,7 @@ image:iconAlert.png["Invalid"] image:iconStop.png["Stopped"] ), depending on whether or not the component is valid. -A component is then disabled by selecting the component and clicking the Disable button ( +A component is then disabled by selecting the component and clicking the `Disable` button ( image:buttonDisable.png["Disable"] ) in the Operate Palette, or by clearing the checkbox next to the "Enabled" option in the Settings tab of the Processor configuration dialog or the configuration dialog for a Port. @@ -1644,10 +1701,8 @@ The FlowFiles enqueued in a Connection can be viewed when necessary. The Queue l a Connection's context menu. The listing will return the top 100 FlowFiles in the active queue according to the configured priority. The listing can be performed even if the source and destination are actively running. -Additionally, details for a Flowfile in the listing can be viewed by clicking on the Details icon ( -image:iconDetails.png["Details"] -) in the left most column. From here, the FlowFile details and attributes are available as well buttons for -downloading or viewing the content. Viewing the content is only available if the nifi.content.viewer.url has been configured. +Additionally, details for a Flowfile in the listing can be viewed by clicking the `Details` button (image:iconDetails.png["Details"]) in the left most column. From here, the FlowFile details and attributes are available as well as buttons for +downloading or viewing the content. Viewing the content is only available if the `nifi.content.viewer.url` has been configured. If the source or destination of the Connection are actively running, there is a chance that the desired FlowFile will no longer be in the active queue. @@ -1697,8 +1752,8 @@ The Summary page also includes the following elements: - *Status History*: Clicking the Status History icon will open a new dialog that shows a historical view of the statistics that are rendered for this component. See the section <> for more information. -- *Refresh*: The Refresh button allows the user to refresh the information displayed without closing the dialog and opening it - again. The time at which the information was last refreshed is shown just to the right of the Refresh button. The information +- *Refresh*: The `Refresh` button allows the user to refresh the information displayed without closing the dialog and opening it + again. The time at which the information was last refreshed is shown just to the right of the `Refresh` button. The information on the page is not automatically refreshed. - *Filter*: The Filter element allows users to filter the contents of the Summary table by typing in all or part of some criteria, @@ -1709,9 +1764,9 @@ The Summary page also includes the following elements: entries exist in the table. - *Pop-Out*: When monitoring a flow, it is helpful to be able to open the Summary table in a separate browser tab or window. The - Pop-Out button, next to the Close button, will cause the entire Summary dialog to be opened in a new browser tab or window + "Pop Out" button, next to the `Close` button, will cause the entire Summary dialog to be opened in a new browser tab or window (depending on the configuration of the browser). Once the page is "popped out", the dialog is closed in the original - browser tab/window. In the new tab/window, the Pop-Out button and the Go-To button will no longer be available. + browser tab/window. In the new tab/window, the "Pop Out" button and the "Go To" button will no longer be available. - *System Diagnostics*: The System Diagnostics window provides information about how the system is performing with respect to system resource utilization. While this is intended mostly for administrators, it is provided in this view because it @@ -1729,7 +1784,7 @@ past five minutes, it is often useful to have a view of historical statistics as by right-clicking on a component and choosing the "Status History" menu option or by clicking on the Status History in the Summary page (see <> for more information). -The amount of historical information that is stored is configurable in the NiFi properties but defaults to 24 hours. For specific +The amount of historical information that is stored is configurable in the NiFi properties but defaults to `24 hours`. For specific configuration information reference the Component Status Repository of the link:administration-guide.html[System Administrator’s Guide]. When the Status History dialog is opened, it provides a graph of historical statistics: @@ -1781,7 +1836,7 @@ To connect NiFi to a Registry, select Controller Settings from the Global Menu. image::controller-settings-selection.png["Global Menu - Controller Settings"] -This displays the NiFi Settings window. Select the Registry Clients tab and click the "+" button in the upper-right corner to register a new Registry client. +This displays the NiFi Settings window. Select the Registry Clients tab and click the `+` button in the upper-right corner to register a new Registry client. image::registry-clients-tab.png["Registry Clients Tab"] @@ -1877,7 +1932,9 @@ The following actions are not considered local changes: * modifying sensitive property values * modifying remote process group URLs * updating a processor that was referencing a non-existent controller service to reference an externally available controller service -* modifying variables +* creating, modifying or deleting variables + +NOTE: Creating a variable does not trigger a local change because creating a variable on its own has not changed anything about what the flow processes. A component will have to be created or modified that uses the new variable, which will trigger a local change. Modifying a variable does not trigger a local change because variable values are intended to be different in each environment. When a versioned flow is imported, it is assumed there is a one-time operation required to set those variables specific for the given environment. Deleting a variable does not trigger a local change because the component that references that variable will need need to be modified, which will trigger a local change. WARNING: Variables do not support sensitive values and will be included when versioning a Process Group. See <> for more information. @@ -2093,7 +2150,7 @@ received from others can then be imported into an instance of NiFi and dragged o [[Create_Template]] === Creating a Template To create a Template, select the components that are to be a part of the template, and then click the -"Create Template" ( +`Create Template` ( image:iconNewTemplate.png["Create Template"] ) button in the Operate Palette (See <> for more information on the Operate Palette). @@ -2102,7 +2159,7 @@ current Process Group. This means that creating a Template with nothing selected will create a single Template that contains the entire flow. After clicking this button, the user is prompted to provide a name and an optional description for the template. -Each template must have a unique name. After entering the name and optional description, clicking the Create button +Each template must have a unique name. After entering the name and optional description, clicking the `Create` button will generate the template and notify the user that the template was successfully created, or provide an appropriate error message if unable to create the template for some reason. @@ -2118,12 +2175,12 @@ After receiving a Template that has been exported from another NiFi, the first s the template into this instance of NiFi. You may import templates into any Process Group where you have the appropriate authorization. -From the Operate Palette, click the "Upload Template" ( +From the Operate Palette, click the `Upload Template` ( image:iconUploadTemplate.png["Upload Template"] ) button (see <> for more information on the Operate Palette). This will display the Upload Template dialog. Click the find icon and use the File Selection dialog to choose which template file to upload. Select the file and click Open. -Clicking the "Upload" button will attempt to import the Template into this instance of NiFi. +Clicking the `Upload` button will attempt to import the Template into this instance of NiFi. The Upload Template dialog will update to show "Success" or an error message if there was a problem importing the template. @@ -2135,7 +2192,7 @@ image:iconTemplate.png["Template"] ) from the Components Toolbar (see <>) onto the canvas. This will present a dialog to choose which Template to add to the canvas. After choosing the Template to add, simply -click the "Add" button. The Template will be added to the canvas with the upper-left-hand side of the Template +click the `Add` button. The Template will be added to the canvas with the upper-left-hand side of the Template being placed wherever the user dropped the Template icon. This leaves the contents of the newly instantiated Template selected. If there was a mistake, and this Template is no @@ -2156,8 +2213,8 @@ filter the templates to see only those of interest, export, and delete Templates ==== Exporting a Template Once a Template has been created, it can be shared with others in the Template Management page. To export a Template, locate the Template in the table. The Filter in the top-right corner -can be used to help find the appropriate Template if several are available. Then click the Export or Download button ( -image:iconExport.png["Export"] +can be used to help find the appropriate Template if several are available. Then click the `Download` button ( +image:iconDownloadTemplate.png["Export"] ). This will download the template as an XML file to your computer. This XML file can then be sent to others and imported into other instances of NiFi (see <>). @@ -2165,7 +2222,7 @@ into other instances of NiFi (see <>). ==== Removing a Template Once it is decided that a Template is no longer needed, it can be easily removed from the Template Management page. To delete a Template, locate it in the table (the Filter in the top-right corner -may be used to find the appropriate Template if several are available) and click the Delete button ( +may be used to find the appropriate Template if several are available) and click the `Delete` button ( image:iconDelete.png["Delete"] ). This will prompt for confirmation. After confirming the deletion, the Template will be removed from this table and will no longer be available to add to the canvas. @@ -2179,8 +2236,7 @@ like dataflow compliance and optimization in real time. By default, NiFi updates is configurable. -To access the Data Provenance page, select Data Provenance from the Global Menu. Clicking this button opens a dialog window t -hat allows the user to see the most recent Data Provenance information available, +To access the Data Provenance page, select "Data Provenance" from the Global Menu. This opens a dialog window that allows the user to see the most recent Data Provenance information available, search the information for specific items, and filter the search results. It is also possible to open additional dialog windows to see event details, replay data at any point within the dataflow, and see a graphical representation of the data's lineage, or path through the flow. (These features are described in depth below.) @@ -2238,7 +2294,7 @@ image:search-receive-event-abc.png["Search for RECEIVE Event"] [[event_details]] === Details of an Event -In the far-left column of the Data Provenance page, there is a View Details icon for each event (image:iconDetails.png["Details"]). +In the far-left column of the Data Provenance page, there is a `View Details` icon for each event (image:iconDetails.png["Details"]). Clicking this button opens a dialog window with three tabs: Details, Attributes, and Content. image:event-details.png["Event Details", width=700] @@ -2297,7 +2353,7 @@ image:expanded-events.png["Expanded Events"] [[writeahead-provenance]] === Write Ahead Provenance Repository -By default, the Provenance Repository is implemented in a Persistent Provenance configuration. In Apache NiFi 1.2.0, the Write Ahead configuration was introduced to provide the same capabilities as Persistent Provenance, but with far better performance. Migrating to the Write Ahead configuration is easy to accomplish. Simply change the setting for the `nifi.provenance.repository.implementation` system property in the `nifi.properties` file from the default value of `org.apache.nifi.provenance.PersistentProvenanceRepository` to `org.apache.nifi.provenance.WriteAheadProvenanceRepository` and restart NiFi. +By default, the Provenance Repository is implemented in a Persistent Provenance configuration. In Apache NiFi 1.2.0, the Write Ahead configuration was introduced to provide the same capabilities as Persistent Provenance, but with far better performance. Migrating to the Write Ahead configuration is easy to accomplish. Simply change the setting for the `nifi.provenance.repository.implementation` system property in the _nifi.properties_ file from the default value of `org.apache.nifi.provenance.PersistentProvenanceRepository` to `org.apache.nifi.provenance.WriteAheadProvenanceRepository` and restart NiFi. However, to increase the chances of a successful migration consider the following factors and recommended actions. @@ -2309,7 +2365,7 @@ The `WriteAheadProvenanceRepository` can use the Provenance data stored by the ` If you are upgrading from an older version of NiFi to 1.2.0 or later, it is recommended that you do not change the provenance configuration to Write Ahead until you confirm your flows and environment are stable in 1.2.0 first. This reduces the number of variables in your upgrade and can simplify the debugging process if any issues arise. ==== Bootstrap.conf -While better performance is achieved with the G1 garbage collector, Java 8 bugs may surface more frequently in the Write Ahead configuration. It is recommended that the following line is commented out in the `bootstrap.conf` file in the `conf` directory: +While better performance is achieved with the G1 garbage collector, Java 8 bugs may surface more frequently in the Write Ahead configuration. It is recommended that the following line is commented out in the _bootstrap.conf_ file in the `conf` directory: .... java.arg.13=-XX:+UseG1GC @@ -2352,10 +2408,10 @@ The `EncryptedWriteAheadProvenanceRepository` is a new implementation of the pro The `WriteAheadProvenanceRepository` was introduced in NiFi 1.2.0 and provided a refactored and much faster provenance repository implementation than the previous `PersistentProvenanceRepository`. The encrypted version wraps that implementation with a record writer and reader which encrypt and decrypt the serialized bytes respectively. -The fully qualified class `org.apache.nifi.provenance.EncryptedWriteAheadProvenanceRepository` is specified as the provenance repository implementation in `nifi.properties` as the value of `nifi.provenance.repository.implementation`. In addition, <> must be populated to allow successful initialization. +The fully qualified class `org.apache.nifi.provenance.EncryptedWriteAheadProvenanceRepository` is specified as the provenance repository implementation in _nifi.properties_ as the value of `nifi.provenance.repository.implementation`. In addition, <> must be populated to allow successful initialization. ===== StaticKeyProvider -The `StaticKeyProvider` implementation defines keys directly in `nifi.properties`. Individual keys are provided in hexadecimal encoding. The keys can also be encrypted like any other sensitive property in `nifi.properties` using the <> tool in the NiFi Toolkit. +The `StaticKeyProvider` implementation defines keys directly in _nifi.properties_. Individual keys are provided in hexadecimal encoding. The keys can also be encrypted like any other sensitive property in _nifi.properties_ using the <> tool in the NiFi Toolkit. The following configuration section would result in a key provider with two available keys, "Key1" (active) and "AnotherKey". .... @@ -2376,10 +2432,10 @@ key4=kZprfcTSTH69UuOU3jMkZfrtiVR/eqWmmbdku3bQcUJ/+UToecNB5lzOVEMBChyEXppyXXC35Wa key5=c6FzfnKm7UR7xqI2NFpZ+fEKBfSU7+1NvRw+XWQ9U39MONWqk5gvoyOCdFR1kUgeg46jrN5dGXk13sRqE0GETQ== .... -Each line defines a key ID and then the Base64-encoded cipher text of a 16 byte IV and wrapped AES-128, AES-192, or AES-256 key depending on the JCE policies available. The individual keys are wrapped by AES/GCM encryption using the **master key** defined by `nifi.bootstrap.sensitive.key` in `conf/bootstrap.conf`. +Each line defines a key ID and then the Base64-encoded cipher text of a 16 byte IV and wrapped AES-128, AES-192, or AES-256 key depending on the JCE policies available. The individual keys are wrapped by AES/GCM encryption using the **master key** defined by `nifi.bootstrap.sensitive.key` in _conf/bootstrap.conf_. ===== Key Rotation -Simply update `nifi.properties` to reference a new key ID in `nifi.provenance.repository.encryption.key.id`. Previously-encrypted events can still be decrypted as long as that key is still available in the key definition file or `nifi.provenance.repository.encryption.key.id.` as the key ID is serialized alongside the encrypted record. +Simply update _nifi.properties_ to reference a new key ID in `nifi.provenance.repository.encryption.key.id`. Previously-encrypted events can still be decrypted as long as that key is still available in the key definition file or `nifi.provenance.repository.encryption.key.id.` as the key ID is serialized alongside the encrypted record. ==== Writing and Reading Event Records Once the repository is initialized, all provenance event record write operations are serialized according to the configured schema writer (`EventIdFirstSchemaRecordWriter` by default for `WriteAheadProvenanceRepository`) to a `byte[]`. Those bytes are then encrypted using an implementation of `ProvenanceEventEncryptor` (the only current implementation is `AES/GCM/NoPadding`) and the encryption metadata (`keyId`, `algorithm`, `version`, `IV`) is serialized and prepended. The complete `byte[]` is then written to the repository on disk as normal. diff --git a/nifi-external/nifi-example-bundle/nifi-nifi-example-nar/pom.xml b/nifi-external/nifi-example-bundle/nifi-nifi-example-nar/pom.xml index 591b86e6efdc..5b0aecc7278b 100644 --- a/nifi-external/nifi-example-bundle/nifi-nifi-example-nar/pom.xml +++ b/nifi-external/nifi-example-bundle/nifi-nifi-example-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-example-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-example-nar diff --git a/nifi-external/nifi-example-bundle/nifi-nifi-example-processors/pom.xml b/nifi-external/nifi-example-bundle/nifi-nifi-example-processors/pom.xml index 9e2f84935c00..e890a7d570e1 100644 --- a/nifi-external/nifi-example-bundle/nifi-nifi-example-processors/pom.xml +++ b/nifi-external/nifi-example-bundle/nifi-nifi-example-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-example-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-nifi-example-processors @@ -29,17 +29,17 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-external/nifi-example-bundle/pom.xml b/nifi-external/nifi-example-bundle/pom.xml index e725e49d31d8..dc9ab7ea423e 100644 --- a/nifi-external/nifi-example-bundle/pom.xml +++ b/nifi-external/nifi-example-bundle/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-external - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-example-bundle @@ -35,7 +35,7 @@ org.apache.nifi nifi-nifi-example-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-external/nifi-spark-receiver/pom.xml b/nifi-external/nifi-spark-receiver/pom.xml index 581ee4cb7693..ad7ecb17eb9c 100644 --- a/nifi-external/nifi-spark-receiver/pom.xml +++ b/nifi-external/nifi-spark-receiver/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-external - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-spark-receiver @@ -33,7 +33,7 @@ org.apache.nifi nifi-site-to-site-client - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.sun.jersey diff --git a/nifi-external/nifi-storm-spout/pom.xml b/nifi-external/nifi-storm-spout/pom.xml index d81249f4bbf2..eccc78d9c8c6 100644 --- a/nifi-external/nifi-storm-spout/pom.xml +++ b/nifi-external/nifi-storm-spout/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-external - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-storm-spout @@ -37,7 +37,7 @@ org.apache.nifi nifi-site-to-site-client - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-external/pom.xml b/nifi-external/pom.xml index 4e73ef72c9c2..3122248550ff 100644 --- a/nifi-external/pom.xml +++ b/nifi-external/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-external diff --git a/nifi-framework-api/pom.xml b/nifi-framework-api/pom.xml index 2144b2e2d1c7..f04afa85f966 100644 --- a/nifi-framework-api/pom.xml +++ b/nifi-framework-api/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-api jar @@ -27,7 +27,7 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/authorization/Group.java b/nifi-framework-api/src/main/java/org/apache/nifi/authorization/Group.java index 7908e856e003..72e03c680239 100644 --- a/nifi-framework-api/src/main/java/org/apache/nifi/authorization/Group.java +++ b/nifi-framework-api/src/main/java/org/apache/nifi/authorization/Group.java @@ -89,7 +89,7 @@ public int hashCode() { @Override public String toString() { - return String.format("identifier[%s], name[%s]", getIdentifier(), getName()); + return String.format("identifier[%s], name[%s], users[%s]", getIdentifier(), getName(), String.join(", ", users)); } diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/FlowFileQueue.java b/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/FlowFileQueue.java index 2c7f55b5ae4b..8870f1d1ce18 100644 --- a/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/FlowFileQueue.java +++ b/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/FlowFileQueue.java @@ -267,6 +267,22 @@ public interface FlowFileQueue { void setLoadBalanceStrategy(LoadBalanceStrategy strategy, String partitioningAttribute); + /** + * Offloads the flowfiles in the queue to other nodes. This disables the queue from partition flowfiles locally. + *

+ * This operation is a no-op if the node that contains this queue is not in a cluster. + */ + void offloadQueue(); + + /** + * Resets a queue that has previously been offloaded. This allows the queue to partition flowfiles locally, and + * has no other effect on processors or remote process groups. + *

+ * This operation is a no-op if the queue is not currently offloaded or the node that contains this queue is not + * clustered. + */ + void resetOffloadedQueue(); + LoadBalanceStrategy getLoadBalanceStrategy(); void setLoadBalanceCompression(LoadBalanceCompression compression); diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/LoadBalancedFlowFileQueue.java b/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/LoadBalancedFlowFileQueue.java index f0eff27ef2e0..b9f695196044 100644 --- a/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/LoadBalancedFlowFileQueue.java +++ b/nifi-framework-api/src/main/java/org/apache/nifi/controller/queue/LoadBalancedFlowFileQueue.java @@ -66,4 +66,10 @@ public interface LoadBalancedFlowFileQueue extends FlowFileQueue { */ boolean isPropagateBackpressureAcrossNodes(); + /** + * Determines whether or not the local partition's size >= backpressure threshold + * + * @return true if the number of FlowFiles or total size of FlowFiles in the local partition alone meets or exceeds the backpressure threshold, false otherwise. + */ + boolean isLocalPartitionFull(); } diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/MetricDescriptor.java b/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/MetricDescriptor.java index c0c52b6af765..14887ef2f363 100644 --- a/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/MetricDescriptor.java +++ b/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/MetricDescriptor.java @@ -64,4 +64,9 @@ enum Formatter { * into a single Long value */ ValueReducer getValueReducer(); + + /** + * @return true if the metric is for a component Counter, false otherwise + */ + boolean isCounter(); } diff --git a/nifi-maven-archetypes/nifi-processor-bundle-archetype/pom.xml b/nifi-maven-archetypes/nifi-processor-bundle-archetype/pom.xml index 1aff5b37af06..460d2acdbb3c 100644 --- a/nifi-maven-archetypes/nifi-processor-bundle-archetype/pom.xml +++ b/nifi-maven-archetypes/nifi-processor-bundle-archetype/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-maven-archetypes - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-processor-bundle-archetype diff --git a/nifi-maven-archetypes/nifi-service-bundle-archetype/pom.xml b/nifi-maven-archetypes/nifi-service-bundle-archetype/pom.xml index dab3a339597a..ed8dd46e1344 100644 --- a/nifi-maven-archetypes/nifi-service-bundle-archetype/pom.xml +++ b/nifi-maven-archetypes/nifi-service-bundle-archetype/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-maven-archetypes - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-service-bundle-archetype diff --git a/nifi-maven-archetypes/pom.xml b/nifi-maven-archetypes/pom.xml index d89d5e972882..c4660ac31831 100644 --- a/nifi-maven-archetypes/pom.xml +++ b/nifi-maven-archetypes/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-maven-archetypes diff --git a/nifi-mock/pom.xml b/nifi-mock/pom.xml index 583090451404..9589ee84219e 100644 --- a/nifi-mock/pom.xml +++ b/nifi-mock/pom.xml @@ -18,29 +18,29 @@ org.apache.nifi nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-mock org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-framework-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-expression-language - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT asm @@ -51,7 +51,7 @@ org.apache.nifi nifi-data-provenance-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-ambari-bundle/pom.xml b/nifi-nar-bundles/nifi-ambari-bundle/pom.xml index 51479be390e8..a443ef7bbcb1 100644 --- a/nifi-nar-bundles/nifi-ambari-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-ambari-bundle/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-ambari-bundle diff --git a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-nar/pom.xml b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-nar/pom.xml index 6d7f2f69ac86..11f927942659 100644 --- a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-nar/pom.xml +++ b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-nar/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-amqp-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-amqp-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -31,7 +31,7 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/pom.xml b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/pom.xml index 4a61ee4021ad..27c7bc48ee15 100644 --- a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/pom.xml +++ b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-amqp-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-amqp-processors jar @@ -40,12 +40,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/main/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessor.java b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/main/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessor.java index 2a8349780155..238b1be87055 100644 --- a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/main/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessor.java +++ b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/main/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessor.java @@ -16,17 +16,11 @@ */ package org.apache.nifi.amqp.processors; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.net.ssl.SSLContext; - +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultSaslConfig; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.lifecycle.OnStopped; -import org.apache.nifi.authentication.exception.ProviderCreationException; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.processor.AbstractProcessor; @@ -37,9 +31,12 @@ import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.ssl.SSLContextService; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultSaslConfig; +import javax.net.ssl.SSLContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; /** @@ -224,7 +221,7 @@ protected Connection createConnection(ProcessContext context) { final SSLContextService sslService = context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); // if the property to use cert authentication is set but the SSL service hasn't been configured, throw an exception. if (useCertAuthentication && sslService == null) { - throw new ProviderCreationException("This processor is configured to use cert authentication, " + + throw new IllegalStateException("This processor is configured to use cert authentication, " + "but the SSL Context Service hasn't been configured. You need to configure the SSL Context Service."); } final String rawClientAuth = context.getProperty(CLIENT_AUTH).getValue(); @@ -237,7 +234,7 @@ protected Connection createConnection(ProcessContext context) { try { clientAuth = SSLContextService.ClientAuth.valueOf(rawClientAuth); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", + throw new IllegalStateException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", rawClientAuth, StringUtils.join(SslContextFactory.ClientAuth.values(), ", "))); } } diff --git a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/test/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessorTest.java b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/test/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessorTest.java index bc4c32d83078..8cbb8a3d0674 100644 --- a/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/test/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessorTest.java +++ b/nifi-nar-bundles/nifi-amqp-bundle/nifi-amqp-processors/src/test/java/org/apache/nifi/amqp/processors/AbstractAMQPProcessorTest.java @@ -16,10 +16,7 @@ */ package org.apache.nifi.amqp.processors; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.apache.nifi.authentication.exception.ProviderCreationException; +import com.rabbitmq.client.Connection; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.exception.ProcessException; @@ -29,7 +26,8 @@ import org.junit.Before; import org.junit.Test; -import com.rabbitmq.client.Connection; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** @@ -46,7 +44,7 @@ public void setUp() throws Exception { testRunner = TestRunners.newTestRunner(processor); } - @Test(expected = ProviderCreationException.class) + @Test(expected = IllegalStateException.class) public void testConnectToCassandraWithSSLBadClientAuth() throws Exception { SSLContextService sslService = mock(SSLContextService.class); when(sslService.getIdentifier()).thenReturn("ssl-context"); @@ -63,7 +61,7 @@ public void testConnectToCassandraWithSSLBadClientAuth() throws Exception { processor.onTrigger(testRunner.getProcessContext(), testRunner.getProcessSessionFactory()); } - @Test(expected = ProviderCreationException.class) + @Test(expected = IllegalStateException.class) public void testInvalidSSLConfiguration() throws Exception { // it's invalid to have use_cert_auth enabled and not have the SSL Context Service configured testRunner.setProperty(AbstractAMQPProcessor.USE_CERT_AUTHENTICATION, "true"); diff --git a/nifi-nar-bundles/nifi-amqp-bundle/pom.xml b/nifi-nar-bundles/nifi-amqp-bundle/pom.xml index 54a4b00ff359..51c2d8289af5 100644 --- a/nifi-nar-bundles/nifi-amqp-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-amqp-bundle/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-amqp-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom A bundle of processors that publish to and consume messages from AMQP. @@ -33,7 +33,7 @@ org.apache.nifi nifi-amqp-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-nar/pom.xml b/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-nar/pom.xml index 971548edd37c..df7f532d05fb 100644 --- a/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-nar/pom.xml +++ b/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-atlas-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-atlas-nar @@ -37,7 +37,7 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-reporting-task/pom.xml b/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-reporting-task/pom.xml index 21bbff3ace60..39cb44d11b0f 100644 --- a/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-reporting-task/pom.xml +++ b/nifi-nar-bundles/nifi-atlas-bundle/nifi-atlas-reporting-task/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-atlas-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-atlas-reporting-task @@ -33,12 +33,12 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-reporting-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -48,6 +48,13 @@ org.apache.nifi nifi-kerberos-credentials-service-api + + + commons-beanutils + commons-beanutils + 1.9.3 + org.apache.atlas atlas-client @@ -64,6 +71,11 @@ it.unimi.dsi fastutil + + + commons-beanutils + commons-beanutils-core + @@ -106,13 +118,12 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test junit junit - 4.11 test diff --git a/nifi-nar-bundles/nifi-atlas-bundle/pom.xml b/nifi-nar-bundles/nifi-atlas-bundle/pom.xml index e4db5a651fe1..7efd162f51c3 100644 --- a/nifi-nar-bundles/nifi-atlas-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-atlas-bundle/pom.xml @@ -19,12 +19,12 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-atlas-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom 0.8.1 @@ -36,15 +36,21 @@ + + + io.netty + netty + 3.7.1.Final + org.apache.nifi nifi-atlas-reporting-task - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-client-dto - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.atlas diff --git a/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-nar/pom.xml b/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-nar/pom.xml index c86db577b409..c6067621a562 100644 --- a/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-nar/pom.xml +++ b/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-avro-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-avro-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,7 +34,7 @@ org.apache.nifi nifi-avro-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-processors/pom.xml b/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-processors/pom.xml index 4776b2151251..977c5ee6cdd3 100644 --- a/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-processors/pom.xml +++ b/nifi-nar-bundles/nifi-avro-bundle/nifi-avro-processors/pom.xml @@ -15,7 +15,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-avro-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-avro-processors @@ -29,7 +29,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.commons @@ -54,7 +54,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-avro-bundle/pom.xml b/nifi-nar-bundles/nifi-avro-bundle/pom.xml index 2fd48cd366d6..f50b226e6549 100644 --- a/nifi-nar-bundles/nifi-avro-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-avro-bundle/pom.xml @@ -19,12 +19,12 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-avro-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/pom.xml b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/pom.xml index cdd93250674c..2702a16d950c 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/pom.xml +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/pom.xml @@ -19,7 +19,7 @@ nifi-aws-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-aws-abstract-processors @@ -28,6 +28,16 @@ com.amazonaws aws-java-sdk-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + + com.amazonaws @@ -70,7 +80,7 @@ org.apache.nifi nifi-aws-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -79,12 +89,25 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-proxy-configuration-api + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.7 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + 2.9.7 + diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/src/main/java/org/apache/nifi/processors/aws/AbstractAWSProcessor.java b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/src/main/java/org/apache/nifi/processors/aws/AbstractAWSProcessor.java index 00f443f80765..06f4a165906e 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/src/main/java/org/apache/nifi/processors/aws/AbstractAWSProcessor.java +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-abstract-processors/src/main/java/org/apache/nifi/processors/aws/AbstractAWSProcessor.java @@ -37,6 +37,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import org.apache.commons.lang3.StringUtils; import org.apache.http.conn.ssl.DefaultHostnameVerifier; @@ -53,7 +55,6 @@ import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.processors.aws.credentials.provider.factory.CredentialPropertyDescriptors; -import org.apache.nifi.processors.aws.regions.AWSRegions; import org.apache.nifi.proxy.ProxyConfiguration; import org.apache.nifi.proxy.ProxySpec; import org.apache.nifi.ssl.SSLContextService; @@ -157,7 +158,7 @@ public abstract class AbstractAWSProcessor org.apache.nifi nifi-aws-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-aws-nar @@ -33,13 +33,13 @@ org.apache.nifi nifi-aws-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-aws-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml index 734be215546c..1da1cc6b75ff 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-aws-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-aws-processors @@ -32,28 +32,28 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-aws-abstract-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-aws-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-standard-web-test-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java index f5a69acb59bb..d3bade9faa3a 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/ListS3.java @@ -229,7 +229,8 @@ public void onTrigger(final ProcessContext context, final ProcessSession session final AmazonS3 client = getClient(); int listCount = 0; - long maxTimestamp = 0L; + int totalListCount = 0; + long latestListedTimestampInThisCycle = currentTimestamp; String delimiter = context.getProperty(DELIMITER).getValue(); String prefix = context.getProperty(PREFIX).evaluateAttributeExpressions().getValue(); @@ -251,6 +252,9 @@ public void onTrigger(final ProcessContext context, final ProcessSession session } VersionListing versionListing; + final Set listedKeys = new HashSet<>(); + getLogger().trace("Start listing, listingTimestamp={}, currentTimestamp={}, currentKeys={}", new Object[]{listingTimestamp, currentTimestamp, currentKeys}); + do { versionListing = bucketLister.listVersions(); for (S3VersionSummary versionSummary : versionListing.getVersionSummaries()) { @@ -261,6 +265,8 @@ public void onTrigger(final ProcessContext context, final ProcessSession session continue; } + getLogger().trace("Listed key={}, lastModified={}, currentKeys={}", new Object[]{versionSummary.getKey(), lastModified, currentKeys}); + // Create the attributes final Map attributes = new HashMap<>(); attributes.put(CoreAttributes.FILENAME.key(), versionSummary.getKey()); @@ -286,30 +292,40 @@ public void onTrigger(final ProcessContext context, final ProcessSession session flowFile = session.putAllAttributes(flowFile, attributes); session.transfer(flowFile, REL_SUCCESS); - // Update state - if (lastModified > maxTimestamp) { - maxTimestamp = lastModified; - currentKeys.clear(); - } - if (lastModified == maxTimestamp) { - currentKeys.add(versionSummary.getKey()); + // Track the latest lastModified timestamp and keys having that timestamp. + // NOTE: Amazon S3 lists objects in UTF-8 character encoding in lexicographical order. Not ordered by timestamps. + if (lastModified > latestListedTimestampInThisCycle) { + latestListedTimestampInThisCycle = lastModified; + listedKeys.clear(); + listedKeys.add(versionSummary.getKey()); + + } else if (lastModified == latestListedTimestampInThisCycle) { + listedKeys.add(versionSummary.getKey()); } + listCount++; } bucketLister.setNextMarker(); + totalListCount += listCount; commit(context, session, listCount); listCount = 0; } while (bucketLister.isTruncated()); - currentTimestamp = maxTimestamp; + + // Update currentKeys. + if (latestListedTimestampInThisCycle > currentTimestamp) { + currentKeys.clear(); + } + currentKeys.addAll(listedKeys); + + // Update stateManger with the most recent timestamp + currentTimestamp = latestListedTimestampInThisCycle; + persistState(context); final long listMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); getLogger().info("Successfully listed S3 bucket {} in {} millis", new Object[]{bucket, listMillis}); - if (!commit(context, session, listCount)) { - if (currentTimestamp > 0) { - persistState(context); - } + if (totalListCount == 0) { getLogger().debug("No new objects in S3 bucket {} to list. Yielding.", new Object[]{bucket}); context.yield(); } @@ -320,7 +336,6 @@ private boolean commit(final ProcessContext context, final ProcessSession sessio if (willCommit) { getLogger().info("Successfully listed {} new files from S3; routing to success", new Object[] {listCount}); session.commit(); - persistState(context); } return willCommit; } diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/TagS3Object.java b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/TagS3Object.java new file mode 100644 index 000000000000..6c9d72a143c6 --- /dev/null +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/s3/TagS3Object.java @@ -0,0 +1,201 @@ +/* + * 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.processors.aws.s3; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GetObjectTaggingRequest; +import com.amazonaws.services.s3.model.GetObjectTaggingResult; +import com.amazonaws.services.s3.model.ObjectTagging; +import com.amazonaws.services.s3.model.SetObjectTaggingRequest; +import com.amazonaws.services.s3.model.Tag; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.util.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + + +@SupportsBatching +@WritesAttributes({ + @WritesAttribute(attribute = "s3.tag.___", description = "The tags associated with the S3 object will be " + + "written as part of the FlowFile attributes")}) +@SeeAlso({PutS3Object.class, FetchS3Object.class, ListS3.class}) +@Tags({"Amazon", "S3", "AWS", "Archive", "Tag"}) +@InputRequirement(Requirement.INPUT_REQUIRED) +@CapabilityDescription("Sets tags on a FlowFile within an Amazon S3 Bucket. " + + "If attempting to tag a file that does not exist, FlowFile is routed to success.") +public class TagS3Object extends AbstractS3Processor { + + public static final PropertyDescriptor TAG_KEY = new PropertyDescriptor.Builder() + .name("tag-key") + .displayName("Tag Key") + .description("The key of the tag that will be set on the S3 Object") + .addValidator(new StandardValidators.StringLengthValidator(1, 127)) + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .required(true) + .build(); + + public static final PropertyDescriptor TAG_VALUE = new PropertyDescriptor.Builder() + .name("tag-value") + .displayName("Tag Value") + .description("The value of the tag that will be set on the S3 Object") + .addValidator(new StandardValidators.StringLengthValidator(1, 255)) + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .required(true) + .build(); + + public static final PropertyDescriptor APPEND_TAG = new PropertyDescriptor.Builder() + .name("append-tag") + .displayName("Append Tag") + .description("If set to true, the tag will be appended to the existing set of tags on the S3 object. " + + "Any existing tags with the same key as the new tag will be updated with the specified value. If " + + "set to false, the existing tags will be removed and the new tag will be set on the S3 object.") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .allowableValues("true", "false") + .expressionLanguageSupported(ExpressionLanguageScope.NONE) + .required(true) + .defaultValue("true") + .build(); + + public static final PropertyDescriptor VERSION_ID = new PropertyDescriptor.Builder() + .name("version") + .displayName("Version ID") + .description("The Version of the Object to tag") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .required(false) + .build(); + + public static final List properties = Collections.unmodifiableList( + Arrays.asList(KEY, BUCKET, VERSION_ID, TAG_KEY, TAG_VALUE, APPEND_TAG, ACCESS_KEY, SECRET_KEY, + CREDENTIALS_FILE, AWS_CREDENTIALS_PROVIDER_SERVICE, REGION, TIMEOUT, SSL_CONTEXT_SERVICE, + ENDPOINT_OVERRIDE, SIGNER_OVERRIDE, PROXY_CONFIGURATION_SERVICE, PROXY_HOST, PROXY_HOST_PORT, + PROXY_USERNAME, PROXY_PASSWORD)); + + @Override + protected List getSupportedPropertyDescriptors() { + return properties; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) { + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final long startNanos = System.nanoTime(); + + final String bucket = context.getProperty(BUCKET).evaluateAttributeExpressions(flowFile).getValue(); + final String key = context.getProperty(KEY).evaluateAttributeExpressions(flowFile).getValue(); + final String newTagKey = context.getProperty(TAG_KEY).evaluateAttributeExpressions(flowFile).getValue(); + final String newTagVal = context.getProperty(TAG_VALUE).evaluateAttributeExpressions(flowFile).getValue(); + + if(StringUtils.isBlank(bucket)){ + failFlowWithBlankEvaluatedProperty(session, flowFile, BUCKET); + return; + } + + if(StringUtils.isBlank(key)){ + failFlowWithBlankEvaluatedProperty(session, flowFile, KEY); + return; + } + + if(StringUtils.isBlank(newTagKey)){ + failFlowWithBlankEvaluatedProperty(session, flowFile, TAG_KEY); + return; + } + + if(StringUtils.isBlank(newTagVal)){ + failFlowWithBlankEvaluatedProperty(session, flowFile, TAG_VALUE); + return; + } + + final String version = context.getProperty(VERSION_ID).evaluateAttributeExpressions(flowFile).getValue(); + + final AmazonS3 s3 = getClient(); + + SetObjectTaggingRequest r; + List tags = new ArrayList<>(); + + try { + if(context.getProperty(APPEND_TAG).asBoolean()) { + final GetObjectTaggingRequest gr = new GetObjectTaggingRequest(bucket, key); + GetObjectTaggingResult res = s3.getObjectTagging(gr); + + // preserve tags on S3 object, but filter out existing tag keys that match the one we're setting + tags = res.getTagSet().stream().filter(t -> !t.getKey().equals(newTagKey)).collect(Collectors.toList()); + } + + tags.add(new Tag(newTagKey, newTagVal)); + + if(StringUtils.isBlank(version)){ + r = new SetObjectTaggingRequest(bucket, key, new ObjectTagging(tags)); + } else{ + r = new SetObjectTaggingRequest(bucket, key, version, new ObjectTagging(tags)); + } + s3.setObjectTagging(r); + } catch (final AmazonServiceException ase) { + getLogger().error("Failed to tag S3 Object for {}; routing to failure", new Object[]{flowFile, ase}); + flowFile = session.penalize(flowFile); + session.transfer(flowFile, REL_FAILURE); + return; + } + + flowFile = setTagAttributes(session, flowFile, tags); + + session.transfer(flowFile, REL_SUCCESS); + final long transferMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + getLogger().info("Successfully tagged S3 Object for {} in {} millis; routing to success", new Object[]{flowFile, transferMillis}); + } + + private void failFlowWithBlankEvaluatedProperty(ProcessSession session, FlowFile flowFile, PropertyDescriptor pd) { + getLogger().error("{} value is blank after attribute expression language evaluation", new Object[]{pd.getName()}); + flowFile = session.penalize(flowFile); + session.transfer(flowFile, REL_FAILURE); + } + + private FlowFile setTagAttributes(ProcessSession session, FlowFile flowFile, List tags) { + flowFile = session.removeAllAttributes(flowFile, Pattern.compile("^s3\\.tag\\..*")); + + final Map tagAttrs = new HashMap<>(); + tags.stream().forEach(t -> tagAttrs.put("s3.tag." + t.getKey(), t.getValue())); + flowFile = session.putAllAttributes(flowFile, tagAttrs); + return flowFile; + } +} diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor index c628fb108a7c..68dd72aef721 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor @@ -15,6 +15,7 @@ org.apache.nifi.processors.aws.s3.FetchS3Object org.apache.nifi.processors.aws.s3.PutS3Object org.apache.nifi.processors.aws.s3.DeleteS3Object +org.apache.nifi.processors.aws.s3.TagS3Object org.apache.nifi.processors.aws.s3.ListS3 org.apache.nifi.processors.aws.sns.PutSNS org.apache.nifi.processors.aws.sqs.GetSQS diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/ITTagS3Object.java b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/ITTagS3Object.java new file mode 100644 index 000000000000..b1b42c088952 --- /dev/null +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/ITTagS3Object.java @@ -0,0 +1,150 @@ +/* + * 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.processors.aws.s3; + +import com.amazonaws.services.s3.model.GetObjectTaggingRequest; +import com.amazonaws.services.s3.model.GetObjectTaggingResult; +import com.amazonaws.services.s3.model.Tag; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * Provides integration level testing with actual AWS S3 resources for {@link TagS3Object} and requires additional + * configuration and resources to work. + */ +public class ITTagS3Object extends AbstractS3IT { + + @Test + public void testSimpleTag() throws Exception { + String objectKey = "test-file"; + String tagKey = "nifi-key"; + String tagValue = "nifi-val"; + + // put file in s3 + putTestFile(objectKey, getFileFromResourceName(SAMPLE_FILE_RESOURCE_NAME)); + + // Set up processor + final TestRunner runner = TestRunners.newTestRunner(new TagS3Object()); + runner.setProperty(TagS3Object.CREDENTIALS_FILE, CREDENTIALS_FILE); + runner.setProperty(TagS3Object.REGION, REGION); + runner.setProperty(TagS3Object.BUCKET, BUCKET_NAME); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagValue); + + final Map attrs = new HashMap<>(); + attrs.put("filename", objectKey); + runner.enqueue(new byte[0], attrs); + + // tag file + runner.run(1); + + // Verify processor succeeds + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + + // Verify tag exists on S3 object + GetObjectTaggingResult res = client.getObjectTagging(new GetObjectTaggingRequest(BUCKET_NAME, objectKey)); + assertTrue("Expected tag not found on S3 object", res.getTagSet().contains(new Tag(tagKey, tagValue))); + } + + @Test + public void testAppendTag() throws Exception { + String objectKey = "test-file"; + String tagKey = "nifi-key"; + String tagValue = "nifi-val"; + + Tag existingTag = new Tag("oldkey", "oldvalue"); + + // put file in s3 + putFileWithObjectTag(objectKey, getFileFromResourceName(SAMPLE_FILE_RESOURCE_NAME), Arrays.asList(existingTag)); + + // Set up processor + final TestRunner runner = TestRunners.newTestRunner(new TagS3Object()); + runner.setProperty(TagS3Object.CREDENTIALS_FILE, CREDENTIALS_FILE); + runner.setProperty(TagS3Object.REGION, REGION); + runner.setProperty(TagS3Object.BUCKET, BUCKET_NAME); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagValue); + + final Map attrs = new HashMap<>(); + attrs.put("filename", objectKey); + runner.enqueue(new byte[0], attrs); + + // tag file + runner.run(1); + + // Verify processor succeeds + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + + // Verify new tag and existing exist on S3 object + GetObjectTaggingResult res = client.getObjectTagging(new GetObjectTaggingRequest(BUCKET_NAME, objectKey)); + assertTrue("Expected new tag not found on S3 object", res.getTagSet().contains(new Tag(tagKey, tagValue))); + assertTrue("Expected existing tag not found on S3 object", res.getTagSet().contains(existingTag)); + } + + @Test + public void testReplaceTags() throws Exception { + String objectKey = "test-file"; + String tagKey = "nifi-key"; + String tagValue = "nifi-val"; + + Tag existingTag = new Tag("s3.tag.oldkey", "oldvalue"); + + // put file in s3 + putFileWithObjectTag(objectKey, getFileFromResourceName(SAMPLE_FILE_RESOURCE_NAME), Arrays.asList(existingTag)); + + // Set up processor + final TestRunner runner = TestRunners.newTestRunner(new TagS3Object()); + runner.setProperty(TagS3Object.CREDENTIALS_FILE, CREDENTIALS_FILE); + runner.setProperty(TagS3Object.REGION, REGION); + runner.setProperty(TagS3Object.BUCKET, BUCKET_NAME); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagValue); + runner.setProperty(TagS3Object.APPEND_TAG, "false"); + + final Map attrs = new HashMap<>(); + attrs.put("filename", objectKey); + attrs.put("s3.tag."+existingTag.getKey(), existingTag.getValue()); + runner.enqueue(new byte[0], attrs); + + // tag file + runner.run(1); + + // Verify processor succeeds + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + + // Verify flowfile attributes match s3 tags + MockFlowFile flowFiles = runner.getFlowFilesForRelationship(TagS3Object.REL_SUCCESS).get(0); + flowFiles.assertAttributeNotExists(existingTag.getKey()); + flowFiles.assertAttributeEquals("s3.tag."+tagKey, tagValue); + + // Verify new tag exists on S3 object and prior tag removed + GetObjectTaggingResult res = client.getObjectTagging(new GetObjectTaggingRequest(BUCKET_NAME, objectKey)); + assertTrue("Expected new tag not found on S3 object", res.getTagSet().contains(new Tag(tagKey, tagValue))); + assertFalse("Existing tag not replaced on S3 object", res.getTagSet().contains(existingTag)); + } +} + diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestTagS3Object.java b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestTagS3Object.java new file mode 100644 index 000000000000..d337796aa881 --- /dev/null +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/s3/TestTagS3Object.java @@ -0,0 +1,319 @@ +/* + * 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.processors.aws.s3; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.GetObjectTaggingResult; +import com.amazonaws.services.s3.model.SetObjectTaggingRequest; +import com.amazonaws.services.s3.model.Tag; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.proxy.ProxyConfigurationService; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + + +public class TestTagS3Object { + + private TestRunner runner = null; + private TagS3Object mockTagS3Object = null; + private AmazonS3Client actualS3Client = null; + private AmazonS3Client mockS3Client = null; + + @Before + public void setUp() { + mockS3Client = Mockito.mock(AmazonS3Client.class); + mockTagS3Object = new TagS3Object() { + protected AmazonS3Client getClient() { + actualS3Client = client; + return mockS3Client; + } + }; + runner = TestRunners.newTestRunner(mockTagS3Object); + } + + @Test + public void testTagObjectSimple() throws IOException { + final String tagKey = "k"; + final String tagVal = "v"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + runner.setProperty(TagS3Object.APPEND_TAG, "false"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "object-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + Mockito.verify(mockS3Client, Mockito.times(1)).setObjectTagging(captureRequest.capture()); + SetObjectTaggingRequest request = captureRequest.getValue(); + assertEquals("test-bucket", request.getBucketName()); + assertEquals("object-key", request.getKey()); + assertNull("test-version", request.getVersionId()); + assertTrue("Expected tag not found in request", request.getTagging().getTagSet().contains(new Tag(tagKey, tagVal))); + + List flowFiles = runner.getFlowFilesForRelationship(ListS3.REL_SUCCESS); + MockFlowFile ff0 = flowFiles.get(0); + ff0.assertAttributeEquals("s3.tag."+tagKey, tagVal); + } + + @Test + public void testTagObjectVersion() throws IOException { + final String tagKey = "k"; + final String tagVal = "v"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.VERSION_ID, "test-version"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + runner.setProperty(TagS3Object.APPEND_TAG, "false"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "object-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + Mockito.verify(mockS3Client, Mockito.times(1)).setObjectTagging(captureRequest.capture()); + SetObjectTaggingRequest request = captureRequest.getValue(); + assertEquals("test-bucket", request.getBucketName()); + assertEquals("object-key", request.getKey()); + assertEquals("test-version", request.getVersionId()); + assertTrue("Expected tag not found in request", request.getTagging().getTagSet().contains(new Tag(tagKey, tagVal))); + } + + @Test + public void testTagObjectAppendToExistingTags() throws IOException { + //set up existing tags on S3 object + Tag currentTag = new Tag("ck", "cv"); + mockGetExistingTags(currentTag); + + final String tagKey = "nk"; + final String tagVal = "nv"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + final Map attrs = new HashMap<>(); + attrs.put("filename", "object-key"); + attrs.put("s3.tag."+currentTag.getKey(), currentTag.getValue()); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + Mockito.verify(mockS3Client, Mockito.times(1)).setObjectTagging(captureRequest.capture()); + SetObjectTaggingRequest request = captureRequest.getValue(); + assertEquals("test-bucket", request.getBucketName()); + assertEquals("object-key", request.getKey()); + assertTrue("New tag not found in request", request.getTagging().getTagSet().contains(new Tag(tagKey, tagVal))); + assertTrue("Existing tag not found in request", request.getTagging().getTagSet().contains(currentTag)); + + List flowFiles = runner.getFlowFilesForRelationship(ListS3.REL_SUCCESS); + MockFlowFile ff0 = flowFiles.get(0); + ff0.assertAttributeEquals("s3.tag."+tagKey, tagVal); + ff0.assertAttributeEquals("s3.tag."+currentTag.getKey(), currentTag.getValue()); + } + + @Test + public void testTagObjectAppendUpdatesExistingTagValue() throws IOException { + //set up existing tags on S3 object + Tag currentTag1 = new Tag("ck", "cv"); + Tag currentTag2 = new Tag("nk", "ov"); + mockGetExistingTags(currentTag1, currentTag2); + + final String tagKey = "nk"; + final String tagVal = "nv"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + final Map attrs = new HashMap<>(); + attrs.put("filename", "object-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + Mockito.verify(mockS3Client, Mockito.times(1)).setObjectTagging(captureRequest.capture()); + SetObjectTaggingRequest request = captureRequest.getValue(); + assertEquals("test-bucket", request.getBucketName()); + assertEquals("object-key", request.getKey()); + assertTrue("New tag not found in request", request.getTagging().getTagSet().contains(new Tag(tagKey, tagVal))); + assertTrue("Existing tag not found in request", request.getTagging().getTagSet().contains(currentTag1)); + assertFalse("Existing tag should be excluded from request", request.getTagging().getTagSet().contains(currentTag2)); + } + + @Test + public void testTagObjectReplacesExistingTags() throws IOException { + //set up existing tags on S3 object + Tag currentTag = new Tag("ck", "cv"); + mockGetExistingTags(currentTag); + + final String tagKey = "nk"; + final String tagVal = "nv"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + runner.setProperty(TagS3Object.APPEND_TAG, "false"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "object-key"); + attrs.put("s3.tag."+currentTag.getKey(), currentTag.getValue()); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(TagS3Object.REL_SUCCESS, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + Mockito.verify(mockS3Client, Mockito.times(1)).setObjectTagging(captureRequest.capture()); + SetObjectTaggingRequest request = captureRequest.getValue(); + assertEquals("test-bucket", request.getBucketName()); + assertEquals("object-key", request.getKey()); + assertTrue("New tag not found in request", request.getTagging().getTagSet().contains(new Tag(tagKey, tagVal))); + assertFalse("Existing tag should be excluded from request", request.getTagging().getTagSet().contains(currentTag)); + + List flowFiles = runner.getFlowFilesForRelationship(ListS3.REL_SUCCESS); + MockFlowFile ff0 = flowFiles.get(0); + ff0.assertAttributeEquals("s3.tag."+tagKey, tagVal); + ff0.assertAttributeNotExists("s3.tag."+currentTag.getKey()); + } + + @Test + public void testTagObjectS3Exception() { + //set up existing tags on S3 object + Tag currentTag = new Tag("ck", "cv"); + mockGetExistingTags(currentTag); + + final String tagKey = "nk"; + final String tagVal = "nv"; + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, tagKey); + runner.setProperty(TagS3Object.TAG_VALUE, tagVal); + final Map attrs = new HashMap<>(); + attrs.put("filename", "delete-key"); + runner.enqueue(new byte[0], attrs); + Mockito.doThrow(new AmazonS3Exception("TagFailure")).when(mockS3Client).setObjectTagging(Mockito.any()); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(DeleteS3Object.REL_FAILURE, 1); + ArgumentCaptor captureRequest = ArgumentCaptor.forClass(SetObjectTaggingRequest.class); + } + + @Test + public void testGetPropertyDescriptors() throws Exception { + TagS3Object processor = new TagS3Object(); + List pd = processor.getSupportedPropertyDescriptors(); + assertEquals("size should be eq", 20, pd.size()); + assertTrue(pd.contains(TagS3Object.ACCESS_KEY)); + assertTrue(pd.contains(TagS3Object.AWS_CREDENTIALS_PROVIDER_SERVICE)); + assertTrue(pd.contains(TagS3Object.BUCKET)); + assertTrue(pd.contains(TagS3Object.CREDENTIALS_FILE)); + assertTrue(pd.contains(TagS3Object.ENDPOINT_OVERRIDE)); + assertTrue(pd.contains(TagS3Object.KEY)); + assertTrue(pd.contains(TagS3Object.REGION)); + assertTrue(pd.contains(TagS3Object.SECRET_KEY)); + assertTrue(pd.contains(TagS3Object.SIGNER_OVERRIDE)); + assertTrue(pd.contains(TagS3Object.SSL_CONTEXT_SERVICE)); + assertTrue(pd.contains(TagS3Object.TIMEOUT)); + assertTrue(pd.contains(ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE)); + assertTrue(pd.contains(TagS3Object.PROXY_HOST)); + assertTrue(pd.contains(TagS3Object.PROXY_HOST_PORT)); + assertTrue(pd.contains(TagS3Object.PROXY_USERNAME)); + assertTrue(pd.contains(TagS3Object.PROXY_PASSWORD)); + assertTrue(pd.contains(TagS3Object.TAG_KEY)); + assertTrue(pd.contains(TagS3Object.TAG_VALUE)); + assertTrue(pd.contains(TagS3Object.APPEND_TAG)); + assertTrue(pd.contains(TagS3Object.VERSION_ID)); + } + + @Test + public void testBucketEvaluatedAsBlank() { + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "${not.existant.attribute}"); + runner.setProperty(TagS3Object.TAG_KEY, "key"); + runner.setProperty(TagS3Object.TAG_VALUE, "val"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "delete-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(DeleteS3Object.REL_FAILURE, 1); + } + + @Test + public void testTagKeyEvaluatedAsBlank() { + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, "${not.existant.attribute}"); + runner.setProperty(TagS3Object.TAG_VALUE, "val"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "delete-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(DeleteS3Object.REL_FAILURE, 1); + } + + @Test + public void testTagValEvaluatedAsBlank() { + runner.setProperty(TagS3Object.REGION, "us-west-2"); + runner.setProperty(TagS3Object.BUCKET, "test-bucket"); + runner.setProperty(TagS3Object.TAG_KEY, "tagKey"); + runner.setProperty(TagS3Object.TAG_VALUE, "${not.existant.attribute}"); + final Map attrs = new HashMap<>(); + attrs.put("filename", "delete-key"); + runner.enqueue(new byte[0], attrs); + + runner.run(1); + + runner.assertAllFlowFilesTransferred(DeleteS3Object.REL_FAILURE, 1); + } + + private void mockGetExistingTags(Tag... currentTag) { + List currentTags = new ArrayList<>(Arrays.asList(currentTag)); + Mockito.when(mockS3Client.getObjectTagging(Mockito.anyObject())).thenReturn(new GetObjectTaggingResult(currentTags)); + } +} diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api-nar/pom.xml b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api-nar/pom.xml index 0b9859bd34f6..59a9b45d1b86 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-aws-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-aws-service-api-nar @@ -33,13 +33,13 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-aws-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api/pom.xml b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api/pom.xml index 3de49a34b2b0..98e81b814a6a 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api/pom.xml +++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-service-api/pom.xml @@ -17,7 +17,7 @@ nifi-aws-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT 4.0.0 @@ -27,11 +27,34 @@ com.amazonaws aws-java-sdk-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + + org.apache.nifi nifi-api + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.7 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + 2.9.7 + diff --git a/nifi-nar-bundles/nifi-aws-bundle/pom.xml b/nifi-nar-bundles/nifi-aws-bundle/pom.xml index b3bb9437e48c..85d33d43eec5 100644 --- a/nifi-nar-bundles/nifi-aws-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-aws-bundle/pom.xml @@ -19,14 +19,14 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-aws-bundle pom - 1.11.412 + 1.11.461 diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-nar/pom.xml b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-nar/pom.xml index a31e166ea8b1..0df6d54b1839 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-nar/pom.xml +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-azure-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-azure-nar @@ -33,13 +33,13 @@ org.apache.nifi nifi-azure-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml index 02a0eb034f43..326962076aa1 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml @@ -15,7 +15,7 @@ org.apache.nifi nifi-azure-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-azure-processors jar @@ -30,7 +30,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -68,13 +68,13 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/DeleteAzureBlobStorage.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/DeleteAzureBlobStorage.java index a3f66d80c8f4..603bc697bd62 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/DeleteAzureBlobStorage.java +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/DeleteAzureBlobStorage.java @@ -21,28 +21,56 @@ import com.microsoft.azure.storage.blob.CloudBlob; import com.microsoft.azure.storage.blob.CloudBlobClient; import com.microsoft.azure.storage.blob.CloudBlobContainer; +import com.microsoft.azure.storage.blob.DeleteSnapshotsOption; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.SeeAlso; import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.processors.azure.AbstractAzureBlobProcessor; import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; - @Tags({ "azure", "microsoft", "cloud", "storage", "blob" }) @SeeAlso({ ListAzureBlobStorage.class, FetchAzureBlobStorage.class, PutAzureBlobStorage.class}) @CapabilityDescription("Deletes the provided blob from Azure Storage") @InputRequirement(Requirement.INPUT_REQUIRED) public class DeleteAzureBlobStorage extends AbstractAzureBlobProcessor { + private static final AllowableValue DELETE_SNAPSHOTS_NONE = new AllowableValue(DeleteSnapshotsOption.NONE.name(), "None", "Delete the blob only."); + + private static final AllowableValue DELETE_SNAPSHOTS_ALSO = new AllowableValue(DeleteSnapshotsOption.INCLUDE_SNAPSHOTS.name(), "Include Snapshots", "Delete the blob and its snapshots."); + + private static final AllowableValue DELETE_SNAPSHOTS_ONLY = new AllowableValue(DeleteSnapshotsOption.DELETE_SNAPSHOTS_ONLY.name(), "Delete Snapshots Only", "Delete only the blob's snapshots."); + + private static final PropertyDescriptor DELETE_SNAPSHOTS_OPTION = new PropertyDescriptor.Builder() + .name("delete-snapshots-option") + .displayName("Delete Snapshots Option") + .description("Specifies the snapshot deletion options to be used when deleting a blob.") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .allowableValues(DELETE_SNAPSHOTS_NONE, DELETE_SNAPSHOTS_ALSO, DELETE_SNAPSHOTS_ONLY) + .defaultValue(DELETE_SNAPSHOTS_NONE.getValue()) + .required(true) + .build(); + + @Override + public List getSupportedPropertyDescriptors() { + List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); + properties.add(DELETE_SNAPSHOTS_OPTION); + return properties; + } + @Override public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { FlowFile flowFile = session.get(); @@ -52,8 +80,9 @@ public void onTrigger(ProcessContext context, ProcessSession session) throws Pro } final long startNanos = System.nanoTime(); - String containerName = context.getProperty(AzureStorageUtils.CONTAINER).evaluateAttributeExpressions(flowFile).getValue(); - String blobPath = context.getProperty(BLOB).evaluateAttributeExpressions(flowFile).getValue(); + final String containerName = context.getProperty(AzureStorageUtils.CONTAINER).evaluateAttributeExpressions(flowFile).getValue(); + final String blobPath = context.getProperty(BLOB).evaluateAttributeExpressions(flowFile).getValue(); + final String deleteSnapshotOptions = context.getProperty(DELETE_SNAPSHOTS_OPTION).getValue(); try { CloudBlobClient blobClient = AzureStorageUtils.createCloudBlobClient(context, getLogger(), flowFile); @@ -62,12 +91,12 @@ public void onTrigger(ProcessContext context, ProcessSession session) throws Pro final OperationContext operationContext = new OperationContext(); AzureStorageUtils.setProxy(operationContext, context); - blob.deleteIfExists(null, null, null, operationContext); + blob.deleteIfExists(DeleteSnapshotsOption.valueOf(deleteSnapshotOptions), null, null, operationContext); session.transfer(flowFile, REL_SUCCESS); final long transferMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); session.getProvenanceReporter().send(flowFile, blob.getSnapshotQualifiedUri().toString(), transferMillis); - }catch ( StorageException | URISyntaxException e) { + } catch ( StorageException | URISyntaxException e) { getLogger().error("Failed to delete the specified blob {} from Azure Storage. Routing to failure", new Object[]{blobPath}, e); flowFile = session.penalize(flowFile); session.transfer(flowFile, REL_FAILURE); diff --git a/nifi-nar-bundles/nifi-azure-bundle/pom.xml b/nifi-nar-bundles/nifi-azure-bundle/pom.xml index f6237f8e7f59..26741598e74f 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-azure-bundle/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-azure-bundle diff --git a/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-nar/pom.xml b/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-nar/pom.xml index 82be3247bdef..286cd33e7df2 100644 --- a/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-nar/pom.xml +++ b/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-nar/pom.xml @@ -19,23 +19,23 @@ org.apache.nifi nifi-beats-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-beats-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-beats-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-processors/pom.xml b/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-processors/pom.xml index deeb43aedd7b..318267cfc50d 100644 --- a/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-processors/pom.xml +++ b/nifi-nar-bundles/nifi-beats-bundle/nifi-beats-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-beats-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-beats-processors @@ -33,7 +33,7 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.google.code.gson @@ -43,17 +43,17 @@ org.apache.nifi nifi-socket-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-flowfile-packager - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -63,7 +63,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-beats-bundle/pom.xml b/nifi-nar-bundles/nifi-beats-bundle/pom.xml index 4a5559597bef..392d9d2b3c36 100644 --- a/nifi-nar-bundles/nifi-beats-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-beats-bundle/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-beats-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-nar/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-nar/pom.xml index 53f998d2fca4..3735e2e5f09a 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-nar/pom.xml +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-cassandra-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cassandra-nar @@ -32,8 +32,8 @@ org.apache.nifi - nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + nifi-cassandra-services-api-nar + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/pom.xml index 3359b3dc81d9..84fb2e46335d 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/pom.xml +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-cassandra-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cassandra-processors @@ -33,7 +33,7 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -42,11 +42,12 @@ com.datastax.cassandra cassandra-driver-core - 3.3.0 + ${cassandra.sdk.version} org.apache.nifi nifi-ssl-context-service-api + provided org.apache.avro @@ -55,13 +56,13 @@ org.apache.nifi - nifi-mock - 1.8.0-SNAPSHOT - test + nifi-cassandra-services-api + provided org.apache.nifi - nifi-ssl-context-service + nifi-cassandra-services + 1.9.0-SNAPSHOT test @@ -77,7 +78,8 @@ org.apache.nifi nifi-mock-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT + test org.apache.commons diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java index ad05cbb552a8..9e39e3497afb 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessor.java @@ -25,10 +25,14 @@ import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.exceptions.AuthenticationException; +import com.datastax.driver.core.exceptions.NoHostAvailableException; import org.apache.avro.Schema; import org.apache.avro.SchemaBuilder; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.authentication.exception.ProviderCreationException; +import org.apache.nifi.cassandra.CassandraSessionProviderService; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.PropertyValue; import org.apache.nifi.components.ValidationContext; @@ -38,6 +42,7 @@ import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.ssl.SSLContextService; @@ -63,26 +68,36 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { public static final int DEFAULT_CASSANDRA_PORT = 9042; // Common descriptors - public static final PropertyDescriptor CONTACT_POINTS = new PropertyDescriptor.Builder() + static final PropertyDescriptor CONNECTION_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("cassandra-connection-provider") + .displayName("Cassandra Connection Provider") + .description("Specifies the Cassandra connection providing controller service to be used to connect to Cassandra cluster.") + .required(false) + .identifiesControllerService(CassandraSessionProviderService.class) + .build(); + + static final PropertyDescriptor CONTACT_POINTS = new PropertyDescriptor.Builder() .name("Cassandra Contact Points") .description("Contact points are addresses of Cassandra nodes. The list of contact points should be " + "comma-separated and in hostname:port format. Example node1:port,node2:port,...." + " The default client port for Cassandra is 9042, but the port(s) must be explicitly specified.") - .required(true) + .required(false) .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) .addValidator(StandardValidators.HOSTNAME_PORT_LIST_VALIDATOR) .build(); - public static final PropertyDescriptor KEYSPACE = new PropertyDescriptor.Builder() + static final PropertyDescriptor KEYSPACE = new PropertyDescriptor.Builder() .name("Keyspace") - .description("The Cassandra Keyspace to connect to. If not set, the keyspace name has to be provided with the " + + .description("The Cassandra Keyspace to connect to. If no keyspace is specified, the query will need to " + + "include the keyspace name before any table reference, in case of 'query' native processors or " + + "if the processor exposes the 'Table' property, the keyspace name has to be provided with the " + "table name in the form of .") .required(false) .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); - public static final PropertyDescriptor PROP_SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() + static final PropertyDescriptor PROP_SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() .name("SSL Context Service") .description("The SSL Context Service used to provide client certificate information for TLS/SSL " + "connections.") @@ -90,7 +105,7 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .identifiesControllerService(SSLContextService.class) .build(); - public static final PropertyDescriptor CLIENT_AUTH = new PropertyDescriptor.Builder() + static final PropertyDescriptor CLIENT_AUTH = new PropertyDescriptor.Builder() .name("Client Auth") .description("Client authentication policy when connecting to secure (TLS/SSL) cluster. " + "Possible values are REQUIRED, WANT, NONE. This property is only used when an SSL Context " @@ -100,7 +115,7 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .defaultValue("REQUIRED") .build(); - public static final PropertyDescriptor USERNAME = new PropertyDescriptor.Builder() + static final PropertyDescriptor USERNAME = new PropertyDescriptor.Builder() .name("Username") .description("Username to access the Cassandra cluster") .required(false) @@ -108,7 +123,7 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); - public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder() + static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder() .name("Password") .description("Password to access the Cassandra cluster") .required(false) @@ -117,15 +132,15 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); - public static final PropertyDescriptor CONSISTENCY_LEVEL = new PropertyDescriptor.Builder() + static final PropertyDescriptor CONSISTENCY_LEVEL = new PropertyDescriptor.Builder() .name("Consistency Level") .description("The strategy for how many replicas must respond before results are returned.") - .required(true) + .required(false) .allowableValues(ConsistencyLevel.values()) .defaultValue("ONE") .build(); - public static final PropertyDescriptor CHARSET = new PropertyDescriptor.Builder() + static final PropertyDescriptor CHARSET = new PropertyDescriptor.Builder() .name("Character Set") .description("Specifies the character set of the record data.") .required(true) @@ -134,30 +149,30 @@ public abstract class AbstractCassandraProcessor extends AbstractProcessor { .addValidator(StandardValidators.CHARACTER_SET_VALIDATOR) .build(); - // Relationships - public static final Relationship REL_SUCCESS = new Relationship.Builder() + static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("A FlowFile is transferred to this relationship if the operation completed successfully.") .build(); + static final Relationship REL_FAILURE = new Relationship.Builder() + .name("failure") + .description("A FlowFile is transferred to this relationship if the operation failed.") + .build(); + static final Relationship REL_ORIGINAL = new Relationship.Builder() .name("original") .description("All input FlowFiles that are part of a successful CQL operation execution go here.") .build(); - public static final Relationship REL_FAILURE = new Relationship.Builder() - .name("failure") - .description("CQL operation execution failed.") - .build(); - - public static final Relationship REL_RETRY = new Relationship.Builder().name("retry") + static final Relationship REL_RETRY = new Relationship.Builder().name("retry") .description("A FlowFile is transferred to this relationship if the operation cannot be completed but attempting " - + "the operation again may succeed.") + + "it again may succeed.") .build(); - static List descriptors = new ArrayList<>(); + protected static List descriptors = new ArrayList<>(); static { + descriptors.add(CONNECTION_PROVIDER_SERVICE); descriptors.add(CONTACT_POINTS); descriptors.add(KEYSPACE); descriptors.add(PROP_SSL_CONTEXT_SERVICE); @@ -180,15 +195,53 @@ protected Collection customValidate(ValidationContext validati // Ensure that if username or password is set, then the other is too String userName = validationContext.getProperty(USERNAME).evaluateAttributeExpressions().getValue(); String password = validationContext.getProperty(PASSWORD).evaluateAttributeExpressions().getValue(); + if (StringUtils.isEmpty(userName) != StringUtils.isEmpty(password)) { - results.add(new ValidationResult.Builder().valid(false).explanation( + results.add(new ValidationResult.Builder().subject("Username / Password configuration").valid(false).explanation( "If username or password is specified, then the other must be specified as well").build()); } + // Ensure that both Connection provider service and the processor specific configurations are not provided + boolean connectionProviderIsSet = validationContext.getProperty(CONNECTION_PROVIDER_SERVICE).isSet(); + boolean contactPointsIsSet = validationContext.getProperty(CONTACT_POINTS).isSet(); + + if (connectionProviderIsSet && contactPointsIsSet) { + results.add(new ValidationResult.Builder().subject("Cassandra configuration").valid(false).explanation("both " + CONNECTION_PROVIDER_SERVICE.getDisplayName() + + " and processor level Cassandra configuration cannot be provided at the same time.").build()); + } + + if (!connectionProviderIsSet && !contactPointsIsSet) { + results.add(new ValidationResult.Builder().subject("Cassandra configuration").valid(false).explanation("either " + CONNECTION_PROVIDER_SERVICE.getDisplayName() + + " or processor level Cassandra configuration has to be provided.").build()); + } + return results; } - protected void connectToCassandra(ProcessContext context) { + @OnScheduled + public void onScheduled(ProcessContext context) { + final boolean connectionProviderIsSet = context.getProperty(CONNECTION_PROVIDER_SERVICE).isSet(); + + if (connectionProviderIsSet) { + CassandraSessionProviderService sessionProvider = context.getProperty(CONNECTION_PROVIDER_SERVICE).asControllerService(CassandraSessionProviderService.class); + cluster.set(sessionProvider.getCluster()); + cassandraSession.set(sessionProvider.getCassandraSession()); + return; + } + + try { + connectToCassandra(context); + } catch (NoHostAvailableException nhae) { + getLogger().error("No host in the Cassandra cluster can be contacted successfully to execute this statement", nhae); + getLogger().error(nhae.getCustomMessage(10, true, false)); + throw new ProcessException(nhae); + } catch (AuthenticationException ae) { + getLogger().error("Invalid username/password combination", ae); + throw new ProcessException(ae); + } + } + + void connectToCassandra(ProcessContext context) { if (cluster.get() == null) { ComponentLog log = getLogger(); final String contactPointList = context.getProperty(CONTACT_POINTS).evaluateAttributeExpressions().getValue(); @@ -196,13 +249,13 @@ protected void connectToCassandra(ProcessContext context) { List contactPoints = getContactPoints(contactPointList); // Set up the client for secure (SSL/TLS communications) if configured to do so - final SSLContextService sslService = - context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); + final SSLContextService sslService = context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); final String rawClientAuth = context.getProperty(CLIENT_AUTH).getValue(); final SSLContext sslContext; if (sslService != null) { final SSLContextService.ClientAuth clientAuth; + if (StringUtils.isBlank(rawClientAuth)) { clientAuth = SSLContextService.ClientAuth.REQUIRED; } else { @@ -213,6 +266,7 @@ protected void connectToCassandra(ProcessContext context) { rawClientAuth, StringUtils.join(SslContextFactory.ClientAuth.values(), ", "))); } } + sslContext = sslService.createSSLContext(clientAuth); } else { sslContext = null; @@ -233,15 +287,19 @@ protected void connectToCassandra(ProcessContext context) { // Create the cluster and connect to it Cluster newCluster = createCluster(contactPoints, sslContext, username, password); PropertyValue keyspaceProperty = context.getProperty(KEYSPACE).evaluateAttributeExpressions(); + final Session newSession; if (keyspaceProperty != null) { newSession = newCluster.connect(keyspaceProperty.getValue()); } else { newSession = newCluster.connect(); } + newCluster.getConfiguration().getQueryOptions().setConsistencyLevel(ConsistencyLevel.valueOf(consistencyLevel)); Metadata metadata = newCluster.getMetadata(); + log.info("Connected to Cassandra cluster: {}", new Object[]{metadata.getClusterName()}); + cluster.set(newCluster); cassandraSession.set(newSession); } @@ -271,14 +329,20 @@ protected Cluster createCluster(List contactPoints, SSLContex return builder.build(); } - public void stop() { - if (cassandraSession.get() != null) { - cassandraSession.get().close(); - cassandraSession.set(null); - } - if (cluster.get() != null) { - cluster.get().close(); - cluster.set(null); + public void stop(ProcessContext context) { + // We don't want to close the connection when using 'Cassandra Connection Provider' + // because each time @OnUnscheduled/@OnShutdown annotated method is triggered on a + // processor, the connection would be closed which is not ideal for a centralized + // connection provider controller service + if (!context.getProperty(CONNECTION_PROVIDER_SERVICE).isSet()) { + if (cassandraSession.get() != null) { + cassandraSession.get().close(); + cassandraSession.set(null); + } + if (cluster.get() != null) { + cluster.get().close(); + cluster.set(null); + } } } @@ -362,7 +426,7 @@ protected static Object getCassandraObject(Row row, int i, DataType dataType) { * * @param dataType The data type of the field */ - public static Schema getUnionFieldType(String dataType) { + protected static Schema getUnionFieldType(String dataType) { return SchemaBuilder.builder().unionOf().nullBuilder().endNull().and().type(getSchemaForType(dataType)).endUnion(); } @@ -371,7 +435,7 @@ public static Schema getUnionFieldType(String dataType) { * * @param dataType The data type of the field */ - public static Schema getSchemaForType(String dataType) { + protected static Schema getSchemaForType(String dataType) { SchemaBuilder.TypeBuilder typeBuilder = SchemaBuilder.builder(); Schema returnSchema; switch (dataType) { @@ -402,7 +466,7 @@ public static Schema getSchemaForType(String dataType) { return returnSchema; } - public static String getPrimitiveAvroTypeFromCassandraType(DataType dataType) { + protected static String getPrimitiveAvroTypeFromCassandraType(DataType dataType) { // Map types from Cassandra to Avro where possible if (dataType.equals(DataType.ascii()) || dataType.equals(DataType.text()) @@ -440,7 +504,7 @@ public static String getPrimitiveAvroTypeFromCassandraType(DataType dataType) { } } - public static DataType getPrimitiveDataTypeFromString(String dataTypeName) { + protected static DataType getPrimitiveDataTypeFromString(String dataTypeName) { Set primitiveTypes = DataType.allPrimitiveTypes(); for (DataType primitiveType : primitiveTypes) { if (primitiveType.toString().equals(dataTypeName)) { @@ -456,7 +520,7 @@ public static DataType getPrimitiveDataTypeFromString(String dataTypeName) { * @param contactPointList A comma-separated list of Cassandra contact points (host:port,host2:port2, etc.) * @return List of InetSocketAddresses for the Cassandra contact points */ - public List getContactPoints(String contactPointList) { + protected List getContactPoints(String contactPointList) { if (contactPointList == null) { return null; diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraQL.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraQL.java index 1b14874e8621..5edc97359be4 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraQL.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraQL.java @@ -22,7 +22,6 @@ import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Session; import com.datastax.driver.core.TypeCodec; -import com.datastax.driver.core.exceptions.AuthenticationException; import com.datastax.driver.core.exceptions.InvalidTypeException; import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.core.exceptions.QueryExecutionException; @@ -163,7 +162,7 @@ public Set getRelationships() { @OnScheduled public void onScheduled(final ProcessContext context) { - ComponentLog log = getLogger(); + super.onScheduled(context); // Initialize the prepared statement cache int statementCacheSize = context.getProperty(STATEMENT_CACHE_SIZE).evaluateAttributeExpressions().asInteger(); @@ -171,22 +170,6 @@ public void onScheduled(final ProcessContext context) { .maximumSize(statementCacheSize) .build() .asMap(); - - try { - connectToCassandra(context); - - } catch (final NoHostAvailableException nhae) { - log.error("No host in the Cassandra cluster can be contacted successfully to execute this statement", nhae); - // Log up to 10 error messages. Otherwise if a 1000-node cluster was specified but there was no connectivity, - // a thousand error messages would be logged. However we would like information from Cassandra itself, so - // cap the error limit at 10, format the messages, and don't include the stack trace (it is displayed by the - // logger message above). - log.error(nhae.getCustomMessage(10, true, false)); - throw new ProcessException(nhae); - } catch (final AuthenticationException ae) { - log.error("Invalid username/password combination", ae); - throw new ProcessException(ae); - } } @Override @@ -417,8 +400,8 @@ protected void setStatementObject(final BoundStatement statement, final int para } @OnStopped - public void stop() { - super.stop(); + public void stop(ProcessContext context) { + super.stop(context); statementCache.clear(); } } diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraRecord.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraRecord.java index 402ec3dd014b..84016dcf8948 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraRecord.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/PutCassandraRecord.java @@ -19,14 +19,11 @@ import com.datastax.driver.core.BatchStatement; import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.Session; -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.core.querybuilder.Insert; import com.datastax.driver.core.querybuilder.QueryBuilder; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; -import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.annotation.lifecycle.OnShutdown; import org.apache.nifi.annotation.lifecycle.OnUnscheduled; import org.apache.nifi.components.PropertyDescriptor; @@ -105,8 +102,8 @@ public class PutCassandraRecord extends AbstractCassandraProcessor { .build(); private final static List propertyDescriptors = Collections.unmodifiableList(Arrays.asList( - CONTACT_POINTS, KEYSPACE, TABLE, CLIENT_AUTH, USERNAME, PASSWORD, RECORD_READER_FACTORY, - BATCH_SIZE, CONSISTENCY_LEVEL, BATCH_STATEMENT_TYPE, PROP_SSL_CONTEXT_SERVICE)); + CONNECTION_PROVIDER_SERVICE, CONTACT_POINTS, KEYSPACE, TABLE, CLIENT_AUTH, USERNAME, PASSWORD, + RECORD_READER_FACTORY, BATCH_SIZE, CONSISTENCY_LEVEL, BATCH_STATEMENT_TYPE, PROP_SSL_CONTEXT_SERVICE)); private final static Set relationships = Collections.unmodifiableSet( new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE))); @@ -121,20 +118,6 @@ public Set getRelationships() { return relationships; } - @OnScheduled - public void onScheduled(ProcessContext context) { - try { - connectToCassandra(context); - } catch (NoHostAvailableException nhae) { - getLogger().error("No host in the Cassandra cluster can be contacted successfully to execute this statement", nhae); - getLogger().error(nhae.getCustomMessage(10, true, false)); - throw new ProcessException(nhae); - } catch (AuthenticationException ae) { - getLogger().error("Invalid username/password combination", ae); - throw new ProcessException(ae); - } - } - @Override public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { FlowFile inputFlowFile = session.get(); @@ -210,13 +193,13 @@ public void onTrigger(ProcessContext context, ProcessSession session) throws Pro } @OnUnscheduled - public void stop() { - super.stop(); + public void stop(ProcessContext context) { + super.stop(context); } @OnShutdown - public void shutdown() { - super.stop(); + public void shutdown(ProcessContext context) { + super.stop(context); } } diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java index c57c53208697..16435418af5e 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/main/java/org/apache/nifi/processors/cassandra/QueryCassandra.java @@ -21,7 +21,6 @@ import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; -import com.datastax.driver.core.exceptions.AuthenticationException; import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.core.exceptions.QueryExecutionException; import com.datastax.driver.core.exceptions.QueryValidationException; @@ -193,27 +192,15 @@ public final List getSupportedPropertyDescriptors() { @OnScheduled public void onScheduled(final ProcessContext context) { - ComponentLog log = getLogger(); - try { - connectToCassandra(context); - final int fetchSize = context.getProperty(FETCH_SIZE).evaluateAttributeExpressions().asInteger(); - if (fetchSize > 0) { - synchronized (cluster.get()) { - cluster.get().getConfiguration().getQueryOptions().setFetchSize(fetchSize); - } + super.onScheduled(context); + + final int fetchSize = context.getProperty(FETCH_SIZE).evaluateAttributeExpressions().asInteger(); + if (fetchSize > 0) { + synchronized (cluster.get()) { + cluster.get().getConfiguration().getQueryOptions().setFetchSize(fetchSize); } - } catch (final NoHostAvailableException nhae) { - log.error("No host in the Cassandra cluster can be contacted successfully to execute this query", nhae); - // Log up to 10 error messages. Otherwise if a 1000-node cluster was specified but there was no connectivity, - // a thousand error messages would be logged. However we would like information from Cassandra itself, so - // cap the error limit at 10, format the messages, and don't include the stack trace (it is displayed by the - // logger message above). - log.error(nhae.getCustomMessage(10, true, false)); - throw new ProcessException(nhae); - } catch (final AuthenticationException ae) { - log.error("Invalid username/password combination", ae); - throw new ProcessException(ae); } + } @Override @@ -221,8 +208,6 @@ public void onTrigger(final ProcessContext context, final ProcessSession session FlowFile inputFlowFile = null; FlowFile fileToProcess = null; - Map attributes = null; - if (context.hasIncomingConnection()) { inputFlowFile = session.get(); @@ -232,8 +217,6 @@ public void onTrigger(final ProcessContext context, final ProcessSession session if (inputFlowFile == null && context.hasNonLoopConnection()) { return; } - - attributes = inputFlowFile.getAttributes(); } final ComponentLog logger = getLogger(); @@ -245,9 +228,7 @@ public void onTrigger(final ProcessContext context, final ProcessSession session final Charset charset = Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(inputFlowFile).getValue()); final StopWatch stopWatch = new StopWatch(true); - /*if(inputFlowFile != null){ - session.transfer(inputFlowFile, REL_ORIGINAL); - }*/ + try { // The documentation for the driver recommends the session remain open the entire time the processor is running @@ -400,13 +381,13 @@ public void process(final OutputStream out) throws IOException { @OnUnscheduled - public void stop() { - super.stop(); + public void stop(ProcessContext context) { + super.stop(context); } @OnShutdown - public void shutdown() { - super.stop(); + public void shutdown(ProcessContext context) { + super.stop(context); } /** diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessorTest.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessorTest.java index aeb8f593958f..a2c64c7278f1 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessorTest.java +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-processors/src/test/java/org/apache/nifi/processors/cassandra/AbstractCassandraProcessorTest.java @@ -22,11 +22,15 @@ import com.datastax.driver.core.Metadata; import com.datastax.driver.core.Row; import com.google.common.collect.Sets; +import org.apache.nifi.annotation.lifecycle.OnEnabled; import org.apache.nifi.authentication.exception.ProviderCreationException; import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.controller.ConfigurationContext; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.service.CassandraSessionProvider; import org.apache.nifi.ssl.SSLContextService; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; @@ -50,7 +54,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; - /** * Unit tests for the AbstractCassandraProcessor class */ @@ -204,7 +207,7 @@ public void testConnectToCassandra() throws Exception { processor.setCluster(cluster); testRunner.setProperty(AbstractCassandraProcessor.CONSISTENCY_LEVEL, "ONE"); processor.connectToCassandra(testRunner.getProcessContext()); - processor.stop(); + processor.stop(testRunner.getProcessContext()); assertNull(processor.getCluster()); // Now do a connect where a cluster is "built" @@ -257,6 +260,33 @@ public void testConnectToCassandraUsernamePassword() throws Exception { assertNotNull(processor.getCluster()); } + @Test + public void testCustomValidateCassandraConnectionConfiguration() throws InitializationException { + MockCassandraSessionProvider sessionProviderService = new MockCassandraSessionProvider(); + + testRunner.addControllerService("cassandra-connection-provider", sessionProviderService); + testRunner.setProperty(sessionProviderService, CassandraSessionProvider.CONTACT_POINTS, "localhost:9042"); + testRunner.setProperty(sessionProviderService, CassandraSessionProvider.KEYSPACE, "somekyespace"); + + testRunner.setProperty(AbstractCassandraProcessor.CONNECTION_PROVIDER_SERVICE, "cassandra-connection-provider"); + testRunner.setProperty(AbstractCassandraProcessor.CONTACT_POINTS, "localhost:9042"); + testRunner.setProperty(AbstractCassandraProcessor.KEYSPACE, "some-keyspace"); + testRunner.setProperty(AbstractCassandraProcessor.CONSISTENCY_LEVEL, "ONE"); + testRunner.setProperty(AbstractCassandraProcessor.USERNAME, "user"); + testRunner.setProperty(AbstractCassandraProcessor.PASSWORD, "password"); + testRunner.enableControllerService(sessionProviderService); + + testRunner.assertNotValid(); + + testRunner.removeProperty(AbstractCassandraProcessor.CONTACT_POINTS); + testRunner.removeProperty(AbstractCassandraProcessor.KEYSPACE); + testRunner.removeProperty(AbstractCassandraProcessor.CONSISTENCY_LEVEL); + testRunner.removeProperty(AbstractCassandraProcessor.USERNAME); + testRunner.removeProperty(AbstractCassandraProcessor.PASSWORD); + + testRunner.assertValid(); + } + /** * Provides a stubbed processor instance for testing */ @@ -264,7 +294,7 @@ public static class MockAbstractCassandraProcessor extends AbstractCassandraProc @Override protected List getSupportedPropertyDescriptors() { - return Arrays.asList(CONTACT_POINTS, KEYSPACE, USERNAME, PASSWORD, CONSISTENCY_LEVEL, CHARSET); + return Arrays.asList(CONNECTION_PROVIDER_SERVICE, CONTACT_POINTS, KEYSPACE, USERNAME, PASSWORD, CONSISTENCY_LEVEL, CHARSET); } @Override @@ -292,4 +322,16 @@ public void setCluster(Cluster newCluster) { this.cluster.set(newCluster); } } + + /** + * Mock CassandraSessionProvider implementation for testing purpose + */ + private class MockCassandraSessionProvider extends CassandraSessionProvider { + + @OnEnabled + public void onEnabled(final ConfigurationContext context) { + + } + + } } \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/pom.xml new file mode 100644 index 000000000000..4ef4f7bd629e --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/pom.xml @@ -0,0 +1,45 @@ + + + + + nifi-cassandra-bundle + org.apache.nifi + 1.9.0-SNAPSHOT + + 4.0.0 + + nifi-cassandra-services-api-nar + 1.9.0-SNAPSHOT + nar + + + + org.apache.nifi + nifi-standard-services-api-nar + 1.9.0-SNAPSHOT + nar + + + org.apache.nifi + nifi-cassandra-services-api + 1.9.0-SNAPSHOT + compile + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-service-api-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/LICENSE similarity index 78% rename from nifi-nar-bundles/nifi-redis-bundle/nifi-redis-service-api-nar/src/main/resources/META-INF/LICENSE rename to nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/LICENSE index 958de4d6bcf7..106720e0f851 100644 --- a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-service-api-nar/src/main/resources/META-INF/LICENSE +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/LICENSE @@ -1,239 +1,266 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed 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. - -APACHE NIFI SUBCOMPONENTS: - -The Apache NiFi project contains subcomponents with separate copyright -notices and license terms. Your use of the source code for the these -subcomponents is subject to the terms and conditions of the following -licenses. - - The binary distribution of this product bundles 'ParaNamer' and 'Paranamer Core' - which is available under a BSD style license. - - Copyright (c) 2006 Paul Hammant & ThoughtWorks Inc - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + +This product bundles 'asm' which is available under a 3-Clause BSD style license. +For details see http://asm.ow2.org/asmdex-license.html + + Copyright (c) 2012 France Télécom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'JNR x86asm' under an MIT +style license. + + Copyright (C) 2010 Wayne Meissner + Copyright (c) 2008-2009, Petr Kobalicek + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000000..3706faf951f2 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,226 @@ +nifi-cassandra-services-api-nar +Copyright 2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + + (ASLv2) The Netty Project + The following NOTICE information applies: + Copyright 2014 The Netty Project + ------------------------------------------------------------------------------- + This product contains the extensions to Java Collections Framework which has + been derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene: + + * LICENSE: + * license/LICENSE.jsr166y.txt (Public Domain) + * HOMEPAGE: + * http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/ + * http://viewvc.jboss.org/cgi-bin/viewvc.cgi/jbosscache/experimental/jsr166/ + + This product contains a modified version of Robert Harder's Public Domain + Base64 Encoder and Decoder, which can be obtained at: + + * LICENSE: + * license/LICENSE.base64.txt (Public Domain) + * HOMEPAGE: + * http://iharder.sourceforge.net/current/java/base64/ + + This product contains a modified portion of 'Webbit', an event based + WebSocket and HTTP server, which can be obtained at: + + * LICENSE: + * license/LICENSE.webbit.txt (BSD License) + * HOMEPAGE: + * https://github.com/joewalnes/webbit + + This product contains a modified portion of 'SLF4J', a simple logging + facade for Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.slf4j.txt (MIT License) + * HOMEPAGE: + * http://www.slf4j.org/ + + This product contains a modified portion of 'Apache Harmony', an open source + Java SE, which can be obtained at: + + * LICENSE: + * license/LICENSE.harmony.txt (Apache License 2.0) + * HOMEPAGE: + * http://archive.apache.org/dist/harmony/ + + This product contains a modified portion of 'jbzip2', a Java bzip2 compression + and decompression library written by Matthew J. Francis. It can be obtained at: + + * LICENSE: + * license/LICENSE.jbzip2.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jbzip2/ + + This product contains a modified portion of 'libdivsufsort', a C API library to construct + the suffix array and the Burrows-Wheeler transformed string for any input string of + a constant-size alphabet written by Yuta Mori. It can be obtained at: + + * LICENSE: + * license/LICENSE.libdivsufsort.txt (MIT License) + * HOMEPAGE: + * https://github.com/y-256/libdivsufsort + + This product contains a modified portion of Nitsan Wakart's 'JCTools', Java Concurrency Tools for the JVM, + which can be obtained at: + + * LICENSE: + * license/LICENSE.jctools.txt (ASL2 License) + * HOMEPAGE: + * https://github.com/JCTools/JCTools + + This product optionally depends on 'JZlib', a re-implementation of zlib in + pure Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.jzlib.txt (BSD style License) + * HOMEPAGE: + * http://www.jcraft.com/jzlib/ + + This product optionally depends on 'Compress-LZF', a Java library for encoding and + decoding data in LZF format, written by Tatu Saloranta. It can be obtained at: + + * LICENSE: + * license/LICENSE.compress-lzf.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/ning/compress + + This product optionally depends on 'lz4', a LZ4 Java compression + and decompression library written by Adrien Grand. It can be obtained at: + + * LICENSE: + * license/LICENSE.lz4.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jpountz/lz4-java + + This product optionally depends on 'lzma-java', a LZMA Java compression + and decompression library, which can be obtained at: + + * LICENSE: + * license/LICENSE.lzma-java.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jponge/lzma-java + + This product contains a modified portion of 'jfastlz', a Java port of FastLZ compression + and decompression library written by William Kinney. It can be obtained at: + + * LICENSE: + * license/LICENSE.jfastlz.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jfastlz/ + + This product contains a modified portion of and optionally depends on 'Protocol Buffers', Google's data + interchange format, which can be obtained at: + + * LICENSE: + * license/LICENSE.protobuf.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/protobuf + + This product optionally depends on 'Bouncy Castle Crypto APIs' to generate + a temporary self-signed X.509 certificate when the JVM does not provide the + equivalent functionality. It can be obtained at: + + * LICENSE: + * license/LICENSE.bouncycastle.txt (MIT License) + * HOMEPAGE: + * http://www.bouncycastle.org/ + + This product optionally depends on 'Snappy', a compression library produced + by Google Inc, which can be obtained at: + + * LICENSE: + * license/LICENSE.snappy.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/snappy + + This product optionally depends on 'JBoss Marshalling', an alternative Java + serialization API, which can be obtained at: + + * LICENSE: + * license/LICENSE.jboss-marshalling.txt (GNU LGPL 2.1) + * HOMEPAGE: + * http://www.jboss.org/jbossmarshalling + + This product optionally depends on 'Caliper', Google's micro- + benchmarking framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.caliper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/google/caliper + + This product optionally depends on 'Apache Commons Logging', a logging + framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-logging.txt (Apache License 2.0) + * HOMEPAGE: + * http://commons.apache.org/logging/ + + This product optionally depends on 'Apache Log4J', a logging framework, which + can be obtained at: + + * LICENSE: + * license/LICENSE.log4j.txt (Apache License 2.0) + * HOMEPAGE: + * http://logging.apache.org/log4j/ + + This product optionally depends on 'Aalto XML', an ultra-high performance + non-blocking XML processor, which can be obtained at: + + * LICENSE: + * license/LICENSE.aalto-xml.txt (Apache License 2.0) + * HOMEPAGE: + * http://wiki.fasterxml.com/AaltoHome + + This product contains a modified version of 'HPACK', a Java implementation of + the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at: + + * LICENSE: + * license/LICENSE.hpack.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/twitter/hpack + + This product contains a modified portion of 'Apache Commons Lang', a Java library + provides utilities for the java.lang API, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-lang.txt (Apache License 2.0) + * HOMEPAGE: + * https://commons.apache.org/proper/commons-lang/ + + This product contains a forked and modified version of Tomcat Native + + * LICENSE: + * ASL2 + * HOMEPAGE: + * http://tomcat.apache.org/native-doc/ + * https://svn.apache.org/repos/asf/tomcat/native/ + + (ASLv2) Guava + The following NOTICE information applies: + Guava + Copyright 2015 The Guava Authors + + (ASLv2) Dropwizard Metrics + The following NOTICE information applies: + Copyright (c) 2010-2013 Coda Hale, Yammer.com + + ************************ + Eclipse Public License 1.0 + ************************ + + The following binary components are provided under the Eclipse Public License 1.0. See project link for details. + + (EPL 2.0)(GPL 2)(LGPL 2.1) JNR Posix ( jnr.posix ) https://github.com/jnr/jnr-posix/blob/master/LICENSE.txt + diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/pom.xml new file mode 100644 index 000000000000..bb8b59f8b7db --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/pom.xml @@ -0,0 +1,43 @@ + + + + + nifi-cassandra-bundle + org.apache.nifi + 1.9.0-SNAPSHOT + + 4.0.0 + + nifi-cassandra-services-api + jar + + + + org.apache.nifi + nifi-api + provided + + + + com.datastax.cassandra + cassandra-driver-core + ${cassandra.sdk.version} + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/src/main/java/org/apache/nifi/cassandra/CassandraSessionProviderService.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/src/main/java/org/apache/nifi/cassandra/CassandraSessionProviderService.java new file mode 100644 index 000000000000..fc2d22204af8 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-api/src/main/java/org/apache/nifi/cassandra/CassandraSessionProviderService.java @@ -0,0 +1,35 @@ +/* + * 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.cassandra; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import org.apache.nifi.controller.ControllerService; + +public interface CassandraSessionProviderService extends ControllerService { + /** + * Obtains a Cassandra session instance + * @return {@link Session} + */ + Session getCassandraSession(); + + /** + * Obtains a Cassandra cluster instance + * @return {@link Cluster} + */ + Cluster getCluster(); +} diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/pom.xml new file mode 100644 index 000000000000..925dc927d552 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/pom.xml @@ -0,0 +1,43 @@ + + + + + nifi-cassandra-bundle + org.apache.nifi + 1.9.0-SNAPSHOT + + 4.0.0 + + nifi-cassandra-services-nar + nar + + + + org.apache.nifi + nifi-cassandra-services-api-nar + 1.9.0-SNAPSHOT + nar + + + org.apache.nifi + nifi-cassandra-services + 1.9.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/LICENSE new file mode 100644 index 000000000000..5a38c1c6714f --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/LICENSE @@ -0,0 +1,369 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + +This product bundles 'libffi' which is available under an MIT style license. + libffi - Copyright (c) 1996-2014 Anthony Green, Red Hat, Inc and others. + see https://github.com/java-native-access/jna/blob/master/native/libffi/LICENSE + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + ``Software''), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'asm' which is available under a 3-Clause BSD style license. +For details see http://asm.ow2.org/asmdex-license.html + + Copyright (c) 2012 France Télécom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'Bouncy Castle JDK 1.5' + under an MIT style license. + + Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +The binary distribution of this product bundles 'JNR x86asm' under an MIT +style license. + + Copyright (C) 2010 Wayne Meissner + Copyright (c) 2008-2009, Petr Kobalicek + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'jBCrypt' which is available under an MIT license. +For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +This product bundles 'logback' which is dual-licensed under the EPL v1.0 +and the LGPL 2.1. + + Logback: the reliable, generic, fast and flexible logging framework. + + Copyright (C) 1999-2017, QOS.ch. All rights reserved. + + This program and the accompanying materials are dual-licensed under + either the terms of the Eclipse Public License v1.0 as published by + the Eclipse Foundation or (per the licensee's choosing) under the + terms of the GNU Lesser General Public License version 2.1 as + published by the Free Software Foundation. + +The binary distribution of this product bundles 'ANTLR 3' which is available +under a "3-clause BSD" license. For details see http://www.antlr.org/license.html + + Copyright (c) 2012 Terence Parr and Sam Harwell + All rights reserved. + Redistribution and use in source and binary forms, with or without modification, are permitted + provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + + Neither the name of the author nor the names of its contributors may be used to endorse + or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000000..08538a724305 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,291 @@ +nifi-cassandra-services-nar +Copyright 2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) DataStax Java Driver for Apache Cassandra - Core + The following NOTICE information applies: + DataStax Java Driver for Apache Cassandra - Core + Copyright (C) 2012-2017 DataStax Inc. + + (ASLv2) Jackson JSON processor + The following NOTICE information applies: + # Jackson JSON processor + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (http://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. + + (ASLv2) Apache Commons Codec + The following NOTICE information applies: + Apache Commons Codec + Copyright 2002-2014 The Apache Software Foundation + + src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java + contains test data from http://aspell.net/test/orig/batch0.tab. + Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org) + + =============================================================================== + + The content of package org.apache.commons.codec.language.bm has been translated + from the original php source code available at http://stevemorse.org/phoneticinfo.htm + with permission from the original authors. + Original source copyright: + Copyright (c) 2008 Alexander Beider & Stephen P. Morse. + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2017 The Apache Software Foundation + + This product includes software from the Spring Framework, + under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + + (ASLv2) Guava + The following NOTICE information applies: + Guava + Copyright 2015 The Guava Authors + + (ASLv2) JSON-SMART + The following NOTICE information applies: + Copyright 2011 JSON-SMART authors + + (ASLv2) Dropwizard Metrics + The following NOTICE information applies: + Copyright (c) 2010-2013 Coda Hale, Yammer.com + + (ASLv2) The Netty Project + The following NOTICE information applies: + Copyright 2014 The Netty Project + ------------------------------------------------------------------------------- + This product contains the extensions to Java Collections Framework which has + been derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene: + + * LICENSE: + * license/LICENSE.jsr166y.txt (Public Domain) + * HOMEPAGE: + * http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/ + * http://viewvc.jboss.org/cgi-bin/viewvc.cgi/jbosscache/experimental/jsr166/ + + This product contains a modified version of Robert Harder's Public Domain + Base64 Encoder and Decoder, which can be obtained at: + + * LICENSE: + * license/LICENSE.base64.txt (Public Domain) + * HOMEPAGE: + * http://iharder.sourceforge.net/current/java/base64/ + + This product contains a modified portion of 'Webbit', an event based + WebSocket and HTTP server, which can be obtained at: + + * LICENSE: + * license/LICENSE.webbit.txt (BSD License) + * HOMEPAGE: + * https://github.com/joewalnes/webbit + + This product contains a modified portion of 'SLF4J', a simple logging + facade for Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.slf4j.txt (MIT License) + * HOMEPAGE: + * http://www.slf4j.org/ + + This product contains a modified portion of 'Apache Harmony', an open source + Java SE, which can be obtained at: + + * LICENSE: + * license/LICENSE.harmony.txt (Apache License 2.0) + * HOMEPAGE: + * http://archive.apache.org/dist/harmony/ + + This product contains a modified portion of 'jbzip2', a Java bzip2 compression + and decompression library written by Matthew J. Francis. It can be obtained at: + + * LICENSE: + * license/LICENSE.jbzip2.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jbzip2/ + + This product contains a modified portion of 'libdivsufsort', a C API library to construct + the suffix array and the Burrows-Wheeler transformed string for any input string of + a constant-size alphabet written by Yuta Mori. It can be obtained at: + + * LICENSE: + * license/LICENSE.libdivsufsort.txt (MIT License) + * HOMEPAGE: + * https://github.com/y-256/libdivsufsort + + This product contains a modified portion of Nitsan Wakart's 'JCTools', Java Concurrency Tools for the JVM, + which can be obtained at: + + * LICENSE: + * license/LICENSE.jctools.txt (ASL2 License) + * HOMEPAGE: + * https://github.com/JCTools/JCTools + + This product optionally depends on 'JZlib', a re-implementation of zlib in + pure Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.jzlib.txt (BSD style License) + * HOMEPAGE: + * http://www.jcraft.com/jzlib/ + + This product optionally depends on 'Compress-LZF', a Java library for encoding and + decoding data in LZF format, written by Tatu Saloranta. It can be obtained at: + + * LICENSE: + * license/LICENSE.compress-lzf.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/ning/compress + + This product optionally depends on 'lz4', a LZ4 Java compression + and decompression library written by Adrien Grand. It can be obtained at: + + * LICENSE: + * license/LICENSE.lz4.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jpountz/lz4-java + + This product optionally depends on 'lzma-java', a LZMA Java compression + and decompression library, which can be obtained at: + + * LICENSE: + * license/LICENSE.lzma-java.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jponge/lzma-java + + This product contains a modified portion of 'jfastlz', a Java port of FastLZ compression + and decompression library written by William Kinney. It can be obtained at: + + * LICENSE: + * license/LICENSE.jfastlz.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jfastlz/ + + This product contains a modified portion of and optionally depends on 'Protocol Buffers', Google's data + interchange format, which can be obtained at: + + * LICENSE: + * license/LICENSE.protobuf.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/protobuf + + This product optionally depends on 'Bouncy Castle Crypto APIs' to generate + a temporary self-signed X.509 certificate when the JVM does not provide the + equivalent functionality. It can be obtained at: + + * LICENSE: + * license/LICENSE.bouncycastle.txt (MIT License) + * HOMEPAGE: + * http://www.bouncycastle.org/ + + This product optionally depends on 'Snappy', a compression library produced + by Google Inc, which can be obtained at: + + * LICENSE: + * license/LICENSE.snappy.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/snappy + + This product optionally depends on 'JBoss Marshalling', an alternative Java + serialization API, which can be obtained at: + + * LICENSE: + * license/LICENSE.jboss-marshalling.txt (GNU LGPL 2.1) + * HOMEPAGE: + * http://www.jboss.org/jbossmarshalling + + This product optionally depends on 'Caliper', Google's micro- + benchmarking framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.caliper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/google/caliper + + This product optionally depends on 'Apache Commons Logging', a logging + framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-logging.txt (Apache License 2.0) + * HOMEPAGE: + * http://commons.apache.org/logging/ + + This product optionally depends on 'Apache Log4J', a logging framework, which + can be obtained at: + + * LICENSE: + * license/LICENSE.log4j.txt (Apache License 2.0) + * HOMEPAGE: + * http://logging.apache.org/log4j/ + + This product optionally depends on 'Aalto XML', an ultra-high performance + non-blocking XML processor, which can be obtained at: + + * LICENSE: + * license/LICENSE.aalto-xml.txt (Apache License 2.0) + * HOMEPAGE: + * http://wiki.fasterxml.com/AaltoHome + + This product contains a modified version of 'HPACK', a Java implementation of + the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at: + + * LICENSE: + * license/LICENSE.hpack.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/twitter/hpack + + This product contains a modified portion of 'Apache Commons Lang', a Java library + provides utilities for the java.lang API, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-lang.txt (Apache License 2.0) + * HOMEPAGE: + * https://commons.apache.org/proper/commons-lang/ + + This product contains a forked and modified version of Tomcat Native + + * LICENSE: + * ASL2 + * HOMEPAGE: + * http://tomcat.apache.org/native-doc/ + * https://svn.apache.org/repos/asf/tomcat/native/ + + (ASLv2) Objenesis + The following NOTICE information applies: + Objenesis + Copyright 2006-2013 Joe Walnes, Henri Tremblay, Leonardo Mesquita + +************************ +Eclipse Public License 1.0 +************************ + +The following binary components are provided under the Eclipse Public License 1.0. See project link for details. + + (EPL 2.0)(GPL 2)(LGPL 2.1) JNR Posix ( jnr.posix ) https://github.com/jnr/jnr-posix/blob/master/LICENSE.txt + (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.2.3 - http://logback.qos.ch/) + (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.2.3 - http://logback.qos.ch/) diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/pom.xml new file mode 100644 index 000000000000..1af9ebfa9a78 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/pom.xml @@ -0,0 +1,82 @@ + + + + + nifi-cassandra-bundle + org.apache.nifi + 1.9.0-SNAPSHOT + + 4.0.0 + + nifi-cassandra-services + jar + + + + + org.apache.nifi + nifi-api + 1.9.0-SNAPSHOT + + + + org.apache.nifi + nifi-utils + 1.9.0-SNAPSHOT + + + + org.apache.nifi + nifi-cassandra-services-api + provided + + + + com.datastax.cassandra + cassandra-driver-core + ${cassandra.sdk.version} + + + + org.apache.nifi + nifi-ssl-context-service-api + 1.9.0-SNAPSHOT + + + + org.apache.nifi + nifi-framework-api + 1.9.0-SNAPSHOT + + + + org.apache.nifi + nifi-security-utils + 1.9.0-SNAPSHOT + + + + org.apache.nifi + nifi-mock + 1.9.0-SNAPSHOT + + + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/java/org/apache/nifi/service/CassandraSessionProvider.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/java/org/apache/nifi/service/CassandraSessionProvider.java new file mode 100644 index 000000000000..3a266770a3e2 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/java/org/apache/nifi/service/CassandraSessionProvider.java @@ -0,0 +1,285 @@ +/* + * 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.service; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.JdkSSLOptions; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.Session; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.annotation.lifecycle.OnStopped; +import org.apache.nifi.authentication.exception.ProviderCreationException; +import org.apache.nifi.cassandra.CassandraSessionProviderService; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.PropertyValue; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.controller.ControllerServiceInitializationContext; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.security.util.SslContextFactory; +import org.apache.nifi.ssl.SSLContextService; + +import javax.net.ssl.SSLContext; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Tags({"cassandra", "dbcp", "database", "connection", "pooling"}) +@CapabilityDescription("Provides connection session for Cassandra processors to work with Apache Cassandra.") +public class CassandraSessionProvider extends AbstractControllerService implements CassandraSessionProviderService { + + public static final int DEFAULT_CASSANDRA_PORT = 9042; + + // Common descriptors + public static final PropertyDescriptor CONTACT_POINTS = new PropertyDescriptor.Builder() + .name("Cassandra Contact Points") + .description("Contact points are addresses of Cassandra nodes. The list of contact points should be " + + "comma-separated and in hostname:port format. Example node1:port,node2:port,...." + + " The default client port for Cassandra is 9042, but the port(s) must be explicitly specified.") + .required(true) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(StandardValidators.HOSTNAME_PORT_LIST_VALIDATOR) + .build(); + + public static final PropertyDescriptor KEYSPACE = new PropertyDescriptor.Builder() + .name("Keyspace") + .description("The Cassandra Keyspace to connect to. If no keyspace is specified, the query will need to " + + "include the keyspace name before any table reference, in case of 'query' native processors or " + + "if the processor supports the 'Table' property, the keyspace name has to be provided with the " + + "table name in the form of .
") + .required(false) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor PROP_SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() + .name("SSL Context Service") + .description("The SSL Context Service used to provide client certificate information for TLS/SSL " + + "connections.") + .required(false) + .identifiesControllerService(SSLContextService.class) + .build(); + + public static final PropertyDescriptor CLIENT_AUTH = new PropertyDescriptor.Builder() + .name("Client Auth") + .description("Client authentication policy when connecting to secure (TLS/SSL) cluster. " + + "Possible values are REQUIRED, WANT, NONE. This property is only used when an SSL Context " + + "has been defined and enabled.") + .required(false) + .allowableValues(SSLContextService.ClientAuth.values()) + .defaultValue("REQUIRED") + .build(); + + public static final PropertyDescriptor USERNAME = new PropertyDescriptor.Builder() + .name("Username") + .description("Username to access the Cassandra cluster") + .required(false) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder() + .name("Password") + .description("Password to access the Cassandra cluster") + .required(false) + .sensitive(true) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor CONSISTENCY_LEVEL = new PropertyDescriptor.Builder() + .name("Consistency Level") + .description("The strategy for how many replicas must respond before results are returned.") + .required(true) + .allowableValues(ConsistencyLevel.values()) + .defaultValue("ONE") + .build(); + + private List properties; + private Cluster cluster; + private Session cassandraSession; + + @Override + public void init(final ControllerServiceInitializationContext context) { + List props = new ArrayList<>(); + + props.add(CONTACT_POINTS); + props.add(CLIENT_AUTH); + props.add(CONSISTENCY_LEVEL); + props.add(KEYSPACE); + props.add(USERNAME); + props.add(PASSWORD); + props.add(PROP_SSL_CONTEXT_SERVICE); + + properties = props; + } + + @Override + public List getSupportedPropertyDescriptors() { + return properties; + } + + @OnEnabled + public void onEnabled(final ConfigurationContext context) { + connectToCassandra(context); + } + + @OnDisabled + public void onDisabled(){ + if (cassandraSession != null) { + cassandraSession.close(); + } + if (cluster != null) { + cluster.close(); + } + } + + @OnStopped + public void onStopped() { + if (cassandraSession != null) { + cassandraSession.close(); + } + if (cluster != null) { + cluster.close(); + } + } + + @Override + public Cluster getCluster() { + if (cluster != null) { + return cluster; + } else { + throw new ProcessException("Unable to get the Cassandra cluster detail."); + } + } + + @Override + public Session getCassandraSession() { + if (cassandraSession != null) { + return cassandraSession; + } else { + throw new ProcessException("Unable to get the Cassandra session."); + } + } + + private void connectToCassandra(ConfigurationContext context) { + if (cluster == null) { + ComponentLog log = getLogger(); + final String contactPointList = context.getProperty(CONTACT_POINTS).evaluateAttributeExpressions().getValue(); + final String consistencyLevel = context.getProperty(CONSISTENCY_LEVEL).getValue(); + List contactPoints = getContactPoints(contactPointList); + + // Set up the client for secure (SSL/TLS communications) if configured to do so + final SSLContextService sslService = + context.getProperty(PROP_SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); + final String rawClientAuth = context.getProperty(CLIENT_AUTH).getValue(); + final SSLContext sslContext; + + if (sslService != null) { + final SSLContextService.ClientAuth clientAuth; + if (StringUtils.isBlank(rawClientAuth)) { + clientAuth = SSLContextService.ClientAuth.REQUIRED; + } else { + try { + clientAuth = SSLContextService.ClientAuth.valueOf(rawClientAuth); + } catch (final IllegalArgumentException iae) { + throw new ProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", + rawClientAuth, StringUtils.join(SslContextFactory.ClientAuth.values(), ", "))); + } + } + sslContext = sslService.createSSLContext(clientAuth); + } else { + sslContext = null; + } + + final String username, password; + PropertyValue usernameProperty = context.getProperty(USERNAME).evaluateAttributeExpressions(); + PropertyValue passwordProperty = context.getProperty(PASSWORD).evaluateAttributeExpressions(); + + if (usernameProperty != null && passwordProperty != null) { + username = usernameProperty.getValue(); + password = passwordProperty.getValue(); + } else { + username = null; + password = null; + } + + // Create the cluster and connect to it + Cluster newCluster = createCluster(contactPoints, sslContext, username, password); + PropertyValue keyspaceProperty = context.getProperty(KEYSPACE).evaluateAttributeExpressions(); + final Session newSession; + if (keyspaceProperty != null) { + newSession = newCluster.connect(keyspaceProperty.getValue()); + } else { + newSession = newCluster.connect(); + } + newCluster.getConfiguration().getQueryOptions().setConsistencyLevel(ConsistencyLevel.valueOf(consistencyLevel)); + Metadata metadata = newCluster.getMetadata(); + log.info("Connected to Cassandra cluster: {}", new Object[]{metadata.getClusterName()}); + + cluster = newCluster; + cassandraSession = newSession; + } + } + + private List getContactPoints(String contactPointList) { + + if (contactPointList == null) { + return null; + } + + final List contactPointStringList = Arrays.asList(contactPointList.split(",")); + List contactPoints = new ArrayList<>(); + + for (String contactPointEntry : contactPointStringList) { + String[] addresses = contactPointEntry.split(":"); + final String hostName = addresses[0].trim(); + final int port = (addresses.length > 1) ? Integer.parseInt(addresses[1].trim()) : DEFAULT_CASSANDRA_PORT; + + contactPoints.add(new InetSocketAddress(hostName, port)); + } + + return contactPoints; + } + + private Cluster createCluster(List contactPoints, SSLContext sslContext, + String username, String password) { + Cluster.Builder builder = Cluster.builder().addContactPointsWithPorts(contactPoints); + + if (sslContext != null) { + JdkSSLOptions sslOptions = JdkSSLOptions.builder() + .withSSLContext(sslContext) + .build(); + builder = builder.withSSL(sslOptions); + } + + if (username != null && password != null) { + builder = builder.withCredentials(username, password); + } + + return builder.build(); + } +} diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService new file mode 100644 index 000000000000..045f90625fc1 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService @@ -0,0 +1,16 @@ +# 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. + +org.apache.nifi.service.CassandraSessionProvider \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/MockCassandraProcessor.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/MockCassandraProcessor.java new file mode 100644 index 000000000000..4891fe5a07a2 --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/MockCassandraProcessor.java @@ -0,0 +1,51 @@ +/* + * 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.service; + +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; + +import java.util.Collections; +import java.util.List; + +/** + * Mock Cassandra processor for testing CassandraSessionProvider + */ +public class MockCassandraProcessor extends AbstractProcessor{ + private static PropertyDescriptor CASSANDRA_SESSION_PROVIDER = new PropertyDescriptor.Builder() + .name("cassandra-session-provider") + .displayName("Cassandra Session Provider") + .required(true) + .description("Controller Service to obtain a Cassandra connection session") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .identifiesControllerService(CassandraSessionProvider.class) + .build(); + + @Override + public List getSupportedPropertyDescriptors() { + return Collections.singletonList(CASSANDRA_SESSION_PROVIDER); + } + + @Override + public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { + + } +} diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/TestCassandraSessionProvider.java b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/TestCassandraSessionProvider.java new file mode 100644 index 000000000000..2b688bc3368c --- /dev/null +++ b/nifi-nar-bundles/nifi-cassandra-bundle/nifi-cassandra-services/src/test/java/org/apache/nifi/service/TestCassandraSessionProvider.java @@ -0,0 +1,59 @@ +/* + * 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.service; + +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestCassandraSessionProvider { + + private static TestRunner runner; + private static CassandraSessionProvider sessionProvider; + + @BeforeClass + public static void setup() throws InitializationException { + MockCassandraProcessor mockCassandraProcessor = new MockCassandraProcessor(); + sessionProvider = new CassandraSessionProvider(); + + runner = TestRunners.newTestRunner(mockCassandraProcessor); + runner.addControllerService("cassandra-session-provider", sessionProvider); + } + + @Test + public void testGetPropertyDescriptors() { + List properties = sessionProvider.getPropertyDescriptors(); + + assertEquals(7, properties.size()); + assertTrue(properties.contains(CassandraSessionProvider.CLIENT_AUTH)); + assertTrue(properties.contains(CassandraSessionProvider.CONSISTENCY_LEVEL)); + assertTrue(properties.contains(CassandraSessionProvider.CONTACT_POINTS)); + assertTrue(properties.contains(CassandraSessionProvider.KEYSPACE)); + assertTrue(properties.contains(CassandraSessionProvider.PASSWORD)); + assertTrue(properties.contains(CassandraSessionProvider.PROP_SSL_CONTEXT_SERVICE)); + assertTrue(properties.contains(CassandraSessionProvider.USERNAME)); + } + +} diff --git a/nifi-nar-bundles/nifi-cassandra-bundle/pom.xml b/nifi-nar-bundles/nifi-cassandra-bundle/pom.xml index 7b7105583417..393a77a59fad 100644 --- a/nifi-nar-bundles/nifi-cassandra-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-cassandra-bundle/pom.xml @@ -19,15 +19,23 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT + + 3.3.0 + + nifi-cassandra-bundle pom nifi-cassandra-processors nifi-cassandra-nar + nifi-cassandra-services-api + nifi-cassandra-services-api-nar + nifi-cassandra-services + nifi-cassandra-services-nar @@ -35,7 +43,7 @@ org.apache.nifi nifi-cassandra-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-nar/pom.xml b/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-nar/pom.xml index b10d40ea4b71..fff9f592f682 100644 --- a/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-nar/pom.xml +++ b/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-ccda-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-ccda-nar diff --git a/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-processors/pom.xml b/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-processors/pom.xml index 7d7dc821ec35..5a6a2d2452c0 100644 --- a/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-processors/pom.xml +++ b/nifi-nar-bundles/nifi-ccda-bundle/nifi-ccda-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-ccda-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-ccda-processors @@ -33,7 +33,7 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.commons @@ -63,7 +63,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-ccda-bundle/pom.xml b/nifi-nar-bundles/nifi-ccda-bundle/pom.xml index 630c1f824874..234965e5f08b 100644 --- a/nifi-nar-bundles/nifi-ccda-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-ccda-bundle/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-ccda-bundle @@ -35,7 +35,7 @@ org.apache.nifi nifi-ccda-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-api/pom.xml b/nifi-nar-bundles/nifi-cdc/nifi-cdc-api/pom.xml index 4abe273a531f..2982dfaef441 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-api/pom.xml +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-api/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-cdc - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cdc-api diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-nar/pom.xml b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-nar/pom.xml index cae6193e48d6..c130421ef41e 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-nar/pom.xml +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-nar/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-cdc-mysql-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cdc-mysql-nar nar @@ -30,7 +30,7 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/pom.xml b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/pom.xml index f446686ed50c..a63fe3f6d861 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/pom.xml +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-cdc-mysql-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cdc-mysql-processors jar @@ -27,12 +27,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-cdc-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -46,7 +46,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test @@ -62,7 +62,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-ssl-context-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java index f58ed7eddb32..e8c94d1a3b1a 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java @@ -377,7 +377,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor { private final Serializer cacheValueSerializer = new TableInfo.Serializer(); private final Deserializer cacheValueDeserializer = new TableInfo.Deserializer(); - private Connection jdbcConnection = null; + private JDBCConnectionHolder jdbcConnectionHolder = null; private final BeginTransactionEventWriter beginEventWriter = new BeginTransactionEventWriter(); private final CommitTransactionEventWriter commitEventWriter = new CommitTransactionEventWriter(); @@ -710,9 +710,11 @@ protected void connect(List hosts, String username, String pa } if (createEnrichmentConnection) { + jdbcConnectionHolder = new JDBCConnectionHolder(connectedHost, username, password, null, connectTimeout); try { - jdbcConnection = getJdbcConnection(driverLocation, driverName, connectedHost, username, password, null); - } catch (InitializationException | SQLException e) { + // Ensure connection can be created. + getJdbcConnection(); + } catch (SQLException e) { binlogClient.disconnect(); binlogClient = null; throw new IOException("Error creating binlog enrichment JDBC connection to any of the specified hosts", e); @@ -945,6 +947,10 @@ protected void stop(StateManager stateManager) throws CDCException { throw new CDCException("Error closing CDC connection", e); } finally { binlogClient = null; + + if (jdbcConnectionHolder != null) { + jdbcConnectionHolder.close(); + } } } @@ -998,8 +1004,9 @@ BinaryLogClient createBinlogClient(String hostname, int port, String username, S */ protected TableInfo loadTableInfo(TableInfoCacheKey key) throws SQLException { TableInfo tableInfo = null; - if (jdbcConnection != null) { - try (Statement s = jdbcConnection.createStatement()) { + if (jdbcConnectionHolder != null) { + + try (Statement s = getJdbcConnection().createStatement()) { s.execute("USE `" + key.getDatabaseName() + "`"); ResultSet rs = s.executeQuery("SELECT * FROM `" + key.getTableName() + "` LIMIT 0"); ResultSetMetaData rsmd = rs.getMetaData(); @@ -1018,23 +1025,59 @@ protected TableInfo loadTableInfo(TableInfoCacheKey key) throws SQLException { return tableInfo; } + protected Connection getJdbcConnection() throws SQLException { + return jdbcConnectionHolder.getConnection(); + } + + private class JDBCConnectionHolder { + private String connectionUrl; + private Properties connectionProps = new Properties(); + private long connectionTimeoutMillis; + + private Connection connection; + + private JDBCConnectionHolder(InetSocketAddress host, String username, String password, Map customProperties, long connectionTimeoutMillis) { + this.connectionUrl = "jdbc:mysql://" + host.getHostString() + ":" + host.getPort(); + if (customProperties != null) { + connectionProps.putAll(customProperties); + } + connectionProps.put("user", username); + connectionProps.put("password", password); + this.connectionTimeoutMillis = connectionTimeoutMillis; + } + + private Connection getConnection() throws SQLException { + if (connection != null && connection.isValid((int) (connectionTimeoutMillis / 1000))) { + getLogger().trace("Returning the pooled JDBC connection."); + return connection; + } + + // Close the existing connection just in case. + close(); + + getLogger().trace("Creating a new JDBC connection."); + connection = DriverManager.getConnection(connectionUrl, connectionProps); + return connection; + } + + private void close() { + if (connection != null) { + try { + getLogger().trace("Closing the pooled JDBC connection."); + connection.close(); + } catch (SQLException e) { + getLogger().warn("Failed to close JDBC connection due to " + e, e); + } + } + } + } + + /** * using Thread.currentThread().getContextClassLoader(); will ensure that you are using the ClassLoader for you NAR. * * @throws InitializationException if there is a problem obtaining the ClassLoader */ - protected Connection getJdbcConnection(String locationString, String drvName, InetSocketAddress host, String username, String password, Map customProperties) - throws InitializationException, SQLException { - Properties connectionProps = new Properties(); - if (customProperties != null) { - connectionProps.putAll(customProperties); - } - connectionProps.put("user", username); - connectionProps.put("password", password); - - return DriverManager.getConnection("jdbc:mysql://" + host.getHostString() + ":" + host.getPort(), connectionProps); - } - protected void registerDriver(String locationString, String drvName) throws InitializationException { if (locationString != null && locationString.length() > 0) { try { diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy index 7e8607da63f1..5b07850c15eb 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy @@ -967,8 +967,7 @@ class CaptureChangeMySQLTest { } @Override - protected Connection getJdbcConnection(String locationString, String drvName, InetSocketAddress host, String username, String password, Map customProperties) - throws InitializationException, SQLException { + protected Connection getJdbcConnection() throws SQLException { Connection mockConnection = mock(Connection) Statement mockStatement = mock(Statement) when(mockConnection.createStatement()).thenReturn(mockStatement) diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/pom.xml b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/pom.xml index 776a7e38410e..42e72727337e 100644 --- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-cdc - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cdc-mysql-bundle pom @@ -32,7 +32,7 @@ org.apache.nifi nifi-cdc-mysql-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-cdc/pom.xml b/nifi-nar-bundles/nifi-cdc/pom.xml index 463baf192b8e..c9bf866860cd 100644 --- a/nifi-nar-bundles/nifi-cdc/pom.xml +++ b/nifi-nar-bundles/nifi-cdc/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cdc pom diff --git a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-platform-nar/pom.xml b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-platform-nar/pom.xml index ebd9d31edbea..656d5023bc9d 100644 --- a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-platform-nar/pom.xml +++ b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-platform-nar/pom.xml @@ -14,7 +14,7 @@ org.apache.nifi nifi-confluent-platform-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-confluent-platform-nar nar @@ -22,13 +22,13 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-confluent-schema-registry-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/pom.xml b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/pom.xml index bcec3d414c28..9dd375f0f5dc 100644 --- a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/pom.xml +++ b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/pom.xml @@ -15,7 +15,7 @@ org.apache.nifi nifi-confluent-platform-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-confluent-schema-registry-service @@ -35,7 +35,7 @@ org.apache.nifi nifi-avro-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -59,7 +59,7 @@ org.apache.nifi nifi-web-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/src/main/java/org/apache/nifi/confluent/schemaregistry/client/CachingSchemaRegistryClient.java b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/src/main/java/org/apache/nifi/confluent/schemaregistry/client/CachingSchemaRegistryClient.java index d82befe047b3..9075ac2eee21 100644 --- a/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/src/main/java/org/apache/nifi/confluent/schemaregistry/client/CachingSchemaRegistryClient.java +++ b/nifi-nar-bundles/nifi-confluent-platform-bundle/nifi-confluent-schema-registry-service/src/main/java/org/apache/nifi/confluent/schemaregistry/client/CachingSchemaRegistryClient.java @@ -17,113 +17,43 @@ package org.apache.nifi.confluent.schemaregistry.client; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import org.apache.nifi.schema.access.SchemaNotFoundException; import org.apache.nifi.serialization.record.RecordSchema; +import java.io.IOException; +import java.time.Duration; + public class CachingSchemaRegistryClient implements SchemaRegistryClient { private final SchemaRegistryClient client; - private final long expirationNanos; - private final Map nameCache; - private final Map idCache; + private final LoadingCache nameCache; + private final LoadingCache idCache; public CachingSchemaRegistryClient(final SchemaRegistryClient toWrap, final int cacheSize, final long expirationNanos) { this.client = toWrap; - this.expirationNanos = expirationNanos; - nameCache = new Cache<>(cacheSize); - idCache = new Cache<>(cacheSize); + nameCache = Caffeine.newBuilder() + .maximumSize(cacheSize) + .expireAfterWrite(Duration.ofNanos(expirationNanos)) + .build(client::getSchema); + idCache = Caffeine.newBuilder() + .maximumSize(cacheSize) + .expireAfterWrite(Duration.ofNanos(expirationNanos)) + .build(client::getSchema); } @Override public RecordSchema getSchema(final String schemaName) throws IOException, SchemaNotFoundException { - RecordSchema schema = getFromCache(nameCache, schemaName); - if (schema != null) { - return schema; - } - - schema = client.getSchema(schemaName); - - synchronized (nameCache) { - nameCache.put(schemaName, new CachedRecordSchema(schema)); - } - - return schema; + return nameCache.get(schemaName); } @Override public RecordSchema getSchema(final int schemaId) throws IOException, SchemaNotFoundException { - RecordSchema schema = getFromCache(idCache, schemaId); - if (schema != null) { - return schema; - } - - schema = client.getSchema(schemaId); - - synchronized (idCache) { - idCache.put(schemaId, new CachedRecordSchema(schema)); - } - - return schema; - } - - private RecordSchema getFromCache(final Map cache, final Object key) { - final CachedRecordSchema cachedSchema; - synchronized (cache) { - cachedSchema = cache.get(key); - } - - if (cachedSchema == null) { - return null; - } - - if (cachedSchema.isOlderThan(System.nanoTime() - expirationNanos)) { - return null; - } - - return cachedSchema.getSchema(); - } - - - private static class Cache extends LinkedHashMap { - private final int cacheSize; - - public Cache(final int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - protected boolean removeEldestEntry(final Map.Entry eldest) { - return size() >= cacheSize; - } + return idCache.get(schemaId); } - - private static class CachedRecordSchema { - private final RecordSchema schema; - private final long cachedTimestamp; - - public CachedRecordSchema(final RecordSchema schema) { - this(schema, System.nanoTime()); - } - - public CachedRecordSchema(final RecordSchema schema, final long timestamp) { - this.schema = schema; - this.cachedTimestamp = timestamp; - } - - public RecordSchema getSchema() { - return schema; - } - - public boolean isOlderThan(final long timestamp) { - return cachedTimestamp < timestamp; - } - } } diff --git a/nifi-nar-bundles/nifi-confluent-platform-bundle/pom.xml b/nifi-nar-bundles/nifi-confluent-platform-bundle/pom.xml index 7d6016ab2244..7c558b13298a 100644 --- a/nifi-nar-bundles/nifi-confluent-platform-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-confluent-platform-bundle/pom.xml @@ -14,7 +14,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-confluent-platform-bundle pom diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-nar/pom.xml b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-nar/pom.xml index 6b5245100bcd..1d13ef1cc211 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-nar/pom.xml +++ b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-couchbase-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-couchbase-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,13 +34,13 @@ org.apache.nifi nifi-couchbase-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-couchbase-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/pom.xml b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/pom.xml index 17706941f3aa..91515aaa8db9 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/pom.xml +++ b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-couchbase-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-couchbase-processors @@ -33,12 +33,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-couchbase-services-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided @@ -65,7 +65,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/src/test/java/org/apache/nifi/processors/couchbase/TestPutCouchbaseKey.java b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/src/test/java/org/apache/nifi/processors/couchbase/TestPutCouchbaseKey.java index ce9baa71bd7c..c32fff792ae5 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/src/test/java/org/apache/nifi/processors/couchbase/TestPutCouchbaseKey.java +++ b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-processors/src/test/java/org/apache/nifi/processors/couchbase/TestPutCouchbaseKey.java @@ -268,7 +268,6 @@ public void testInputFlowFileUuid() throws Exception { ArgumentCaptor capture = ArgumentCaptor.forClass(RawJsonDocument.class); verify(bucket, times(1)).upsert(capture.capture(), eq(PersistTo.NONE), eq(ReplicateTo.NONE)); - assertEquals(uuid, capture.getValue().id()); assertEquals(inFileData, capture.getValue().content()); testRunner.assertTransferCount(REL_SUCCESS, 1); diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api-nar/pom.xml index 93f142f5d889..9cb6ee3c589d 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-couchbase-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-couchbase-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,13 +34,13 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-couchbase-services-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api/pom.xml b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api/pom.xml index dc7105dbaf59..e341348e2b8a 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api/pom.xml +++ b/nifi-nar-bundles/nifi-couchbase-bundle/nifi-couchbase-services-api/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-couchbase-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-couchbase-services-api diff --git a/nifi-nar-bundles/nifi-couchbase-bundle/pom.xml b/nifi-nar-bundles/nifi-couchbase-bundle/pom.xml index 6025781fa360..72954621c321 100644 --- a/nifi-nar-bundles/nifi-couchbase-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-couchbase-bundle/pom.xml @@ -19,12 +19,12 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-couchbase-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-nar/pom.xml b/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-nar/pom.xml index dc0bc2246a6c..85d8771eb7db 100644 --- a/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-nar/pom.xml +++ b/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-cybersecurity-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cybersecurity-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,7 +34,7 @@ org.apache.nifi nifi-cybersecurity-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-processors/pom.xml b/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-processors/pom.xml index 33dc449a6560..7d1f432d6a63 100644 --- a/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-processors/pom.xml +++ b/nifi-nar-bundles/nifi-cybersecurity-bundle/nifi-cybersecurity-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-cybersecurity-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-cybersecurity-processors @@ -33,7 +33,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT info.debatty @@ -48,7 +48,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-cybersecurity-bundle/pom.xml b/nifi-nar-bundles/nifi-cybersecurity-bundle/pom.xml index 6854d369e255..2821680add46 100644 --- a/nifi-nar-bundles/nifi-cybersecurity-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-cybersecurity-bundle/pom.xml @@ -19,12 +19,12 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-cybersecurity-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-nar/pom.xml b/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-nar/pom.xml index c71a779f0c60..a95403920bc0 100644 --- a/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-nar/pom.xml +++ b/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-nar/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-datadog-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-datadog-nar diff --git a/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-reporting-task/pom.xml b/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-reporting-task/pom.xml index fa4a8c061d01..be6ad75e06ff 100644 --- a/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-reporting-task/pom.xml +++ b/nifi-nar-bundles/nifi-datadog-bundle/nifi-datadog-reporting-task/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-datadog-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-datadog-reporting-task @@ -51,7 +51,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT io.dropwizard.metrics @@ -72,7 +72,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-datadog-bundle/pom.xml b/nifi-nar-bundles/nifi-datadog-bundle/pom.xml index a0c09a293e25..a144ca4c5944 100644 --- a/nifi-nar-bundles/nifi-datadog-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-datadog-bundle/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-datadog-bundle @@ -34,7 +34,7 @@ org.apache.nifi nifi-datadog-reporting-task - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.glassfish.jersey.core diff --git a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api-nar/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api-nar/pom.xml index c11a86229474..1afcb2ab84d4 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api-nar/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-controller-service-api-nar @@ -28,13 +28,13 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-druid-controller-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api/pom.xml index 9a7f2b371fd5..f26451cfe1cd 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service-api/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-controller-service-api @@ -28,7 +28,12 @@ org.apache.nifi nifi-api - provided + + + + commons-collections + commons-collections + 3.2.2 io.druid diff --git a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service/pom.xml index b08e147ec25b..240d4a1023ce 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-controller-service/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-controller-service @@ -33,12 +33,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-druid-controller-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided @@ -59,7 +59,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-nar/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-nar/pom.xml index 43115e5d0b31..e46db65184ba 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-nar/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-nar/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-nar @@ -28,18 +28,18 @@ org.apache.nifi nifi-druid-controller-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-druid-controller-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-druid-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-processors/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-processors/pom.xml index 1d6305941e86..5951250307af 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-processors/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/nifi-druid-processors/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-processors @@ -32,7 +32,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -45,7 +45,7 @@ org.apache.nifi nifi-druid-controller-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided @@ -56,19 +56,19 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-mock-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-druid-controller-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-druid-bundle/pom.xml b/nifi-nar-bundles/nifi-druid-bundle/pom.xml index 6a531d8f4da8..b373ba195f63 100644 --- a/nifi-nar-bundles/nifi-druid-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-druid-bundle/pom.xml @@ -18,11 +18,11 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-druid-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-nar/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-nar/pom.xml index 180e9884d8e0..56db6451b8e3 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-nar/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-nar/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -30,7 +30,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-processors/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-processors/pom.xml index 6f9229292eb9..4132810f3913 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-processors/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-5-processors/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-5-processors @@ -40,12 +40,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api-nar/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api-nar/pom.xml index 3f8e5adf9095..4dc632612ca7 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-elasticsearch-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-client-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,13 +34,13 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-client-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api/pom.xml index fe332c0404b9..736a4f3fa174 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-api/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-elasticsearch-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-client-service-api @@ -29,19 +29,18 @@ org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT - provided + 1.9.0-SNAPSHOT org.apache.nifi nifi-ssl-context-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-nar/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-nar/pom.xml index e693594f99ab..6ec3664e4dfe 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-nar/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service-nar/pom.xml @@ -19,11 +19,11 @@ org.apache.nifi nifi-elasticsearch-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-client-service-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -34,13 +34,13 @@ org.apache.nifi nifi-elasticsearch-client-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-client-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service/pom.xml index 5334aa2c9d86..0467763f7e5e 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-client-service/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-elasticsearch-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-client-service @@ -30,30 +30,29 @@ org.apache.nifi nifi-lookup-service-api provided - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-api provided - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - provided - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-distributed-cache-client-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-record - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided @@ -96,19 +95,19 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-ssl-context-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT compile org.apache.nifi nifi-elasticsearch-client-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided @@ -132,7 +131,7 @@ org.apache.nifi nifi-avro-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT compile @@ -153,7 +152,7 @@ org.apache.nifi nifi-record-path - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT compile diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-nar/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-nar/pom.xml index 8d482a13e636..2d5e6d9699e8 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-nar/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-nar/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -30,7 +30,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/pom.xml index dfe72086604b..4db59f6078b9 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-processors @@ -41,12 +41,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-record-path - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -73,13 +73,13 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-mock-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test @@ -122,6 +122,11 @@ language governing permissions and limitations under the License. --> jackson-databind ${jackson.version} + + org.apache.nifi + nifi-standard-record-utils + 1.9.0-SNAPSHOT + diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java index 2448716d8c0e..d431960d3dbb 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java @@ -55,6 +55,7 @@ import org.apache.nifi.serialization.MalformedRecordException; import org.apache.nifi.serialization.RecordReader; import org.apache.nifi.serialization.RecordReaderFactory; +import org.apache.nifi.serialization.SimpleDateFormatValidator; import org.apache.nifi.serialization.record.DataType; import org.apache.nifi.serialization.record.Record; import org.apache.nifi.serialization.record.RecordField; @@ -73,6 +74,7 @@ import java.io.InputStream; import java.math.BigInteger; import java.net.URL; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -177,6 +179,38 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess .required(true) .build(); + static final PropertyDescriptor DATE_FORMAT = new PropertyDescriptor.Builder() + .name("Date Format") + .description("Specifies the format to use when reading/writing Date fields. " + + "If not specified, the default format '" + RecordFieldType.DATE.getDefaultFormat() + "' is used. " + + "If specified, the value must match the Java Simple Date Format (for example, MM/dd/yyyy for a two-digit month, followed by " + + "a two-digit day, followed by a four-digit year, all separated by '/' characters, as in 01/01/2017).") + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(new SimpleDateFormatValidator()) + .required(false) + .build(); + static final PropertyDescriptor TIME_FORMAT = new PropertyDescriptor.Builder() + .name("Time Format") + .description("Specifies the format to use when reading/writing Time fields. " + + "If not specified, the default format '" + RecordFieldType.TIME.getDefaultFormat() + "' is used. " + + "If specified, the value must match the Java Simple Date Format (for example, HH:mm:ss for a two-digit hour in 24-hour format, followed by " + + "a two-digit minute, followed by a two-digit second, all separated by ':' characters, as in 18:04:15).") + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(new SimpleDateFormatValidator()) + .required(false) + .build(); + static final PropertyDescriptor TIMESTAMP_FORMAT = new PropertyDescriptor.Builder() + .name("Timestamp Format") + .description("Specifies the format to use when reading/writing Timestamp fields. " + + "If not specified, the default format '" + RecordFieldType.TIMESTAMP.getDefaultFormat() + "' is used. " + + "If specified, the value must match the Java Simple Date Format (for example, MM/dd/yyyy HH:mm:ss for a two-digit month, followed by " + + "a two-digit day, followed by a four-digit year, all separated by '/' characters; and then followed by a two-digit hour in 24-hour format, followed by " + + "a two-digit minute, followed by a two-digit second, all separated by ':' characters, as in 01/01/2017 18:04:15).") + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .addValidator(new SimpleDateFormatValidator()) + .required(false) + .build(); + private static final Set relationships; private static final List propertyDescriptors; @@ -185,6 +219,9 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess private final JsonFactory factory = new JsonFactory(); private volatile String nullSuppression; + private volatile String dateFormat; + private volatile String timeFormat; + private volatile String timestampFormat; static { final Set _rels = new HashSet<>(); @@ -198,8 +235,12 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess descriptors.add(ID_RECORD_PATH); descriptors.add(INDEX); descriptors.add(TYPE); + descriptors.add(CHARSET); descriptors.add(INDEX_OP); descriptors.add(SUPPRESS_NULLS); + descriptors.add(DATE_FORMAT); + descriptors.add(TIME_FORMAT); + descriptors.add(TIMESTAMP_FORMAT); propertyDescriptors = Collections.unmodifiableList(descriptors); } @@ -246,6 +287,18 @@ protected Collection customValidate(ValidationContext validati public void setup(ProcessContext context) { super.setup(context); recordPathCache = new RecordPathCache(10); + this.dateFormat = context.getProperty(DATE_FORMAT).evaluateAttributeExpressions().getValue(); + if (this.dateFormat == null) { + this.dateFormat = RecordFieldType.DATE.getDefaultFormat(); + } + this.timeFormat = context.getProperty(TIME_FORMAT).evaluateAttributeExpressions().getValue(); + if (this.timeFormat == null) { + this.timeFormat = RecordFieldType.TIME.getDefaultFormat(); + } + this.timestampFormat = context.getProperty(TIMESTAMP_FORMAT).evaluateAttributeExpressions().getValue(); + if (this.timestampFormat == null) { + this.timestampFormat = RecordFieldType.TIMESTAMP.getDefaultFormat(); + } } @Override @@ -313,6 +366,7 @@ public void onTrigger(final ProcessContext context, final ProcessSession session final String id_path = context.getProperty(ID_RECORD_PATH).evaluateAttributeExpressions(flowFile).getValue(); final RecordPath recordPath = StringUtils.isEmpty(id_path) ? null : recordPathCache.getCompiled(id_path); final StringBuilder sb = new StringBuilder(); + final Charset charset = Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(flowFile).getValue()); int recordCount = 0; try (final InputStream in = session.read(flowFile); @@ -345,7 +399,7 @@ public void onTrigger(final ProcessContext context, final ProcessSession session writeRecord(record, record.getSchema(), generator); generator.flush(); generator.close(); - json.append(out.toString()); + json.append(out.toString(charset.name())); buildBulkCommand(sb, index, docType, indexOp, id, json.toString()); recordCount++; @@ -483,7 +537,7 @@ private void writeValue(final JsonGenerator generator, final Object value, final switch (chosenDataType.getFieldType()) { case DATE: { - final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(RecordFieldType.DATE.getDefaultFormat())); + final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(this.dateFormat)); if (DataTypeUtils.isLongTypeCompatible(stringValue)) { generator.writeNumber(DataTypeUtils.toLong(coercedValue, fieldName)); } else { @@ -492,7 +546,7 @@ private void writeValue(final JsonGenerator generator, final Object value, final break; } case TIME: { - final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(RecordFieldType.TIME.getDefaultFormat())); + final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(this.timeFormat)); if (DataTypeUtils.isLongTypeCompatible(stringValue)) { generator.writeNumber(DataTypeUtils.toLong(coercedValue, fieldName)); } else { @@ -501,7 +555,7 @@ private void writeValue(final JsonGenerator generator, final Object value, final break; } case TIMESTAMP: { - final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(RecordFieldType.TIMESTAMP.getDefaultFormat())); + final String stringValue = DataTypeUtils.toString(coercedValue, () -> DataTypeUtils.getDateFormat(this.timestampFormat)); if (DataTypeUtils.isLongTypeCompatible(stringValue)) { generator.writeNumber(DataTypeUtils.toLong(coercedValue, fieldName)); } else { @@ -569,7 +623,7 @@ private void writeValue(final JsonGenerator generator, final Object value, final default: if (coercedValue instanceof Object[]) { final Object[] values = (Object[]) coercedValue; - final ArrayDataType arrayDataType = (ArrayDataType) dataType; + final ArrayDataType arrayDataType = (ArrayDataType) chosenDataType; final DataType elementType = arrayDataType.getElementType(); writeArray(values, fieldName, generator, elementType); } else { diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/TestPutElasticsearchHttpRecord.java b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/TestPutElasticsearchHttpRecord.java index 862e177068a9..992e6159bcb3 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/TestPutElasticsearchHttpRecord.java +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/TestPutElasticsearchHttpRecord.java @@ -16,18 +16,15 @@ */ package org.apache.nifi.processors.elasticsearch; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.ConnectException; -import java.util.HashMap; -import java.util.List; - +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.provenance.ProvenanceEventRecord; @@ -42,16 +39,24 @@ import org.junit.Ignore; import org.junit.Test; -import okhttp3.Call; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; +import java.io.IOException; +import java.net.ConnectException; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; -public class TestPutElasticsearchHttpRecord { +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +public class TestPutElasticsearchHttpRecord { private TestRunner runner; @After @@ -61,13 +66,46 @@ public void teardown() { @Test public void testPutElasticSearchOnTriggerIndex() throws IOException { - runner = TestRunners.newTestRunner(new PutElasticsearchHttpRecordTestProcessor(false)); // no failures + PutElasticsearchHttpRecordTestProcessor processor = new PutElasticsearchHttpRecordTestProcessor(false); + processor.setRecordChecks(record -> { + assertEquals(1, record.get("id")); + assertEquals("reç1", record.get("name")); + assertEquals(101, record.get("code")); + assertEquals("20/12/2018", record.get("date")); + assertEquals("6:55 PM", record.get("time")); + assertEquals("20/12/2018 6:55 PM", record.get("ts")); + }, record -> { + assertEquals(2, record.get("id")); + assertEquals("ræc2", record.get("name")); + assertEquals(102, record.get("code")); + assertEquals("20/12/2018", record.get("date")); + assertEquals("6:55 PM", record.get("time")); + assertEquals("20/12/2018 6:55 PM", record.get("ts")); + }, record -> { + assertEquals(3, record.get("id")); + assertEquals("rèc3", record.get("name")); + assertEquals(103, record.get("code")); + assertEquals("20/12/2018", record.get("date")); + assertEquals("6:55 PM", record.get("time")); + assertEquals("20/12/2018 6:55 PM", record.get("ts")); + }, record -> { + assertEquals(4, record.get("id")); + assertEquals("rëc4", record.get("name")); + assertEquals(104, record.get("code")); + assertEquals("20/12/2018", record.get("date")); + assertEquals("6:55 PM", record.get("time")); + assertEquals("20/12/2018 6:55 PM", record.get("ts")); + }); + runner = TestRunners.newTestRunner(processor); // no failures generateTestData(); runner.setProperty(AbstractElasticsearchHttpProcessor.ES_URL, "http://127.0.0.1:9200"); runner.setProperty(PutElasticsearchHttpRecord.INDEX, "doc"); runner.setProperty(PutElasticsearchHttpRecord.TYPE, "status"); runner.setProperty(PutElasticsearchHttpRecord.ID_RECORD_PATH, "/id"); + runner.setProperty(PutElasticsearchHttpRecord.DATE_FORMAT, "d/M/yyyy"); + runner.setProperty(PutElasticsearchHttpRecord.TIME_FORMAT, "h:m a"); + runner.setProperty(PutElasticsearchHttpRecord.TIMESTAMP_FORMAT, "d/M/yyyy h:m a"); runner.enqueue(new byte[0], new HashMap() {{ put("doc_id", "28039652140"); @@ -368,6 +406,7 @@ private static class PutElasticsearchHttpRecordTestProcessor extends PutElastics int statusCode = 200; String statusMessage = "OK"; String expectedUrl = null; + Consumer[] recordChecks; PutElasticsearchHttpRecordTestProcessor(boolean responseHasFailures) { this.responseHasFailures = responseHasFailures; @@ -382,6 +421,11 @@ void setExpectedUrl(String url) { expectedUrl = url; } + @SafeVarargs + final void setRecordChecks(Consumer... checks) { + recordChecks = checks; + } + @Override protected void createElasticsearchClient(ProcessContext context) throws ProcessException { client = mock(OkHttpClient.class); @@ -391,6 +435,24 @@ protected void createElasticsearchClient(ProcessContext context) throws ProcessE if (statusCode != -1) { Request realRequest = (Request) invocationOnMock.getArguments()[0]; assertTrue((expectedUrl == null) || (expectedUrl.equals(realRequest.url().toString()))); + if (recordChecks != null) { + final ObjectMapper mapper = new ObjectMapper(); + Buffer sink = new Buffer(); + realRequest.body().writeTo(sink); + String line; + int recordIndex = 0; + boolean content = false; + while ((line = sink.readUtf8Line()) != null) { + if (content) { + content = false; + if (recordIndex < recordChecks.length) { + recordChecks[recordIndex++].accept(mapper.readValue(line, Map.class)); + } + } else { + content = true; + } + } + } StringBuilder sb = new StringBuilder("{\"took\": 1, \"errors\": \""); sb.append(responseHasFailures); sb.append("\", \"items\": ["); @@ -520,10 +582,13 @@ private void generateTestData() throws IOException { parser.addSchemaField("id", RecordFieldType.INT); parser.addSchemaField("name", RecordFieldType.STRING); parser.addSchemaField("code", RecordFieldType.INT); - - parser.addRecord(1, "rec1", 101); - parser.addRecord(2, "rec2", 102); - parser.addRecord(3, "rec3", 103); - parser.addRecord(4, "rec4", 104); + parser.addSchemaField("date", RecordFieldType.DATE); + parser.addSchemaField("time", RecordFieldType.TIME); + parser.addSchemaField("ts", RecordFieldType.TIMESTAMP); + + parser.addRecord(1, "reç1", 101, new Date(1545282000000L), new Time(68150000), new Timestamp(1545332150000L)); + parser.addRecord(2, "ræc2", 102, new Date(1545282000000L), new Time(68150000), new Timestamp(1545332150000L)); + parser.addRecord(3, "rèc3", 103, new Date(1545282000000L), new Time(68150000), new Timestamp(1545332150000L)); + parser.addRecord(4, "rëc4", 104, new Date(1545282000000L), new Time(68150000), new Timestamp(1545332150000L)); } } diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-nar/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-nar/pom.xml index a9232bc9ef69..b1a968ef85cb 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-nar/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-nar/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -30,13 +30,13 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-elasticsearch-client-service-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar org.apache.nifi nifi-elasticsearch-restapi-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/pom.xml index eecad55c9684..71f6d7bf88e3 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-elasticsearch-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-elasticsearch-restapi-processors @@ -30,31 +30,31 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-properties - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test org.apache.nifi nifi-ssl-context-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT commons-io @@ -74,7 +74,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-ssl-context-service - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test @@ -85,13 +85,13 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-elasticsearch-client-service-api - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT provided org.apache.nifi nifi-json-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT compile diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/pom.xml b/nifi-nar-bundles/nifi-elasticsearch-bundle/pom.xml index 953a0cef3fbc..cab549b2e331 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/pom.xml @@ -15,7 +15,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -40,17 +40,17 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-elasticsearch-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-elasticsearch-5-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-elasticsearch-restapi-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-email-bundle/nifi-email-nar/pom.xml b/nifi-nar-bundles/nifi-email-bundle/nifi-email-nar/pom.xml index 023ea8ab27a0..cf1aae869550 100644 --- a/nifi-nar-bundles/nifi-email-bundle/nifi-email-nar/pom.xml +++ b/nifi-nar-bundles/nifi-email-bundle/nifi-email-nar/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-email-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-email-nar @@ -33,7 +33,7 @@ org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-email-bundle/nifi-email-processors/pom.xml b/nifi-nar-bundles/nifi-email-bundle/nifi-email-processors/pom.xml index c3b3e52d6630..fb664f601684 100644 --- a/nifi-nar-bundles/nifi-email-bundle/nifi-email-processors/pom.xml +++ b/nifi-nar-bundles/nifi-email-bundle/nifi-email-processors/pom.xml @@ -19,7 +19,7 @@ org.apache.nifi nifi-email-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-email-processors @@ -33,7 +33,7 @@ org.apache.nifi nifi-processor-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT javax.mail @@ -88,7 +88,7 @@ org.springframework.integration spring-integration-mail - 4.3.0.RELEASE + 4.3.17.RELEASE org.springframework.retry @@ -114,7 +114,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-email-bundle/pom.xml b/nifi-nar-bundles/nifi-email-bundle/pom.xml index 3312bf9af1bb..e72bf3269573 100644 --- a/nifi-nar-bundles/nifi-email-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-email-bundle/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-email-bundle @@ -35,7 +35,7 @@ org.apache.nifi nifi-email-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-nar/pom.xml b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-nar/pom.xml index a42da7bbd63a..b7cdc1ba1ff6 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-nar/pom.xml +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-nar/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-enrich-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-enrich-nar nar diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/pom.xml b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/pom.xml index 2e55a4c5d8e1..6a60b21b848a 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/pom.xml +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-enrich-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT @@ -36,7 +36,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.maxmind.geoip2 @@ -57,7 +57,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/GeoEnrichIP.java b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/GeoEnrichIP.java index 175d8735e492..8e657ecbab0e 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/GeoEnrichIP.java +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/GeoEnrichIP.java @@ -16,12 +16,8 @@ */ package org.apache.nifi.processors; -import java.io.IOException; -import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - +import com.maxmind.geoip2.model.CityResponse; +import com.maxmind.geoip2.record.Subdivision; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; @@ -39,9 +35,11 @@ import org.apache.nifi.processors.maxmind.DatabaseReader; import org.apache.nifi.util.StopWatch; -import com.maxmind.geoip2.exception.GeoIp2Exception; -import com.maxmind.geoip2.model.CityResponse; -import com.maxmind.geoip2.record.Subdivision; +import java.io.IOException; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; @EventDriven @SideEffectFree @@ -102,7 +100,7 @@ public void onTrigger(final ProcessContext context, final ProcessSession session try { response = dbReader.city(inetAddress); stopWatch.stop(); - } catch (final IOException | GeoIp2Exception ex) { + } catch (final IOException ex) { // Note IOException is captured again as dbReader also makes InetAddress.getByName() calls. // Most name or IP resolutions failure should have been triggered in the try loop above but // environmental conditions may trigger errors during the second resolution as well. diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/ISPEnrichIP.java b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/ISPEnrichIP.java index fc159c9ee3e2..781fb71c4702 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/ISPEnrichIP.java +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/ISPEnrichIP.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.processors; -import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.IspResponse; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.EventDriven; @@ -94,7 +93,7 @@ public void onTrigger(final ProcessContext context, final ProcessSession session try { response = dbReader.isp(inetAddress); stopWatch.stop(); - } catch (final IOException | GeoIp2Exception ex) { + } catch (final IOException ex) { // Note IOException is captured again as dbReader also makes InetAddress.getByName() calls. // Most name or IP resolutions failure should have been triggered in the try loop above but // environmental conditions may trigger errors during the second resolution as well. diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/maxmind/DatabaseReader.java b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/maxmind/DatabaseReader.java index fb84daf6ad0a..8cbc74aa99d7 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/maxmind/DatabaseReader.java +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/main/java/org/apache/nifi/processors/maxmind/DatabaseReader.java @@ -16,30 +16,32 @@ */ package org.apache.nifi.processors.maxmind; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.util.Arrays; -import java.util.List; - +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.maxmind.db.Metadata; import com.maxmind.db.Reader; import com.maxmind.db.Reader.FileMode; import com.maxmind.geoip2.GeoIp2Provider; -import com.maxmind.geoip2.exception.AddressNotFoundException; -import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.AnonymousIpResponse; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.ConnectionTypeResponse; import com.maxmind.geoip2.model.CountryResponse; import com.maxmind.geoip2.model.DomainResponse; import com.maxmind.geoip2.model.IspResponse; +import com.maxmind.geoip2.record.Traits; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.List; /** *

@@ -54,10 +56,10 @@ public class DatabaseReader implements GeoIp2Provider, Closeable { private final Reader reader; - private final ObjectMapper om; + private List locales; - private DatabaseReader(Builder builder) throws IOException { + private DatabaseReader(final Builder builder) throws IOException { if (builder.stream != null) { this.reader = new Reader(builder.stream); } else if (builder.database != null) { @@ -65,26 +67,27 @@ private DatabaseReader(Builder builder) throws IOException { } else { // This should never happen. If it does, review the Builder class // constructors for errors. - throw new IllegalArgumentException( - "Unsupported Builder configuration: expected either File or URL"); + throw new IllegalArgumentException("Unsupported Builder configuration: expected either File or URL"); } + this.om = new ObjectMapper(); - this.om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - false); - this.om.configure( - DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - InjectableValues inject = new InjectableValues.Std().addValue( - "locales", builder.locales); + this.om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); + this.om.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + InjectableValues inject = new InjectableValues.Std().addValue("locales", builder.locales); this.om.setInjectableValues(inject); + + this.locales = builder.locales; } /** *

* Constructs a Builder for the DatabaseReader. The file passed to it must be a valid GeoIP2 database file. *

+ * *

* Builder creates instances of DatabaseReader from values set by the methods. *

+ * *

* Only the values set in the Builder constructor are required. *

@@ -150,16 +153,11 @@ public DatabaseReader build() throws IOException { * @return An object of type T with the data for the IP address or null if no information could be found for the given IP address * @throws IOException if there is an error opening or reading from the file. */ - private T get(InetAddress ipAddress, Class cls, boolean hasTraits, - String type) throws IOException, AddressNotFoundException { - + private T get(InetAddress ipAddress, Class cls, boolean hasTraits, String type) throws IOException { String databaseType = this.getMetadata().getDatabaseType(); if (!databaseType.contains(type)) { - String caller = Thread.currentThread().getStackTrace()[2] - .getMethodName(); - throw new UnsupportedOperationException( - "Invalid attempt to open a " + databaseType - + " database using the " + caller + " method"); + String caller = Thread.currentThread().getStackTrace()[2].getMethodName(); + throw new UnsupportedOperationException("Invalid attempt to open a " + databaseType + " database using the " + caller + " method"); } ObjectNode node = (ObjectNode) this.reader.get(ipAddress); @@ -168,20 +166,11 @@ private T get(InetAddress ipAddress, Class cls, boolean hasTraits, return null; } - ObjectNode ipNode; - if (hasTraits) { - if (!node.has("traits")) { - node.set("traits", this.om.createObjectNode()); - } - ipNode = (ObjectNode) node.get("traits"); - } else { - ipNode = node; - } - ipNode.put("ip_address", ipAddress.getHostAddress()); - - return this.om.treeToValue(node, cls); + InjectableValues inject = new JsonInjector(ipAddress.getHostAddress()); + return this.om.reader(inject).treeToValue(node, cls); } + /** *

* Closes the database. @@ -200,15 +189,13 @@ public void close() throws IOException { } @Override - public CountryResponse country(InetAddress ipAddress) throws IOException, - GeoIp2Exception { - return this.get(ipAddress, CountryResponse.class, true, "Country"); + public CountryResponse country(InetAddress ipAddress) throws IOException { + return get(ipAddress, CountryResponse.class, true, "Country"); } @Override - public CityResponse city(InetAddress ipAddress) throws IOException, - GeoIp2Exception { - return this.get(ipAddress, CityResponse.class, true, "City"); + public CityResponse city(InetAddress ipAddress) throws IOException { + return get(ipAddress, CityResponse.class, true, "City"); } /** @@ -216,12 +203,10 @@ public CityResponse city(InetAddress ipAddress) throws IOException, * * @param ipAddress IPv4 or IPv6 address to lookup. * @return a AnonymousIpResponse for the requested IP address. - * @throws GeoIp2Exception if there is an error looking up the IP * @throws IOException if there is an IO error */ - public AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException, - GeoIp2Exception { - return this.get(ipAddress, AnonymousIpResponse.class, false, "GeoIP2-Anonymous-IP"); + public AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException { + return get(ipAddress, AnonymousIpResponse.class, false, "GeoIP2-Anonymous-IP"); } /** @@ -229,13 +214,10 @@ public AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException * * @param ipAddress IPv4 or IPv6 address to lookup. * @return a ConnectTypeResponse for the requested IP address. - * @throws GeoIp2Exception if there is an error looking up the IP * @throws IOException if there is an IO error */ - public ConnectionTypeResponse connectionType(InetAddress ipAddress) - throws IOException, GeoIp2Exception { - return this.get(ipAddress, ConnectionTypeResponse.class, false, - "GeoIP2-Connection-Type"); + public ConnectionTypeResponse connectionType(InetAddress ipAddress) throws IOException { + return get(ipAddress, ConnectionTypeResponse.class, false,"GeoIP2-Connection-Type"); } /** @@ -243,13 +225,10 @@ public ConnectionTypeResponse connectionType(InetAddress ipAddress) * * @param ipAddress IPv4 or IPv6 address to lookup. * @return a DomainResponse for the requested IP address. - * @throws GeoIp2Exception if there is an error looking up the IP * @throws IOException if there is an IO error */ - public DomainResponse domain(InetAddress ipAddress) throws IOException, - GeoIp2Exception { - return this - .get(ipAddress, DomainResponse.class, false, "GeoIP2-Domain"); + public DomainResponse domain(InetAddress ipAddress) throws IOException { + return get(ipAddress, DomainResponse.class, false, "GeoIP2-Domain"); } /** @@ -257,12 +236,10 @@ public DomainResponse domain(InetAddress ipAddress) throws IOException, * * @param ipAddress IPv4 or IPv6 address to lookup. * @return an IspResponse for the requested IP address. - * @throws GeoIp2Exception if there is an error looking up the IP * @throws IOException if there is an IO error */ - public IspResponse isp(InetAddress ipAddress) throws IOException, - GeoIp2Exception { - return this.get(ipAddress, IspResponse.class, false, "GeoIP2-ISP"); + public IspResponse isp(InetAddress ipAddress) throws IOException { + return get(ipAddress, IspResponse.class, false, "GeoIP2-ISP"); } /** @@ -271,4 +248,25 @@ public IspResponse isp(InetAddress ipAddress) throws IOException, public Metadata getMetadata() { return this.reader.getMetadata(); } + + private class JsonInjector extends InjectableValues { + private final String ip; + + JsonInjector(final String ip) { + this.ip = ip; + } + + @Override + public Object findInjectableValue(final Object valueId, final DeserializationContext ctxt, final BeanProperty forProperty, final Object beanInstance) throws JsonMappingException { + if ("ip_address".equals(valueId)) { + return ip; + } else if ("traits".equals(valueId)) { + return new Traits(ip); + } else if ("locales".equals(valueId)) { + return locales; + } + + return null; + } + } } diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestGeoEnrichIP.java b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestGeoEnrichIP.java index 62a8f1e6ba4f..f6629a015721 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestGeoEnrichIP.java +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestGeoEnrichIP.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; -import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CityResponse; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.flowfile.FlowFile; @@ -239,11 +238,11 @@ public void shouldFlowToNotFoundWhenIOExceptionThrownFromMaxMind() throws Except @SuppressWarnings("unchecked") @Test - public void shouldFlowToNotFoundWhenGeoIp2ExceptionThrownFromMaxMind() throws Exception { + public void shouldFlowToNotFoundWhenExceptionThrownFromMaxMind() throws Exception { testRunner.setProperty(GeoEnrichIP.GEO_DATABASE_FILE, "./"); testRunner.setProperty(GeoEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip"); - when(databaseReader.city(InetAddress.getByName("1.2.3.4"))).thenThrow(GeoIp2Exception.class); + when(databaseReader.city(InetAddress.getByName("1.2.3.4"))).thenThrow(IOException.class); final Map attributes = new HashMap<>(); attributes.put("ip", "1.2.3.4"); diff --git a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestISPEnrichIP.java b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestISPEnrichIP.java index 187d0fe8c5e2..8cc82ba50fa8 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestISPEnrichIP.java +++ b/nifi-nar-bundles/nifi-enrich-bundle/nifi-enrich-processors/src/test/java/org/apache/nifi/processors/TestISPEnrichIP.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; -import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.IspResponse; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.flowfile.FlowFile; @@ -224,11 +223,11 @@ public void shouldFlowToNotFoundWhenIOExceptionThrownFromMaxMind() throws Except @SuppressWarnings("unchecked") @Test - public void shouldFlowToNotFoundWhenGeoIp2ExceptionThrownFromMaxMind() throws Exception { + public void shouldFlowToNotFoundWhenExceptionThrownFromMaxMind() throws Exception { testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./"); testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip"); - when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenThrow(GeoIp2Exception.class); + when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenThrow(IOException.class); final Map attributes = new HashMap<>(); attributes.put("ip", "1.2.3.4"); diff --git a/nifi-nar-bundles/nifi-enrich-bundle/pom.xml b/nifi-nar-bundles/nifi-enrich-bundle/pom.xml index 293dbc0dc3cd..b9cd5d82fff4 100644 --- a/nifi-nar-bundles/nifi-enrich-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-enrich-bundle/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-enrich-bundle @@ -35,7 +35,7 @@ org.apache.nifi nifi-enrich-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-nar/pom.xml b/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-nar/pom.xml index 44120f86c4be..89d1b2699e31 100644 --- a/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-nar/pom.xml +++ b/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-nar/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> nifi-evtx-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -29,7 +29,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-standard-services-api-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-processors/pom.xml b/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-processors/pom.xml index 25aecc448a5e..a0266ddb092f 100644 --- a/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-processors/pom.xml +++ b/nifi-nar-bundles/nifi-evtx-bundle/nifi-evtx-processors/pom.xml @@ -20,7 +20,7 @@ nifi-evtx-bundle org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-evtx-processors @@ -40,7 +40,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT com.google.guava @@ -55,7 +55,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-evtx-bundle/pom.xml b/nifi-nar-bundles/nifi-evtx-bundle/pom.xml index 5c702faae5ee..1cc18d3df7e8 100644 --- a/nifi-nar-bundles/nifi-evtx-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-evtx-bundle/pom.xml @@ -22,7 +22,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -43,7 +43,7 @@ org.apache.nifi nifi-evtx-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-hadoop-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-hadoop-utils/pom.xml index f1a36b0f0316..b92b886b7603 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-hadoop-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-hadoop-utils/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-extension-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-hadoop-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT jar 3.0.0 @@ -34,7 +34,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/pom.xml index e4e30d92f198..82f173afaac4 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-extension-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-processor-utils jar @@ -38,12 +38,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT commons-io @@ -69,7 +69,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java b/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java index 04e444c972ef..c30bb0ddf31e 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-processor-utils/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java @@ -215,6 +215,7 @@ public abstract class AbstractListProcessor extends Ab private volatile Long lastRunTimeNanos = 0L; private volatile boolean justElectedPrimaryNode = false; private volatile boolean resetState = false; + private volatile boolean resetEntityTrackingState = false; private volatile List latestIdentifiersProcessed = new ArrayList<>(); private volatile ListedEntityTracker listedEntityTracker; @@ -245,6 +246,7 @@ public void onPropertyModified(final PropertyDescriptor descriptor, final String if (isConfigurationRestored() && isListingResetNecessary(descriptor)) { resetTimeStates(); // clear lastListingTime so that we have to fetch new time resetState = true; + resetEntityTrackingState = true; } } @@ -312,6 +314,7 @@ public final void updateState(final ProcessContext context) throws IOException { if (resetState) { context.getStateManager().clear(getStateScope(context)); + resetState = false; } } @@ -406,8 +409,6 @@ private EntityListing deserialize(final String serializedState) throws JsonParse @Override public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { - resetState = false; - final String listingStrategy = context.getProperty(LISTING_STRATEGY).getValue(); if (BY_TIMESTAMPS.equals(listingStrategy)) { listByTrackingTimestamps(context, session); @@ -712,13 +713,14 @@ public void serialize(final String value, final OutputStream out) throws Seriali @OnScheduled public void initListedEntityTracker(ProcessContext context) { final boolean isTrackingEntityStrategy = BY_ENTITIES.getValue().equals(context.getProperty(LISTING_STRATEGY).getValue()); - if (listedEntityTracker != null && (resetState || !isTrackingEntityStrategy)) { + if (listedEntityTracker != null && (resetEntityTrackingState || !isTrackingEntityStrategy)) { try { listedEntityTracker.clearListedEntities(); } catch (IOException e) { throw new RuntimeException("Failed to reset previously listed entities due to " + e, e); } } + resetEntityTrackingState = false; if (isTrackingEntityStrategy) { if (listedEntityTracker == null) { diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/pom.xml index 05e62f40e60e..5a3c6182ac45 100755 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-avro-record-utils @@ -27,13 +27,18 @@ org.apache.nifi nifi-standard-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.avro avro 1.8.1 + + com.github.ben-manes.caffeine + caffeine + 2.6.2 + org.apache.nifi @@ -46,7 +51,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java index 2e8898a49502..2e2c8f75cbba 100755 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java @@ -396,6 +396,8 @@ public static RecordSchema createSchema(final Schema avroSchema, final String sc final String schemaFullName = avroSchema.getNamespace() + "." + avroSchema.getName(); final SimpleRecordSchema recordSchema = schemaText == null ? new SimpleRecordSchema(schemaId) : new SimpleRecordSchema(schemaText, AVRO_SCHEMA_FORMAT, schemaId); + recordSchema.setSchemaName(avroSchema.getName()); + recordSchema.setSchemaNamespace(avroSchema.getNamespace()); final DataType recordSchemaType = RecordFieldType.RECORD.getRecordDataType(recordSchema); final Map knownRecords = new HashMap<>(); knownRecords.put(schemaFullName, recordSchemaType); @@ -629,7 +631,9 @@ private static Object convertToAvroObject(final Object rawValue, final Schema fi final int desiredScale = decimalType.getScale(); final BigDecimal decimal = rawDecimal.scale() == desiredScale ? rawDecimal : rawDecimal.setScale(desiredScale, BigDecimal.ROUND_HALF_UP); - return new Conversions.DecimalConversion().toBytes(decimal, fieldSchema, logicalType); + return fieldSchema.getType() == Type.BYTES + ? new Conversions.DecimalConversion().toBytes(decimal, fieldSchema, logicalType) //return GenericByte + : new Conversions.DecimalConversion().toFixed(decimal, fieldSchema, logicalType); //return GenericFixed } if (rawValue instanceof byte[]) { return ByteBuffer.wrap((byte[]) rawValue); diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/schema/access/WriteAvroSchemaAttributeStrategy.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/schema/access/WriteAvroSchemaAttributeStrategy.java index 5f94679dbc21..97389a9edb2b 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/schema/access/WriteAvroSchemaAttributeStrategy.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/schema/access/WriteAvroSchemaAttributeStrategy.java @@ -17,26 +17,23 @@ package org.apache.nifi.schema.access; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.apache.nifi.avro.AvroTypeUtil; +import org.apache.nifi.serialization.record.RecordSchema; + import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.EnumSet; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; -import org.apache.avro.Schema; -import org.apache.nifi.avro.AvroTypeUtil; -import org.apache.nifi.serialization.record.RecordSchema; - public class WriteAvroSchemaAttributeStrategy implements SchemaAccessWriter { - private final Map avroSchemaTextCache = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > 10; - } - }; + private final LoadingCache avroSchemaTextCache = Caffeine.newBuilder() + .maximumSize(10) + .build(schema -> AvroTypeUtil.extractAvroSchema(schema).toString()); @Override public void writeHeader(final RecordSchema schema, final OutputStream out) throws IOException { @@ -53,14 +50,7 @@ public Map getAttributes(final RecordSchema schema) { } } - String schemaText = avroSchemaTextCache.get(schema); - if (schemaText == null) { - final Schema avroSchema = AvroTypeUtil.extractAvroSchema(schema); - schemaText = avroSchema.toString(); - avroSchemaTextCache.put(schema, schemaText); - } - - return Collections.singletonMap("avro.schema", schemaText); + return Collections.singletonMap("avro.schema", avroSchemaTextCache.get(schema)); } @Override diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java index 83d54c6fadaa..1f6c29bfb3bb 100755 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/test/java/org/apache/nifi/avro/TestAvroTypeUtil.java @@ -42,6 +42,7 @@ import org.apache.avro.file.DataFileStream; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericFixed; import org.apache.avro.generic.GenericRecord; import org.apache.avro.generic.GenericRecordBuilder; import org.apache.avro.generic.GenericData.Record; @@ -396,6 +397,30 @@ public void testToDecimalConversion() { } + @Test + public void testBytesDecimalConversion(){ + final LogicalTypes.Decimal decimalType = LogicalTypes.decimal(18, 8); + final Schema fieldSchema = Schema.create(Type.BYTES); + decimalType.addToSchema(fieldSchema); + final Object convertedValue = AvroTypeUtil.convertToAvroObject("2.5", fieldSchema, StandardCharsets.UTF_8); + assertTrue(convertedValue instanceof ByteBuffer); + final ByteBuffer serializedBytes = (ByteBuffer)convertedValue; + final BigDecimal bigDecimal = new Conversions.DecimalConversion().fromBytes(serializedBytes, fieldSchema, decimalType); + assertEquals(new BigDecimal("2.5").setScale(8), bigDecimal); + } + + @Test + public void testFixedDecimalConversion(){ + final LogicalTypes.Decimal decimalType = LogicalTypes.decimal(18, 8); + final Schema fieldSchema = Schema.createFixed("mydecimal", "no doc", "myspace", 18); + decimalType.addToSchema(fieldSchema); + final Object convertedValue = AvroTypeUtil.convertToAvroObject("2.5", fieldSchema, StandardCharsets.UTF_8); + assertTrue(convertedValue instanceof GenericFixed); + final GenericFixed genericFixed = (GenericFixed)convertedValue; + final BigDecimal bigDecimal = new Conversions.DecimalConversion().fromFixed(genericFixed, fieldSchema, decimalType); + assertEquals(new BigDecimal("2.5").setScale(8), bigDecimal); + } + @Test public void testSchemaNameNotEmpty() throws IOException { Schema schema = new Schema.Parser().parse(getClass().getResourceAsStream("simpleSchema.json")); diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-hadoop-record-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-hadoop-record-utils/pom.xml index 970721bb3971..d485f1070e3a 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-hadoop-record-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-hadoop-record-utils/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-hadoop-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT jar 2.7.3 @@ -30,12 +30,12 @@ org.apache.nifi nifi-hadoop-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-avro-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/pom.xml index 157b5da4cc95..09830325c332 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-mock-record-utils @@ -31,7 +31,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT @@ -49,7 +49,7 @@ org.apache.nifi nifi-avro-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/src/main/java/org/apache/nifi/serialization/record/MockRecordParser.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/src/main/java/org/apache/nifi/serialization/record/MockRecordParser.java index 9b5441ef3779..be4046cb54ba 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/src/main/java/org/apache/nifi/serialization/record/MockRecordParser.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-mock-record-utils/src/main/java/org/apache/nifi/serialization/record/MockRecordParser.java @@ -58,6 +58,10 @@ public void addSchemaField(final String fieldName, final RecordFieldType type, b fields.add(new RecordField(fieldName, type.getDataType(), isNullable)); } + public void addSchemaField(final RecordField recordField) { + fields.add(recordField); + } + public void addRecord(Object... values) { records.add(values); } @@ -75,7 +79,7 @@ public void close() throws IOException { @Override public Record nextRecord(final boolean coerceTypes, final boolean dropUnknown) throws IOException, MalformedRecordException { - if (failAfterN >= recordCount) { + if (failAfterN >= 0 && recordCount >= failAfterN) { throw new MalformedRecordException("Intentional Unit Test Exception because " + recordCount + " records have been read"); } recordCount++; diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/pom.xml index 36f565ff60a5..b6b658744f51 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-record-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-standard-record-utils @@ -31,12 +31,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-security-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT @@ -64,7 +64,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/main/java/org/apache/nifi/schema/validation/StandardSchemaValidator.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/main/java/org/apache/nifi/schema/validation/StandardSchemaValidator.java index d4679628919c..eb9722c8b4e4 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/main/java/org/apache/nifi/schema/validation/StandardSchemaValidator.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/main/java/org/apache/nifi/schema/validation/StandardSchemaValidator.java @@ -196,21 +196,32 @@ private boolean isTypeCorrect(final Object value, final DataType dataType) { return true; case MAP: - if (!(value instanceof Map)) { - return false; - } - - final MapDataType mapDataType = (MapDataType) dataType; - final DataType valueDataType = mapDataType.getValueType(); - final Map map = (Map) value; - - for (final Object mapValue : map.values()) { - if (!isTypeCorrect(mapValue, valueDataType)) { - return false; + if (value instanceof Map) { + final MapDataType mapDataType = (MapDataType) dataType; + final DataType valueDataType = mapDataType.getValueType(); + final Map map = (Map) value; + + for (final Object mapValue : map.values()) { + if (!isTypeCorrect(mapValue, valueDataType)) { + return false; + } } + return true; + } else if (value instanceof Record) { + Record record = (Record) value; + final MapDataType mapDataType = (MapDataType) dataType; + final DataType valueDataType = mapDataType.getValueType(); + + for (final String fieldName : record.getRawFieldNames()) { + final Object fieldValue = record.getValue(fieldName); + if (!isTypeCorrect(fieldValue, valueDataType)) { + return false; + } + } + return true; + } else { + return false; } - - return true; case RECORD: return value instanceof Record; case CHOICE: diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/test/java/org/apache/nifi/schema/validation/TestStandardSchemaValidator.java b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/test/java/org/apache/nifi/schema/validation/TestStandardSchemaValidator.java index f323a03b442f..00de9f5f7eb0 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/test/java/org/apache/nifi/schema/validation/TestStandardSchemaValidator.java +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/nifi-standard-record-utils/src/test/java/org/apache/nifi/schema/validation/TestStandardSchemaValidator.java @@ -75,6 +75,13 @@ public void testValidateCorrectSimpleTypesStrictValidation() throws ParseExcepti intMap.put("height", 48); intMap.put("width", 96); + List mapRecordFields = new ArrayList<>(); + RecordField mapRecordField = new RecordField("mapRecord", RecordFieldType.MAP.getMapDataType(RecordFieldType.INT.getDataType())); + mapRecordFields.add(mapRecordField); + fields.add(mapRecordField); + RecordSchema mapRecordSchema = new SimpleRecordSchema(mapRecordFields); + MapRecord mapRecord = new MapRecord(mapRecordSchema, intMap); + final RecordSchema schema = new SimpleRecordSchema(fields); final Map valueMap = new LinkedHashMap<>(); valueMap.put("string", "string"); @@ -94,6 +101,7 @@ public void testValidateCorrectSimpleTypesStrictValidation() throws ParseExcepti valueMap.put("array", null); valueMap.put("choice", 48L); valueMap.put("map", intMap); + valueMap.put("mapRecord", mapRecord); final Record record = new MapRecord(schema, valueMap); diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/pom.xml index d516ba4e8810..ce78ff120c72 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-record-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-extension-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom nifi-record-utils @@ -30,4 +30,14 @@ nifi-mock-record-utils + + + + + io.netty + netty + 3.7.1.Final + + + diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-reporting-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-reporting-utils/pom.xml index 681589c33d92..95d8dc572f40 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-reporting-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-reporting-utils/pom.xml @@ -17,12 +17,12 @@ nifi-extension-utils org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT 4.0.0 nifi-reporting-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT jar @@ -33,7 +33,7 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.commons diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-syslog-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-syslog-utils/pom.xml index e78e694c20ff..205ec114d86e 100644 --- a/nifi-nar-bundles/nifi-extension-utils/nifi-syslog-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/nifi-syslog-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-extension-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-syslog-utils jar @@ -31,12 +31,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-extension-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/pom.xml index 85f2a4e61b0d..dee311d5e2ff 100644 --- a/nifi-nar-bundles/nifi-extension-utils/pom.xml +++ b/nifi-nar-bundles/nifi-extension-utils/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom nifi-extension-utils diff --git a/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-nar/pom.xml b/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-nar/pom.xml index 0cf790996a7b..eefc072feca7 100644 --- a/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-nar/pom.xml +++ b/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-nar/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-flume-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-flume-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar true @@ -130,7 +130,7 @@ org.apache.nifi nifi-hadoop-libraries-nar - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nar diff --git a/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-processors/pom.xml b/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-processors/pom.xml index a6b343e7e251..d4477746a456 100644 --- a/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-processors/pom.xml +++ b/nifi-nar-bundles/nifi-flume-bundle/nifi-flume-processors/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-flume-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-flume-processors jar @@ -36,12 +36,12 @@ org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-flowfile-packager - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.flume @@ -144,7 +144,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-flume-bundle/pom.xml b/nifi-nar-bundles/nifi-flume-bundle/pom.xml index 78eaf26fc9cc..be9025fe8bb0 100644 --- a/nifi-nar-bundles/nifi-flume-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-flume-bundle/pom.xml @@ -18,10 +18,10 @@ org.apache.nifi nifi-nar-bundles - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-flume-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT pom A bundle of processors that run Flume sources/sinks @@ -33,7 +33,7 @@ org.apache.nifi nifi-flume-processors - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/pom.xml index 2844fe5cd666..d7bbfb30f21a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/pom.xml @@ -17,7 +17,7 @@ org.apache.nifi nifi-framework-bundle - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-nar nar @@ -42,7 +42,7 @@ org.apache.nifi nifi-standard-prioritizers - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE index e6a732228697..fae5c91e4a2e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE @@ -212,6 +212,6 @@ SIL OFL 1.1 ****************** The following binary components are provided under the SIL Open Font License 1.1 - (SIL OFL 1.1) FontAwesome (4.6.1 - http://fortawesome.github.io/Font-Awesome/license/) + (SIL OFL 1.1) FontAwesome (4.7.0 - https://fontawesome.com/license/free) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/pom.xml index 9be0257b0fc6..cc0ecf108cce 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-administration diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/pom.xml index feb2a15f6c04..d13b37eb291f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/pom.xml @@ -17,7 +17,7 @@ nifi-framework org.apache.nifi - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT 4.0.0 @@ -100,7 +100,7 @@ org.apache.nifi nifi-mock-authorizer - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java index 79d375731e19..1e122642df67 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java @@ -89,6 +89,7 @@ private static JAXBContext initializeJaxbContext() { private Authorizer authorizer; private NiFiProperties properties; + private ExtensionManager extensionManager; private final Map userGroupProviders = new HashMap<>(); private final Map accessPolicyProviders = new HashMap<>(); private final Map authorizers = new HashMap<>(); @@ -208,7 +209,7 @@ private Authorizers loadAuthorizersConfiguration() throws Exception { private UserGroupProvider createUserGroupProvider(final String identifier, final String userGroupProviderClassName) throws Exception { // get the classloader for the specified user group provider - final List userGroupProviderBundles = ExtensionManager.getBundles(userGroupProviderClassName); + final List userGroupProviderBundles = extensionManager.getBundles(userGroupProviderClassName); if (userGroupProviderBundles.size() == 0) { throw new Exception(String.format("The specified user group provider class '%s' is not known to this nifi.", userGroupProviderClassName)); @@ -256,7 +257,7 @@ private UserGroupProvider createUserGroupProvider(final String identifier, final private AccessPolicyProvider createAccessPolicyProvider(final String identifier, final String accessPolicyProviderClassName) throws Exception { // get the classloader for the specified access policy provider - final List accessPolicyProviderBundles = ExtensionManager.getBundles(accessPolicyProviderClassName); + final List accessPolicyProviderBundles = extensionManager.getBundles(accessPolicyProviderClassName); if (accessPolicyProviderBundles.size() == 0) { throw new Exception(String.format("The specified access policy provider class '%s' is not known to this nifi.", accessPolicyProviderClassName)); @@ -304,7 +305,7 @@ private AccessPolicyProvider createAccessPolicyProvider(final String identifier, private Authorizer createAuthorizer(final String identifier, final String authorizerClassName, final String classpathResources) throws Exception { // get the classloader for the specified authorizer - final List authorizerBundles = ExtensionManager.getBundles(authorizerClassName); + final List authorizerBundles = extensionManager.getBundles(authorizerClassName); if (authorizerBundles.size() == 0) { throw new Exception(String.format("The specified authorizer class '%s' is not known to this nifi.", authorizerClassName)); @@ -537,4 +538,8 @@ public void setProperties(NiFiProperties properties) { this.properties = properties; } + public void setExtensionManager(ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/resources/nifi-authorizer-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/resources/nifi-authorizer-context.xml index 71bf68489376..6c484fc5101e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/resources/nifi-authorizer-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/resources/nifi-authorizer-context.xml @@ -21,6 +21,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml index 5a4fafd94ebd..1e2964402150 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-client-dto diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusDTO.java index eb884906ca40..4f66bf185804 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusDTO.java @@ -76,7 +76,9 @@ public void setName(String name) { } - @ApiModelProperty("The run status of the port.") + @ApiModelProperty( + value="The run status of the port.", + allowableValues = "Running, Stopped, Validating, Disabled, Invalid") public String getRunStatus() { return runStatus; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusSnapshotDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusSnapshotDTO.java index fa740d3b4a45..d21a37095b49 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusSnapshotDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/PortStatusSnapshotDTO.java @@ -104,7 +104,9 @@ public void setName(String name) { /** * @return run status of this port */ - @ApiModelProperty("The run status of the port.") + @ApiModelProperty( + value="The run status of the port.", + allowableValues = "Running, Stopped, Validating, Disabled, Invalid") public String getRunStatus() { return runStatus; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusDTO.java index 054d5652bb21..2d6508834a3f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusDTO.java @@ -78,7 +78,7 @@ public void setType(String type) { } @ApiModelProperty(value="The run status of the Processor", - allowableValues = "Running, Stopped, Disabled, Invalid") + allowableValues = "Running, Stopped, Validating, Disabled, Invalid") public String getRunStatus() { return runStatus; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusSnapshotDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusSnapshotDTO.java index 9c1fd59fb191..10c740880c15 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusSnapshotDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessorStatusSnapshotDTO.java @@ -95,7 +95,7 @@ public void setType(String type) { */ @ApiModelProperty( value = "The state of the processor.", - allowableValues = "Running, Stopped, Disabled, Invalid" + allowableValues = "Running, Stopped, Validating, Disabled, Invalid" ) public String getRunStatus() { return runStatus; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ReportingTaskStatusDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ReportingTaskStatusDTO.java index 57ad5537b02b..02bed5d75417 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ReportingTaskStatusDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ReportingTaskStatusDTO.java @@ -29,7 +29,7 @@ public class ReportingTaskStatusDTO extends ComponentStatusDTO { @ApiModelProperty(value = "The run status of this ReportingTask", readOnly = true, - allowableValues = "RUNNING, STOPPED") + allowableValues = "RUNNING, STOPPED, DISABLED") @Override public String getRunStatus() { return super.getRunStatus(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml index 5cc8975399f5..496f0af8fb63 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml @@ -14,7 +14,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-documentation diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java index dd9992728222..aa2dbe020d98 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java @@ -56,14 +56,14 @@ public class DocGenerator { * @param properties to lookup nifi properties * @param extensionMapping extension mapping */ - public static void generate(final NiFiProperties properties, final ExtensionMapping extensionMapping) { + public static void generate(final NiFiProperties properties, final ExtensionManager extensionManager, final ExtensionMapping extensionMapping) { final File explodedNiFiDocsDir = properties.getComponentDocumentationWorkingDirectory(); logger.debug("Generating documentation for: " + extensionMapping.size() + " components in: " + explodedNiFiDocsDir); - documentConfigurableComponent(ExtensionManager.getExtensions(Processor.class), explodedNiFiDocsDir); - documentConfigurableComponent(ExtensionManager.getExtensions(ControllerService.class), explodedNiFiDocsDir); - documentConfigurableComponent(ExtensionManager.getExtensions(ReportingTask.class), explodedNiFiDocsDir); + documentConfigurableComponent(extensionManager.getExtensions(Processor.class), explodedNiFiDocsDir, extensionManager); + documentConfigurableComponent(extensionManager.getExtensions(ControllerService.class), explodedNiFiDocsDir, extensionManager); + documentConfigurableComponent(extensionManager.getExtensions(ReportingTask.class), explodedNiFiDocsDir, extensionManager); } /** @@ -72,12 +72,12 @@ public static void generate(final NiFiProperties properties, final ExtensionMapp * @param extensionClasses types of a configurable component * @param explodedNiFiDocsDir base directory of component documentation */ - private static void documentConfigurableComponent(final Set extensionClasses, final File explodedNiFiDocsDir) { + public static void documentConfigurableComponent(final Set extensionClasses, final File explodedNiFiDocsDir, final ExtensionManager extensionManager) { for (final Class extensionClass : extensionClasses) { if (ConfigurableComponent.class.isAssignableFrom(extensionClass)) { final String extensionClassName = extensionClass.getCanonicalName(); - final Bundle bundle = ExtensionManager.getBundle(extensionClass.getClassLoader()); + final Bundle bundle = extensionManager.getBundle(extensionClass.getClassLoader()); if (bundle == null) { logger.warn("No coordinate found for {}, skipping...", new Object[] {extensionClassName}); continue; @@ -91,7 +91,7 @@ private static void documentConfigurableComponent(final Set extensionClas final Class componentClass = extensionClass.asSubclass(ConfigurableComponent.class); try { logger.debug("Documenting: " + componentClass); - document(componentDirectory, componentClass, coordinate); + document(extensionManager, componentDirectory, componentClass, coordinate); } catch (Exception e) { logger.warn("Unable to document: " + componentClass, e); } @@ -111,14 +111,17 @@ private static void documentConfigurableComponent(final Set extensionClas * @throws IOException ioe * @throws InitializationException ie */ - private static void document(final File componentDocsDir, final Class componentClass, final BundleCoordinate bundleCoordinate) + private static void document(final ExtensionManager extensionManager, + final File componentDocsDir, + final Class componentClass, + final BundleCoordinate bundleCoordinate) throws InstantiationException, IllegalAccessException, IOException, InitializationException { // use temp components from ExtensionManager which should always be populated before doc generation final String classType = componentClass.getCanonicalName(); - final ConfigurableComponent component = ExtensionManager.getTempComponent(classType, bundleCoordinate); + final ConfigurableComponent component = extensionManager.getTempComponent(classType, bundleCoordinate); - final DocumentationWriter writer = getDocumentWriter(componentClass); + final DocumentationWriter writer = getDocumentWriter(extensionManager, componentClass); final File baseDocumentationFile = new File(componentDocsDir, "index.html"); if (baseDocumentationFile.exists()) { @@ -138,13 +141,14 @@ private static void document(final File componentDocsDir, final Class componentClass) { + private static DocumentationWriter getDocumentWriter(final ExtensionManager extensionManager, + final Class componentClass) { if (Processor.class.isAssignableFrom(componentClass)) { - return new HtmlProcessorDocumentationWriter(); + return new HtmlProcessorDocumentationWriter(extensionManager); } else if (ControllerService.class.isAssignableFrom(componentClass)) { - return new HtmlDocumentationWriter(); + return new HtmlDocumentationWriter(extensionManager); } else if (ReportingTask.class.isAssignableFrom(componentClass)) { - return new HtmlDocumentationWriter(); + return new HtmlDocumentationWriter(extensionManager); } return null; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java index fb2c5a04a5a8..1b401f999af5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java @@ -67,6 +67,12 @@ public class HtmlDocumentationWriter implements DocumentationWriter { */ public static final String ADDITIONAL_DETAILS_HTML = "additionalDetails.html"; + private final ExtensionManager extensionManager; + + public HtmlDocumentationWriter(final ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + } + @Override public void write(final ConfigurableComponent configurableComponent, final OutputStream streamToWriteTo, final boolean includesAdditionalDocumentation) throws IOException { @@ -848,7 +854,7 @@ private List> lookupControllerServiceImpls( final List> implementations = new ArrayList<>(); // first get all ControllerService implementations - final Set controllerServices = ExtensionManager.getExtensions(ControllerService.class); + final Set controllerServices = extensionManager.getExtensions(ControllerService.class); // then iterate over all controller services looking for any that is a child of the parent // ControllerService API that was passed in as a parameter @@ -891,7 +897,7 @@ protected void iterateAndLinkComponents(final XMLStreamWriter xmlStreamWriter, f int index = 0; for (final Class linkedComponent : linkedComponents ) { final String linkedComponentName = linkedComponent.getName(); - final List linkedComponentBundles = ExtensionManager.getBundles(linkedComponentName); + final List linkedComponentBundles = extensionManager.getBundles(linkedComponentName); if (linkedComponentBundles != null && linkedComponentBundles.size() > 0) { final Bundle firstLinkedComponentBundle = linkedComponentBundles.get(0); final BundleCoordinate coordinate = firstLinkedComponentBundle.getBundleDetails().getCoordinate(); @@ -927,7 +933,7 @@ protected void iterateAndLinkComponents(final XMLStreamWriter xmlStreamWriter, f } } - final List linkedComponentBundles = ExtensionManager.getBundles(className); + final List linkedComponentBundles = extensionManager.getBundles(className); if (linkedComponentBundles != null && linkedComponentBundles.size() > 0) { final Bundle firstBundle = linkedComponentBundles.get(0); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java index 6ac34a2f90f5..51922221c9d3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java @@ -29,6 +29,7 @@ import org.apache.nifi.annotation.behavior.WritesAttribute; import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.components.ConfigurableComponent; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.Relationship; @@ -40,6 +41,10 @@ */ public class HtmlProcessorDocumentationWriter extends HtmlDocumentationWriter { + public HtmlProcessorDocumentationWriter(ExtensionManager extensionManager) { + super(extensionManager); + } + @Override protected void writeAdditionalBodyInfo(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java index e3b3ffd86ec2..3e93427e669e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java @@ -16,27 +16,29 @@ */ package org.apache.nifi.documentation; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Properties; -import java.util.Set; import org.apache.commons.io.FileUtils; import org.apache.nifi.bundle.Bundle; import org.apache.nifi.bundle.BundleCoordinate; -import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.ExtensionDiscoveringManager; import org.apache.nifi.nar.ExtensionMapping; -import org.apache.nifi.nar.NarClassLoaders; +import org.apache.nifi.nar.NarClassLoadersHolder; import org.apache.nifi.nar.NarUnpacker; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.nar.SystemBundle; import org.apache.nifi.util.NiFiProperties; import org.junit.Assert; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Properties; +import java.util.Set; + public class DocGeneratorTest { @Test @@ -51,11 +53,12 @@ public void testProcessorLoadsNarResources() throws IOException, ClassNotFoundEx final Bundle systemBundle = SystemBundle.create(properties); final ExtensionMapping mapping = NarUnpacker.unpackNars(properties, systemBundle); - NarClassLoaders.getInstance().init(properties.getFrameworkWorkingDirectory(), properties.getExtensionsWorkingDirectory()); + NarClassLoadersHolder.getInstance().init(properties.getFrameworkWorkingDirectory(), properties.getExtensionsWorkingDirectory()); - ExtensionManager.discoverExtensions(systemBundle, NarClassLoaders.getInstance().getBundles()); + final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager(); + extensionManager.discoverExtensions(systemBundle, NarClassLoadersHolder.getInstance().getBundles()); - DocGenerator.generate(properties, mapping); + DocGenerator.generate(properties, extensionManager, mapping); final String extensionClassName = "org.apache.nifi.processors.WriteResourceToStream"; final BundleCoordinate coordinate = mapping.getProcessorNames().get(extensionClassName).stream().findFirst().get(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlDocumentationWriterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlDocumentationWriterTest.java index 84b8b1f8bee6..c37ebeb36b69 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlDocumentationWriterTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlDocumentationWriterTest.java @@ -16,8 +16,8 @@ */ package org.apache.nifi.documentation.html; -import org.apache.nifi.annotation.behavior.SystemResourceConsideration; import org.apache.nifi.annotation.behavior.SystemResource; +import org.apache.nifi.annotation.behavior.SystemResourceConsideration; import org.apache.nifi.controller.ControllerService; import org.apache.nifi.documentation.DocumentationWriter; import org.apache.nifi.documentation.example.ControllerServiceWithLogger; @@ -28,9 +28,12 @@ import org.apache.nifi.init.ReportingTaskingInitializer; import org.apache.nifi.mock.MockControllerServiceInitializationContext; import org.apache.nifi.mock.MockReportingInitializationContext; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.reporting.ReportingTask; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import java.io.ByteArrayOutputStream; @@ -41,6 +44,13 @@ public class HtmlDocumentationWriterTest { + private ExtensionManager extensionManager; + + @Before + public void setup() { + extensionManager = new StandardExtensionDiscoveringManager(); + } + @Test public void testJoin() { assertEquals("a, b, c", HtmlDocumentationWriter.join(new String[] { "a", "b", "c" }, ", ")); @@ -52,10 +62,10 @@ public void testJoin() { public void testDocumentControllerService() throws InitializationException, IOException { FullyDocumentedControllerService controllerService = new FullyDocumentedControllerService(); - ControllerServiceInitializer initializer = new ControllerServiceInitializer(); + ControllerServiceInitializer initializer = new ControllerServiceInitializer(extensionManager); initializer.initialize(controllerService); - DocumentationWriter writer = new HtmlDocumentationWriter(); + DocumentationWriter writer = new HtmlDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -102,10 +112,10 @@ public void testDocumentControllerService() throws InitializationException, IOEx public void testDocumentReportingTask() throws InitializationException, IOException { FullyDocumentedReportingTask reportingTask = new FullyDocumentedReportingTask(); - ReportingTaskingInitializer initializer = new ReportingTaskingInitializer(); + ReportingTaskingInitializer initializer = new ReportingTaskingInitializer(extensionManager); initializer.initialize(reportingTask); - DocumentationWriter writer = new HtmlDocumentationWriter(); + DocumentationWriter writer = new HtmlDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -152,7 +162,7 @@ public void testControllerServiceWithLogger() throws InitializationException, IO ControllerService controllerService = new ControllerServiceWithLogger(); controllerService.initialize(new MockControllerServiceInitializationContext()); - DocumentationWriter writer = new HtmlDocumentationWriter(); + DocumentationWriter writer = new HtmlDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -168,7 +178,7 @@ public void testReportingTaskWithLogger() throws InitializationException, IOExce ReportingTask controllerService = new ReportingTaskWithLogger(); controllerService.initialize(new MockReportingInitializationContext()); - DocumentationWriter writer = new HtmlDocumentationWriter(); + DocumentationWriter writer = new HtmlDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java index 4e45b7bab0d1..573cd0c41cc1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java @@ -16,12 +16,6 @@ */ package org.apache.nifi.documentation.html; -import static org.apache.nifi.documentation.html.XmlValidator.assertContains; -import static org.apache.nifi.documentation.html.XmlValidator.assertNotContains; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import org.apache.nifi.annotation.behavior.SystemResource; import org.apache.nifi.annotation.behavior.SystemResourceConsideration; import org.apache.nifi.annotation.documentation.CapabilityDescription; @@ -32,18 +26,28 @@ import org.apache.nifi.documentation.example.NakedProcessor; import org.apache.nifi.documentation.example.ProcessorWithLogger; import org.apache.nifi.init.ProcessorInitializer; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.junit.Assert; import org.junit.Test; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.apache.nifi.documentation.html.XmlValidator.assertContains; +import static org.apache.nifi.documentation.html.XmlValidator.assertNotContains; + public class ProcessorDocumentationWriterTest { @Test public void testFullyDocumentedProcessor() throws IOException { + ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + FullyDocumentedProcessor processor = new FullyDocumentedProcessor(); - ProcessorInitializer initializer = new ProcessorInitializer(); + ProcessorInitializer initializer = new ProcessorInitializer(extensionManager); initializer.initialize(processor); - DocumentationWriter writer = new HtmlProcessorDocumentationWriter(); + DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -114,11 +118,13 @@ public void testFullyDocumentedProcessor() throws IOException { @Test public void testNakedProcessor() throws IOException { + ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + NakedProcessor processor = new NakedProcessor(); - ProcessorInitializer initializer = new ProcessorInitializer(); + ProcessorInitializer initializer = new ProcessorInitializer(extensionManager); initializer.initialize(processor); - DocumentationWriter writer = new HtmlProcessorDocumentationWriter(); + DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -152,11 +158,13 @@ public void testNakedProcessor() throws IOException { @Test public void testProcessorWithLoggerInitialization() throws IOException { + ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + ProcessorWithLogger processor = new ProcessorWithLogger(); - ProcessorInitializer initializer = new ProcessorInitializer(); + ProcessorInitializer initializer = new ProcessorInitializer(extensionManager); initializer.initialize(processor); - DocumentationWriter writer = new HtmlProcessorDocumentationWriter(); + DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -170,11 +178,13 @@ public void testProcessorWithLoggerInitialization() throws IOException { @Test public void testDeprecatedProcessor() throws IOException { + ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + DeprecatedProcessor processor = new DeprecatedProcessor(); - ProcessorInitializer initializer = new ProcessorInitializer(); + ProcessorInitializer initializer = new ProcessorInitializer(extensionManager); initializer.initialize(processor); - DocumentationWriter writer = new HtmlProcessorDocumentationWriter(); + DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager); ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/resources/conf/nifi.properties index 38cbd91d3ae1..a768adca2f3b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/resources/conf/nifi.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/resources/conf/nifi.properties @@ -82,7 +82,6 @@ nifi.security.keyPasswd= nifi.security.truststore= nifi.security.truststoreType= nifi.security.truststorePasswd= -nifi.security.needClientAuth= nifi.security.user.authorizer= # cluster common properties (cluster manager and nodes must have same values) # diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml index 554875d553ff..b4289162b11b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-file-authorizer diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/pom.xml index 1b3b28925929..a1a6fc547503 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/pom.xml @@ -14,7 +14,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-flowfile-repo-serialization @@ -30,17 +30,17 @@ org.apache.nifi nifi-repository-models - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-write-ahead-log - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-schema-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/main/java/org/apache/nifi/controller/repository/SchemaRepositoryRecordSerde.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/main/java/org/apache/nifi/controller/repository/SchemaRepositoryRecordSerde.java index 970d45e6f9b5..0013846af9e7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/main/java/org/apache/nifi/controller/repository/SchemaRepositoryRecordSerde.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/main/java/org/apache/nifi/controller/repository/SchemaRepositoryRecordSerde.java @@ -17,12 +17,6 @@ package org.apache.nifi.controller.repository; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.util.Map; - import org.apache.nifi.controller.queue.FlowFileQueue; import org.apache.nifi.controller.repository.claim.ContentClaim; import org.apache.nifi.controller.repository.claim.ResourceClaimManager; @@ -34,6 +28,7 @@ import org.apache.nifi.controller.repository.schema.RepositoryRecordUpdate; import org.apache.nifi.repository.schema.FieldType; import org.apache.nifi.repository.schema.Record; +import org.apache.nifi.repository.schema.RecordIterator; import org.apache.nifi.repository.schema.RecordSchema; import org.apache.nifi.repository.schema.Repetition; import org.apache.nifi.repository.schema.SchemaRecordReader; @@ -43,6 +38,13 @@ import org.slf4j.LoggerFactory; import org.wali.SerDe; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.util.Map; + public class SchemaRepositoryRecordSerde extends RepositoryRecordSerde implements SerDe { private static final Logger logger = LoggerFactory.getLogger(SchemaRepositoryRecordSerde.class); private static final int MAX_ENCODING_VERSION = 2; @@ -51,7 +53,8 @@ public class SchemaRepositoryRecordSerde extends RepositoryRecordSerde implement private final RecordSchema contentClaimSchema = ContentClaimSchema.CONTENT_CLAIM_SCHEMA_V1; private final ResourceClaimManager resourceClaimManager; - private volatile RecordSchema recoverySchema; + private volatile SchemaRecordReader reader; + private RecordIterator recordIterator = null; public SchemaRepositoryRecordSerde(final ResourceClaimManager resourceClaimManager) { this.resourceClaimManager = resourceClaimManager; @@ -101,7 +104,8 @@ protected void serializeRecord(final RepositoryRecord record, final DataOutputSt @Override public void readHeader(final DataInputStream in) throws IOException { - recoverySchema = RecordSchema.readFrom(in); + final RecordSchema recoverySchema = RecordSchema.readFrom(in); + reader = SchemaRecordReader.fromSchema(recoverySchema); } @Override @@ -120,8 +124,41 @@ public RepositoryRecord deserializeEdit(final DataInputStream in, final Map org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-authorization @@ -51,7 +51,7 @@ org.apache.nifi nifi-mock-authorizer - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml index 38352e31fae0..40a3cb2d0106 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-cluster-protocol jar @@ -36,12 +36,12 @@ org.apache.nifi nifi-logging-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-socket-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterCoordinator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterCoordinator.java index 11786c27c037..fad51d1c4ad8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterCoordinator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterCoordinator.java @@ -17,6 +17,7 @@ package org.apache.nifi.cluster.coordination; +import org.apache.nifi.cluster.coordination.node.OffloadCode; import org.apache.nifi.cluster.coordination.node.DisconnectionCode; import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; @@ -61,6 +62,30 @@ public interface ClusterCoordinator { */ void finishNodeConnection(NodeIdentifier nodeId); + /** + * Indicates that the node has finished being offloaded + * + * @param nodeId the identifier of the node + */ + void finishNodeOffload(NodeIdentifier nodeId); + + /** + * Sends a request to the node to be offloaded. + * The node will be marked as offloading immediately. + *

+ * When a node is offloaded: + *

    + *
  • all processors on the node are stopped
  • + *
  • all processors on the node are terminated
  • + *
  • all remote process groups on the node stop transmitting
  • + *
  • all flowfiles on the node are sent to other nodes in the cluster
  • + *
+ * @param nodeId the identifier of the node + * @param offloadCode the code that represents why this node is being asked to be offloaded + * @param explanation an explanation as to why the node is being asked to be offloaded + */ + void requestNodeOffload(NodeIdentifier nodeId, OffloadCode offloadCode, String explanation); + /** * Sends a request to the node to disconnect from the cluster. * The node will be marked as disconnected immediately. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterTopologyEventListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterTopologyEventListener.java index 54cc4de1179d..d31339b91a32 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterTopologyEventListener.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/ClusterTopologyEventListener.java @@ -17,6 +17,7 @@ package org.apache.nifi.cluster.coordination; +import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.protocol.NodeIdentifier; public interface ClusterTopologyEventListener { @@ -26,4 +27,6 @@ public interface ClusterTopologyEventListener { void onNodeRemoved(NodeIdentifier nodeId); void onLocalNodeIdentifierSet(NodeIdentifier localNodeId); + + void onNodeStateChange(NodeIdentifier nodeId, NodeConnectionState newState); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionState.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionState.java index 8d5824f17103..d79552c8cdb8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionState.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionState.java @@ -36,12 +36,22 @@ public enum NodeConnectionState { */ CONNECTED, + /** + * A node that is in the process of offloading its flow files from the node. + */ + OFFLOADING, + /** * A node that is in the process of disconnecting from the cluster. * A DISCONNECTING node will always transition to DISCONNECTED. */ DISCONNECTING, + /** + * A node that has offloaded its flow files from the node. + */ + OFFLOADED, + /** * A node that is not connected to the cluster. * A DISCONNECTED node can transition to CONNECTING. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionStatus.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionStatus.java index 34bd1279e3e7..7d8a94049cc5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionStatus.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/NodeConnectionStatus.java @@ -35,47 +35,53 @@ public class NodeConnectionStatus { private final long updateId; private final NodeIdentifier nodeId; private final NodeConnectionState state; + private final OffloadCode offloadCode; private final DisconnectionCode disconnectCode; - private final String disconnectReason; + private final String reason; private final Long connectionRequestTime; public NodeConnectionStatus(final NodeIdentifier nodeId, final NodeConnectionState state) { - this(nodeId, state, null, null, null); + this(nodeId, state, null, null, null, null); } public NodeConnectionStatus(final NodeIdentifier nodeId, final DisconnectionCode disconnectionCode) { - this(nodeId, NodeConnectionState.DISCONNECTED, disconnectionCode, disconnectionCode.toString(), null); + this(nodeId, NodeConnectionState.DISCONNECTED, null, disconnectionCode, disconnectionCode.toString(), null); + } + + public NodeConnectionStatus(final NodeIdentifier nodeId, final NodeConnectionState state, final OffloadCode offloadCode, final String offloadExplanation) { + this(nodeId, state, offloadCode, null, offloadExplanation, null); } public NodeConnectionStatus(final NodeIdentifier nodeId, final DisconnectionCode disconnectionCode, final String disconnectionExplanation) { - this(nodeId, NodeConnectionState.DISCONNECTED, disconnectionCode, disconnectionExplanation, null); + this(nodeId, NodeConnectionState.DISCONNECTED, null, disconnectionCode, disconnectionExplanation, null); } public NodeConnectionStatus(final NodeIdentifier nodeId, final NodeConnectionState state, final DisconnectionCode disconnectionCode) { - this(nodeId, state, disconnectionCode, disconnectionCode == null ? null : disconnectionCode.toString(), null); + this(nodeId, state, null, disconnectionCode, disconnectionCode == null ? null : disconnectionCode.toString(), null); } public NodeConnectionStatus(final NodeConnectionStatus status) { - this(status.getNodeIdentifier(), status.getState(), status.getDisconnectCode(), status.getDisconnectReason(), status.getConnectionRequestTime()); + this(status.getNodeIdentifier(), status.getState(), status.getOffloadCode(), status.getDisconnectCode(), status.getReason(), status.getConnectionRequestTime()); } - public NodeConnectionStatus(final NodeIdentifier nodeId, final NodeConnectionState state, final DisconnectionCode disconnectCode, - final String disconnectReason, final Long connectionRequestTime) { - this(idGenerator.getAndIncrement(), nodeId, state, disconnectCode, disconnectReason, connectionRequestTime); + public NodeConnectionStatus(final NodeIdentifier nodeId, final NodeConnectionState state, final OffloadCode offloadCode, + final DisconnectionCode disconnectCode, final String reason, final Long connectionRequestTime) { + this(idGenerator.getAndIncrement(), nodeId, state, offloadCode, disconnectCode, reason, connectionRequestTime); } - public NodeConnectionStatus(final long updateId, final NodeIdentifier nodeId, final NodeConnectionState state, final DisconnectionCode disconnectCode, - final String disconnectReason, final Long connectionRequestTime) { + public NodeConnectionStatus(final long updateId, final NodeIdentifier nodeId, final NodeConnectionState state, final OffloadCode offloadCode, + final DisconnectionCode disconnectCode, final String reason, final Long connectionRequestTime) { this.updateId = updateId; this.nodeId = nodeId; this.state = state; + this.offloadCode = offloadCode; if (state == NodeConnectionState.DISCONNECTED && disconnectCode == null) { this.disconnectCode = DisconnectionCode.UNKNOWN; - this.disconnectReason = this.disconnectCode.toString(); + this.reason = this.disconnectCode.toString(); } else { this.disconnectCode = disconnectCode; - this.disconnectReason = disconnectReason; + this.reason = reason; } this.connectionRequestTime = (connectionRequestTime == null && state == NodeConnectionState.CONNECTING) ? Long.valueOf(System.currentTimeMillis()) : connectionRequestTime; @@ -93,12 +99,16 @@ public NodeConnectionState getState() { return state; } + public OffloadCode getOffloadCode() { + return offloadCode; + } + public DisconnectionCode getDisconnectCode() { return disconnectCode; } - public String getDisconnectReason() { - return disconnectReason; + public String getReason() { + return reason; } public Long getConnectionRequestTime() { @@ -110,8 +120,11 @@ public String toString() { final StringBuilder sb = new StringBuilder(); final NodeConnectionState state = getState(); sb.append("NodeConnectionStatus[nodeId=").append(nodeId).append(", state=").append(state); + if (state == NodeConnectionState.OFFLOADED || state == NodeConnectionState.OFFLOADING) { + sb.append(", Offload Code=").append(getOffloadCode()).append(", Offload Reason=").append(getReason()); + } if (state == NodeConnectionState.DISCONNECTED || state == NodeConnectionState.DISCONNECTING) { - sb.append(", Disconnect Code=").append(getDisconnectCode()).append(", Disconnect Reason=").append(getDisconnectReason()); + sb.append(", Disconnect Code=").append(getDisconnectCode()).append(", Disconnect Reason=").append(getReason()); } sb.append(", updateId=").append(getUpdateIdentifier()); sb.append("]"); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/OffloadCode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/OffloadCode.java new file mode 100644 index 000000000000..fb4d30bbc5f0 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/coordination/node/OffloadCode.java @@ -0,0 +1,40 @@ +/* + * 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.cluster.coordination.node; + +/** + * An enumeration of the reasons that a node may be offloaded + */ +public enum OffloadCode { + + /** + * A user explicitly offloaded the node + */ + OFFLOADED("Node Offloaded"); + + private final String description; + + OffloadCode(final String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/ClusterCoordinationProtocolSender.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/ClusterCoordinationProtocolSender.java index 986231efd466..b5485ccd5652 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/ClusterCoordinationProtocolSender.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/ClusterCoordinationProtocolSender.java @@ -19,6 +19,7 @@ import java.util.Set; import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.NodeStatusChangeMessage; import org.apache.nifi.cluster.protocol.message.ReconnectionRequestMessage; @@ -40,6 +41,14 @@ public interface ClusterCoordinationProtocolSender { */ ReconnectionResponseMessage requestReconnection(ReconnectionRequestMessage msg) throws ProtocolException; + /** + * Sends an "offload request" message to a node. + * + * @param msg a message + * @throws ProtocolException if communication failed + */ + void offload(OffloadMessage msg) throws ProtocolException; + /** * Sends a "disconnection request" message to a node. * diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/NodeIdentifier.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/NodeIdentifier.java index b17ec2bc9de4..74c5538414f1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/NodeIdentifier.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/NodeIdentifier.java @@ -17,12 +17,14 @@ package org.apache.nifi.cluster.protocol; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.util.NiFiProperties; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -106,7 +108,8 @@ public class NodeIdentifier { public NodeIdentifier(final String id, final String apiAddress, final int apiPort, final String socketAddress, final int socketPort, final String siteToSiteAddress, final Integer siteToSitePort, final Integer siteToSiteHttpApiPort, final boolean siteToSiteSecure) { - this(id, apiAddress, apiPort, socketAddress, socketPort, socketAddress, 6342, siteToSiteAddress, siteToSitePort, siteToSiteHttpApiPort, siteToSiteSecure, null); + this(id, apiAddress, apiPort, socketAddress, socketPort, socketAddress, NiFiProperties.DEFAULT_LOAD_BALANCE_PORT, siteToSiteAddress, siteToSitePort, siteToSiteHttpApiPort, siteToSiteSecure, + null); } public NodeIdentifier(final String id, final String apiAddress, final int apiPort, final String socketAddress, final int socketPort, final String loadBalanceAddress, final int loadBalancePort, @@ -254,24 +257,21 @@ public boolean logicallyEquals(final NodeIdentifier other) { if (other == null) { return false; } - if ((this.apiAddress == null) ? (other.apiAddress != null) : !this.apiAddress.equals(other.apiAddress)) { + if (other == this) { + return true; + } + if (!Objects.equals(apiAddress, other.apiAddress)) { return false; } if (this.apiPort != other.apiPort) { return false; } - if ((this.socketAddress == null) ? (other.socketAddress != null) : !this.socketAddress.equals(other.socketAddress)) { + if (!Objects.equals(socketAddress, other.socketAddress)) { return false; } if (this.socketPort != other.socketPort) { return false; } - if (!this.loadBalanceAddress.equals(other.loadBalanceAddress)) { - return false; - } - if (this.loadBalancePort != other.loadBalancePort) { - return false; - } return true; } @@ -288,4 +288,10 @@ public String toString() { return apiAddress + ":" + apiPort; } + public String getFullDescription() { + return "NodeIdentifier[UUID=" + id + ", API Address = " + apiAddress + ":" + apiPort + ", Cluster Socket Address = " + socketAddress + ":" + socketPort + + ", Load Balance Address = " + loadBalanceAddress + ":" + loadBalancePort + ", Site-to-Site Raw Address = " + siteToSiteAddress + ":" + siteToSitePort + + ", Site-to-Site HTTP Address = " + apiAddress + ":" + siteToSiteHttpApiPort + ", Site-to-Site Secure = " + siteToSiteSecure + ", Node Identities = " + nodeIdentities + "]"; + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/ClusterCoordinationProtocolSenderListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/ClusterCoordinationProtocolSenderListener.java index ae3a0e50571e..74cc6b476a25 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/ClusterCoordinationProtocolSenderListener.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/ClusterCoordinationProtocolSenderListener.java @@ -26,6 +26,7 @@ import org.apache.nifi.cluster.protocol.ProtocolException; import org.apache.nifi.cluster.protocol.ProtocolHandler; import org.apache.nifi.cluster.protocol.ProtocolListener; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.NodeStatusChangeMessage; import org.apache.nifi.cluster.protocol.message.ReconnectionRequestMessage; @@ -100,6 +101,11 @@ public ReconnectionResponseMessage requestReconnection(final ReconnectionRequest return sender.requestReconnection(msg); } + @Override + public void offload(OffloadMessage msg) throws ProtocolException { + sender.offload(msg); + } + @Override public void disconnect(DisconnectMessage msg) throws ProtocolException { sender.disconnect(msg); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/SocketProtocolListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/SocketProtocolListener.java index 9eaffd37b96a..c588a6807dc2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/SocketProtocolListener.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/SocketProtocolListener.java @@ -24,6 +24,7 @@ import org.apache.nifi.cluster.protocol.ProtocolMessageMarshaller; import org.apache.nifi.cluster.protocol.ProtocolMessageUnmarshaller; import org.apache.nifi.cluster.protocol.message.ConnectionRequestMessage; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.FlowRequestMessage; import org.apache.nifi.cluster.protocol.message.HeartbeatMessage; @@ -210,6 +211,8 @@ private NodeIdentifier getNodeIdentifier(final ProtocolMessage message) { return ((ConnectionRequestMessage) message).getConnectionRequest().getProposedNodeIdentifier(); case HEARTBEAT: return ((HeartbeatMessage) message).getHeartbeat().getNodeIdentifier(); + case OFFLOAD_REQUEST: + return ((OffloadMessage) message).getNodeId(); case DISCONNECTION_REQUEST: return ((DisconnectMessage) message).getNodeId(); case FLOW_REQUEST: diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/StandardClusterCoordinationProtocolSender.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/StandardClusterCoordinationProtocolSender.java index 167ddec93284..b21068ffe5e2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/StandardClusterCoordinationProtocolSender.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/impl/StandardClusterCoordinationProtocolSender.java @@ -36,6 +36,7 @@ import org.apache.nifi.cluster.protocol.ProtocolException; import org.apache.nifi.cluster.protocol.ProtocolMessageMarshaller; import org.apache.nifi.cluster.protocol.ProtocolMessageUnmarshaller; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.NodeConnectionStatusRequestMessage; import org.apache.nifi.cluster.protocol.message.NodeConnectionStatusResponseMessage; @@ -128,6 +129,31 @@ public ReconnectionResponseMessage requestReconnection(final ReconnectionRequest } } + /** + * Requests a node to be offloaded. The configured value for + * handshake timeout is applied to the socket before making the request. + * + * @param msg a message + * @throws ProtocolException if the message failed to be sent + */ + @Override + public void offload(final OffloadMessage msg) throws ProtocolException { + Socket socket = null; + try { + socket = createSocket(msg.getNodeId(), true); + + // marshal message to output stream + try { + final ProtocolMessageMarshaller marshaller = protocolContext.createMarshaller(); + marshaller.marshal(msg, socket.getOutputStream()); + } catch (final IOException ioe) { + throw new ProtocolException("Failed marshalling '" + msg.getType() + "' protocol message due to: " + ioe, ioe); + } + } finally { + SocketUtils.closeQuietly(socket); + } + } + /** * Requests a node to disconnect from the cluster. The configured value for * handshake timeout is applied to the socket before making the request. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/AdaptedNodeConnectionStatus.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/AdaptedNodeConnectionStatus.java index c8c4acf646a3..5eae83e0e11b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/AdaptedNodeConnectionStatus.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/AdaptedNodeConnectionStatus.java @@ -17,6 +17,7 @@ package org.apache.nifi.cluster.protocol.jaxb.message; +import org.apache.nifi.cluster.coordination.node.OffloadCode; import org.apache.nifi.cluster.coordination.node.DisconnectionCode; import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.protocol.NodeIdentifier; @@ -25,8 +26,9 @@ public class AdaptedNodeConnectionStatus { private Long updateId; private NodeIdentifier nodeId; private NodeConnectionState state; + private OffloadCode offloadCode; private DisconnectionCode disconnectCode; - private String disconnectReason; + private String reason; private Long connectionRequestTime; public Long getUpdateId() { @@ -53,20 +55,28 @@ public void setState(NodeConnectionState state) { this.state = state; } + public OffloadCode getOffloadCode() { + return offloadCode; + } + public DisconnectionCode getDisconnectCode() { return disconnectCode; } + public void setOffloadCode(OffloadCode offloadCode) { + this.offloadCode = offloadCode; + } + public void setDisconnectCode(DisconnectionCode disconnectCode) { this.disconnectCode = disconnectCode; } - public String getDisconnectReason() { - return disconnectReason; + public String getReason() { + return reason; } - public void setDisconnectReason(String disconnectReason) { - this.disconnectReason = disconnectReason; + public void setReason(String reason) { + this.reason = reason; } public Long getConnectionRequestTime() { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/NodeConnectionStatusAdapter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/NodeConnectionStatusAdapter.java index ec209de1f540..47e92e8d2a34 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/NodeConnectionStatusAdapter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/NodeConnectionStatusAdapter.java @@ -28,8 +28,9 @@ public NodeConnectionStatus unmarshal(final AdaptedNodeConnectionStatus adapted) return new NodeConnectionStatus(adapted.getUpdateId(), adapted.getNodeId(), adapted.getState(), + adapted.getOffloadCode(), adapted.getDisconnectCode(), - adapted.getDisconnectReason(), + adapted.getReason(), adapted.getConnectionRequestTime()); } @@ -40,8 +41,9 @@ public AdaptedNodeConnectionStatus marshal(final NodeConnectionStatus toAdapt) t adapted.setUpdateId(toAdapt.getUpdateIdentifier()); adapted.setNodeId(toAdapt.getNodeIdentifier()); adapted.setConnectionRequestTime(toAdapt.getConnectionRequestTime()); + adapted.setOffloadCode(toAdapt.getOffloadCode()); adapted.setDisconnectCode(toAdapt.getDisconnectCode()); - adapted.setDisconnectReason(toAdapt.getDisconnectReason()); + adapted.setReason(toAdapt.getReason()); adapted.setState(toAdapt.getState()); } return adapted; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/ObjectFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/ObjectFactory.java index 9a594a403e8d..2f02e5e6fd5b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/ObjectFactory.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/jaxb/message/ObjectFactory.java @@ -20,6 +20,7 @@ import org.apache.nifi.cluster.protocol.message.ConnectionRequestMessage; import org.apache.nifi.cluster.protocol.message.ConnectionResponseMessage; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.FlowRequestMessage; import org.apache.nifi.cluster.protocol.message.FlowResponseMessage; @@ -52,6 +53,10 @@ public ReconnectionResponseMessage createReconnectionResponseMessage() { return new ReconnectionResponseMessage(); } + public OffloadMessage createDecomissionMessage() { + return new OffloadMessage(); + } + public DisconnectMessage createDisconnectionMessage() { return new DisconnectMessage(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/OffloadMessage.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/OffloadMessage.java new file mode 100644 index 000000000000..a7acd56ad60e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/OffloadMessage.java @@ -0,0 +1,53 @@ +/* + * 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.cluster.protocol.message; + +import org.apache.nifi.cluster.protocol.NodeIdentifier; +import org.apache.nifi.cluster.protocol.jaxb.message.NodeIdentifierAdapter; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +@XmlRootElement(name = "offloadMessage") +public class OffloadMessage extends ProtocolMessage { + + private NodeIdentifier nodeId; + private String explanation; + + @XmlJavaTypeAdapter(NodeIdentifierAdapter.class) + public NodeIdentifier getNodeId() { + return nodeId; + } + + public void setNodeId(NodeIdentifier nodeId) { + this.nodeId = nodeId; + } + + public String getExplanation() { + return explanation; + } + + public void setExplanation(String explanation) { + this.explanation = explanation; + } + + @Override + public MessageType getType() { + return MessageType.OFFLOAD_REQUEST; + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/ProtocolMessage.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/ProtocolMessage.java index 482f5d6bc012..fe26c7a2cc12 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/ProtocolMessage.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/message/ProtocolMessage.java @@ -21,6 +21,7 @@ public abstract class ProtocolMessage { public static enum MessageType { CONNECTION_REQUEST, CONNECTION_RESPONSE, + OFFLOAD_REQUEST, DISCONNECTION_REQUEST, EXCEPTION, FLOW_REQUEST, diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/spring/ServerSocketConfigurationFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/spring/ServerSocketConfigurationFactoryBean.java index ae4e70d5095e..1f38d8e7f1db 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/spring/ServerSocketConfigurationFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster-protocol/src/main/java/org/apache/nifi/cluster/protocol/spring/ServerSocketConfigurationFactoryBean.java @@ -36,7 +36,7 @@ public class ServerSocketConfigurationFactoryBean implements FactoryBean org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-cluster jar diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBean.java index 4ea42252d26a..9800d26fdc53 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBean.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.fingerprint.FingerprintFactory; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; @@ -29,6 +30,7 @@ public class PopularVoteFlowElectionFactoryBean implements FactoryBean { private static final Logger logger = LoggerFactory.getLogger(PopularVoteFlowElectionFactoryBean.class); private NiFiProperties properties; + private ExtensionManager extensionManager; @Override public PopularVoteFlowElection getObject() { @@ -47,7 +49,7 @@ public PopularVoteFlowElection getObject() { final String provider = properties.getProperty(NiFiProperties.SENSITIVE_PROPS_PROVIDER); final String password = properties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY); final StringEncryptor encryptor = StringEncryptor.createEncryptor(algorithm, provider, password); - final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor); + final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor, extensionManager); return new PopularVoteFlowElection(maxWaitMillis, TimeUnit.MILLISECONDS, maxNodes, fingerprintFactory); } @@ -64,4 +66,8 @@ public boolean isSingleton() { public void setProperties(final NiFiProperties properties) { this.properties = properties; } + + public void setExtensionManager(ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/heartbeat/AbstractHeartbeatMonitor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/heartbeat/AbstractHeartbeatMonitor.java index 43958833c241..5fbe3f8bfd3d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/heartbeat/AbstractHeartbeatMonitor.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/heartbeat/AbstractHeartbeatMonitor.java @@ -228,6 +228,14 @@ private void processHeartbeat(final NodeHeartbeat heartbeat) { return; } + if (NodeConnectionState.OFFLOADED == connectionState || NodeConnectionState.OFFLOADING == connectionState) { + // Cluster Coordinator can ignore this heartbeat since the node is offloaded + clusterCoordinator.reportEvent(nodeId, Severity.INFO, "Received heartbeat from node that is offloading " + + "or offloaded. Removing this heartbeat. Offloaded nodes will only be reconnected to the cluster by an " + + "explicit connection request or restarting the node."); + removeHeartbeat(nodeId); + } + if (NodeConnectionState.DISCONNECTED == connectionState) { // ignore heartbeats from nodes disconnected by means other than lack of heartbeat, unless it is // the only node. We allow it if it is the only node because if we have a one-node cluster, then @@ -249,7 +257,7 @@ private void processHeartbeat(final NodeHeartbeat heartbeat) { default: { // disconnected nodes should not heartbeat, so we need to issue a disconnection request. logger.info("Ignoring received heartbeat from disconnected node " + nodeId + ". Issuing disconnection request."); - clusterCoordinator.requestNodeDisconnect(nodeId, disconnectionCode, connectionStatus.getDisconnectReason()); + clusterCoordinator.requestNodeDisconnect(nodeId, disconnectionCode, connectionStatus.getReason()); removeHeartbeat(nodeId); break; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java index 65714f3df6b2..3d79a83e778c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerServiceEndpointMerger.java @@ -32,12 +32,15 @@ public class ControllerServiceEndpointMerger extends AbstractSingleEntityEndpoin public static final String CONTROLLER_CONTROLLER_SERVICES_URI = "/nifi-api/controller/controller-services"; public static final Pattern PROCESS_GROUPS_CONTROLLER_SERVICES_URI = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/controller-services"); public static final Pattern CONTROLLER_SERVICE_URI_PATTERN = Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}"); + public static final Pattern CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN = Pattern.compile("/nifi-api/controller-services/[a-f0-9\\-]{36}/run-status"); private final ControllerServiceEntityMerger controllerServiceEntityMerger = new ControllerServiceEntityMerger(); @Override public boolean canHandle(URI uri, String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && CONTROLLER_SERVICE_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; + } else if ("PUT".equalsIgnoreCase(method) && CONTROLLER_SERVICE_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && (CONTROLLER_CONTROLLER_SERVICES_URI.equals(uri.getPath()) || PROCESS_GROUPS_CONTROLLER_SERVICES_URI.matcher(uri.getPath()).matches())) { return true; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/PortEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/PortEndpointMerger.java index 33843ae162c6..3a873eaf042c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/PortEndpointMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/PortEndpointMerger.java @@ -31,9 +31,11 @@ public class PortEndpointMerger extends AbstractSingleEntityEndpoint implements EndpointResponseMerger { public static final Pattern INPUT_PORTS_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/input-ports"); public static final Pattern INPUT_PORT_URI_PATTERN = Pattern.compile("/nifi-api/input-ports/[a-f0-9\\-]{36}"); + public static final Pattern INPUT_PORT_RUN_STATUS_URI_PATTERN = Pattern.compile("/nifi-api/input-ports/[a-f0-9\\-]{36}/run-status"); public static final Pattern OUTPUT_PORTS_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/output-ports"); public static final Pattern OUTPUT_PORT_URI_PATTERN = Pattern.compile("/nifi-api/output-ports/[a-f0-9\\-]{36}"); + public static final Pattern OUTPUT_PORT_RUN_STATUS_URI_PATTERN = Pattern.compile("/nifi-api/output-ports/[a-f0-9\\-]{36}/run-status"); private final PortEntityMerger portEntityMerger = new PortEntityMerger(); @Override @@ -44,6 +46,8 @@ public boolean canHandle(final URI uri, final String method) { private boolean canHandleInputPort(final URI uri, final String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && (INPUT_PORT_URI_PATTERN.matcher(uri.getPath()).matches())) { return true; + } else if ("PUT".equalsIgnoreCase(method) && INPUT_PORT_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && INPUT_PORTS_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; } @@ -54,6 +58,8 @@ private boolean canHandleInputPort(final URI uri, final String method) { private boolean canHandleOutputPort(final URI uri, final String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && (OUTPUT_PORT_URI_PATTERN.matcher(uri.getPath()).matches())) { return true; + } else if ("PUT".equalsIgnoreCase(method) && OUTPUT_PORT_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && OUTPUT_PORTS_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ProcessorEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ProcessorEndpointMerger.java index 747cc12ebe20..0835afa280f0 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ProcessorEndpointMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ProcessorEndpointMerger.java @@ -31,12 +31,15 @@ public class ProcessorEndpointMerger extends AbstractSingleEntityEndpoint implements EndpointResponseMerger { public static final Pattern PROCESSORS_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/processors"); public static final Pattern PROCESSOR_URI_PATTERN = Pattern.compile("/nifi-api/processors/[a-f0-9\\-]{36}"); + public static final Pattern PROCESSOR_RUN_STATUS_URI_PATTERN = Pattern.compile("/nifi-api/processors/[a-f0-9\\-]{36}/run-status"); private final ProcessorEntityMerger processorEntityMerger = new ProcessorEntityMerger(); @Override public boolean canHandle(final URI uri, final String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && (PROCESSOR_URI_PATTERN.matcher(uri.getPath()).matches())) { return true; + } else if ("PUT".equalsIgnoreCase(method) && PROCESSOR_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && PROCESSORS_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RemoteProcessGroupEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RemoteProcessGroupEndpointMerger.java index e32c2533c422..9d06f524a08a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RemoteProcessGroupEndpointMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/RemoteProcessGroupEndpointMerger.java @@ -31,12 +31,15 @@ public class RemoteProcessGroupEndpointMerger extends AbstractSingleEntityEndpoint implements EndpointResponseMerger { public static final Pattern REMOTE_PROCESS_GROUPS_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/remote-process-groups"); public static final Pattern REMOTE_PROCESS_GROUP_URI_PATTERN = Pattern.compile("/nifi-api/remote-process-groups/[a-f0-9\\-]{36}"); + public static final Pattern REMOTE_PROCESS_RUN_STATUS_GROUP_URI_PATTERN = Pattern.compile("/nifi-api/remote-process-groups/[a-f0-9\\-]{36}/run-status"); private final RemoteProcessGroupEntityMerger remoteProcessGroupEntityMerger = new RemoteProcessGroupEntityMerger(); @Override public boolean canHandle(final URI uri, final String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && REMOTE_PROCESS_GROUP_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; + } else if ("PUT".equalsIgnoreCase(method) && REMOTE_PROCESS_RUN_STATUS_GROUP_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && REMOTE_PROCESS_GROUPS_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ReportingTaskEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ReportingTaskEndpointMerger.java index d17459d5bfd8..e0228740e2c4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ReportingTaskEndpointMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ReportingTaskEndpointMerger.java @@ -31,12 +31,15 @@ public class ReportingTaskEndpointMerger extends AbstractSingleEntityEndpoint implements EndpointResponseMerger { public static final String REPORTING_TASKS_URI = "/nifi-api/controller/reporting-tasks"; public static final Pattern REPORTING_TASK_URI_PATTERN = Pattern.compile("/nifi-api/reporting-tasks/[a-f0-9\\-]{36}"); + public static final Pattern REPORTING_TASK_RUN_STATUS_URI_PATTERN = Pattern.compile("/nifi-api/reporting-tasks/[a-f0-9\\-]{36}/run-status"); private final ReportingTaskEntityMerger reportingTaskEntityMerger = new ReportingTaskEntityMerger(); @Override public boolean canHandle(URI uri, String method) { if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && REPORTING_TASK_URI_PATTERN.matcher(uri.getPath()).matches()) { return true; + } else if ("PUT".equalsIgnoreCase(method) && REPORTING_TASK_RUN_STATUS_URI_PATTERN.matcher(uri.getPath()).matches()) { + return true; } else if ("POST".equalsIgnoreCase(method) && REPORTING_TASKS_URI.equals(uri.getPath())) { return true; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java index 85618deef709..b3a3ab965cf2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java @@ -31,6 +31,7 @@ import org.apache.nifi.cluster.manager.exception.DisconnectedNodeMutableRequestException; import org.apache.nifi.cluster.manager.exception.IllegalClusterStateException; import org.apache.nifi.cluster.manager.exception.NoConnectedNodesException; +import org.apache.nifi.cluster.manager.exception.OffloadedNodeMutableRequestException; import org.apache.nifi.cluster.manager.exception.UnknownNodeException; import org.apache.nifi.cluster.manager.exception.UriConstructionException; import org.apache.nifi.cluster.protocol.NodeIdentifier; @@ -170,6 +171,24 @@ public AsyncClusterResponse replicate(NiFiUser user, String method, URI uri, Obj // If the request is mutable, ensure that all nodes are connected. if (mutable) { + final List offloaded = stateMap.get(NodeConnectionState.OFFLOADED); + if (offloaded != null && !offloaded.isEmpty()) { + if (offloaded.size() == 1) { + throw new OffloadedNodeMutableRequestException("Node " + offloaded.iterator().next() + " is currently offloaded"); + } else { + throw new OffloadedNodeMutableRequestException(offloaded.size() + " Nodes are currently offloaded"); + } + } + + final List offloading = stateMap.get(NodeConnectionState.OFFLOADING); + if (offloading != null && !offloading.isEmpty()) { + if (offloading.size() == 1) { + throw new OffloadedNodeMutableRequestException("Node " + offloading.iterator().next() + " is currently offloading"); + } else { + throw new OffloadedNodeMutableRequestException(offloading.size() + " Nodes are currently offloading"); + } + } + final List disconnected = stateMap.get(NodeConnectionState.DISCONNECTED); if (disconnected != null && !disconnected.isEmpty()) { if (disconnected.size() == 1) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinator.java index 484d1556c11a..66eec26dec5a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinator.java @@ -34,6 +34,7 @@ import org.apache.nifi.cluster.exception.NoClusterCoordinatorException; import org.apache.nifi.cluster.firewall.ClusterNodeFirewall; import org.apache.nifi.cluster.manager.NodeResponse; +import org.apache.nifi.cluster.manager.exception.IllegalNodeOffloadException; import org.apache.nifi.cluster.manager.exception.IllegalNodeDisconnectionException; import org.apache.nifi.cluster.protocol.ComponentRevision; import org.apache.nifi.cluster.protocol.ConnectionRequest; @@ -49,6 +50,7 @@ import org.apache.nifi.cluster.protocol.message.ClusterWorkloadResponseMessage; import org.apache.nifi.cluster.protocol.message.ConnectionRequestMessage; import org.apache.nifi.cluster.protocol.message.ConnectionResponseMessage; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.NodeConnectionStatusResponseMessage; import org.apache.nifi.cluster.protocol.message.NodeStatusChangeMessage; @@ -62,6 +64,7 @@ import org.apache.nifi.controller.leader.election.LeaderElectionManager; import org.apache.nifi.controller.state.manager.StandardStateManagerProvider; import org.apache.nifi.events.EventReporter; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.reporting.Severity; import org.apache.nifi.services.FlowService; @@ -121,9 +124,9 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl public NodeClusterCoordinator(final ClusterCoordinationProtocolSenderListener senderListener, final EventReporter eventReporter, final LeaderElectionManager leaderElectionManager, final FlowElection flowElection, final ClusterNodeFirewall firewall, final RevisionManager revisionManager, final NiFiProperties nifiProperties, - final NodeProtocolSender nodeProtocolSender) throws IOException { + final ExtensionManager extensionManager, final NodeProtocolSender nodeProtocolSender) throws IOException { this(senderListener, eventReporter, leaderElectionManager, flowElection, firewall, revisionManager, nifiProperties, nodeProtocolSender, - StandardStateManagerProvider.create(nifiProperties, VariableRegistry.EMPTY_REGISTRY)); + StandardStateManagerProvider.create(nifiProperties, VariableRegistry.EMPTY_REGISTRY, extensionManager)); } public NodeClusterCoordinator(final ClusterCoordinationProtocolSenderListener senderListener, final EventReporter eventReporter, final LeaderElectionManager leaderElectionManager, @@ -174,8 +177,8 @@ private void recoverState() throws IOException { if (localNodeId == null) { localNodeId = nodeId; } else { - logger.warn("When recovering state, determined that tgwo Node Identifiers claim to be the local Node Identifier: {} and {}. Will ignore both of these and wait until " + - "connecting to cluster to determine which Node Identiifer is the local Node Identifier", localNodeId, nodeId); + logger.warn("When recovering state, determined that two Node Identifiers claim to be the local Node Identifier: {} and {}. Will ignore both of these and wait until " + + "connecting to cluster to determine which Node Identiifer is the local Node Identifier", localNodeId.getFullDescription(), nodeId.getFullDescription()); localNodeId = null; } } @@ -339,6 +342,8 @@ private NodeConnectionStatus updateNodeStatus(final NodeIdentifier nodeId, final final NodeConnectionStatus evictedStatus = nodeStatuses.put(nodeId, updatedStatus); if (evictedStatus == null) { onNodeAdded(nodeId, storeState); + } else { + onNodeStateChange(nodeId, updatedStatus.getState()); } return evictedStatus; @@ -357,6 +362,10 @@ private boolean updateNodeStatusConditionally(final NodeIdentifier nodeId, final updated = nodeStatuses.replace(nodeId, expectedStatus, updatedStatus); } + if (updated) { + onNodeStateChange(nodeId, updatedStatus.getState()); + } + return updated; } @@ -431,7 +440,7 @@ public void requestNodeConnect(final NodeIdentifier nodeId, final String userDn) reportEvent(nodeId, Severity.INFO, "Requesting that node connect to cluster on behalf of " + userDn); } - updateNodeStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTING, null, null, System.currentTimeMillis())); + updateNodeStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTING, null, null, null, System.currentTimeMillis())); // create the request final ReconnectionRequestMessage request = new ReconnectionRequestMessage(); @@ -469,6 +478,50 @@ public void finishNodeConnection(final NodeIdentifier nodeId) { updateNodeStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTED)); } + @Override + public void finishNodeOffload(final NodeIdentifier nodeId) { + final NodeConnectionState state = getConnectionState(nodeId); + if (state == null) { + logger.warn("Attempted to finish node offload for {} but node is not known.", nodeId); + return; + } + + if (state != NodeConnectionState.OFFLOADING) { + logger.warn("Attempted to finish node offload for {} but node is not in the offloading state, it is currently {}.", nodeId, state); + return; + } + + logger.info("{} is now offloaded", nodeId); + + updateNodeStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADED)); + } + + @Override + public void requestNodeOffload(final NodeIdentifier nodeId, final OffloadCode offloadCode, final String explanation) { + final Set offloadNodeIds = getNodeIdentifiers(NodeConnectionState.OFFLOADING, NodeConnectionState.OFFLOADED); + if (offloadNodeIds.contains(nodeId)) { + logger.debug("Attempted to offload node but the node is already offloading or offloaded"); + // no need to do anything here, the node is currently offloading or already offloaded + return; + } + + final Set disconnectedNodeIds = getNodeIdentifiers(NodeConnectionState.DISCONNECTED); + if (!disconnectedNodeIds.contains(nodeId)) { + throw new IllegalNodeOffloadException("Cannot offload node " + nodeId + " because it is not currently disconnected"); + } + + logger.info("Requesting that {} is offloaded due to {}", nodeId, explanation == null ? offloadCode : explanation); + + updateNodeStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADING, offloadCode, explanation)); + + final OffloadMessage request = new OffloadMessage(); + request.setNodeId(nodeId); + request.setExplanation(explanation); + + addNodeEvent(nodeId, "Offload requested due to " + explanation); + offloadAsynchronously(request, 10, 5); + } + @Override public void requestNodeDisconnect(final NodeIdentifier nodeId, final DisconnectionCode disconnectionCode, final String explanation) { final Set connectedNodeIds = getNodeIdentifiers(NodeConnectionState.CONNECTED); @@ -527,16 +580,18 @@ public void removeNode(final NodeIdentifier nodeId, final String userDn) { } private void onNodeRemoved(final NodeIdentifier nodeId) { - eventListeners.stream().forEach(listener -> listener.onNodeRemoved(nodeId)); + eventListeners.forEach(listener -> listener.onNodeRemoved(nodeId)); } private void onNodeAdded(final NodeIdentifier nodeId, final boolean storeState) { if (storeState) { storeState(); } + eventListeners.forEach(listener -> listener.onNodeAdded(nodeId)); + } - - eventListeners.stream().forEach(listener -> listener.onNodeAdded(nodeId)); + private void onNodeStateChange(final NodeIdentifier nodeId, final NodeConnectionState nodeConnectionState) { + eventListeners.forEach(listener -> listener.onNodeStateChange(nodeId, nodeConnectionState)); } @Override @@ -821,7 +876,7 @@ void notifyOthersOfNodeStatusChange(final NodeConnectionStatus updatedStatus, fi // Otherwise, get the active coordinator (or wait for one to become active) and then notify the coordinator. final Set nodesToNotify; if (notifyAllNodes) { - nodesToNotify = getNodeIdentifiers(NodeConnectionState.CONNECTED, NodeConnectionState.CONNECTING); + nodesToNotify = getNodeIdentifiers(); // Do not notify ourselves because we already know about the status update. nodesToNotify.remove(getLocalNodeIdentifier()); @@ -841,6 +896,34 @@ void notifyOthersOfNodeStatusChange(final NodeConnectionStatus updatedStatus, fi senderListener.notifyNodeStatusChange(nodesToNotify, message); } + private void offloadAsynchronously(final OffloadMessage request, final int attempts, final int retrySeconds) { + final Thread offloadThread = new Thread(new Runnable() { + @Override + public void run() { + final NodeIdentifier nodeId = request.getNodeId(); + + for (int i = 0; i < attempts; i++) { + try { + senderListener.offload(request); + reportEvent(nodeId, Severity.INFO, "Node was offloaded due to " + request.getExplanation()); + return; + } catch (final Exception e) { + logger.error("Failed to notify {} that it has been offloaded due to {}", request.getNodeId(), request.getExplanation(), e); + + try { + Thread.sleep(retrySeconds * 1000L); + } catch (final InterruptedException ie) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + }, "Offload " + request.getNodeId()); + + offloadThread.start(); + } + private void disconnectAsynchronously(final DisconnectMessage request, final int attempts, final int retrySeconds) { final Thread disconnectThread = new Thread(new Runnable() { @Override @@ -961,8 +1044,8 @@ private String summarizeStatusChange(final NodeConnectionStatus oldStatus, final if (oldStatus == null || status.getState() != oldStatus.getState()) { sb.append("Node Status changed from ").append(oldStatus == null ? "[Unknown Node]" : oldStatus.getState().toString()).append(" to ").append(status.getState().toString()); - if (status.getDisconnectReason() != null) { - sb.append(" due to ").append(status.getDisconnectReason()); + if (status.getReason() != null) { + sb.append(" due to ").append(status.getReason()); } else if (status.getDisconnectCode() != null) { sb.append(" due to ").append(status.getDisconnectCode().toString()); } @@ -1029,19 +1112,20 @@ private NodeIdentifier resolveNodeId(final NodeIdentifier proposedIdentifier) { if (existingStatus == null) { // there is no node with that ID resolvedNodeId = proposedIdentifier; - logger.debug("No existing node with ID {}; resolved node ID is as-proposed", proposedIdentifier.getId()); + logger.debug("No existing node with ID {}; resolved node ID is as-proposed", proposedIdentifier.getFullDescription()); onNodeAdded(resolvedNodeId, true); } else if (existingStatus.getNodeIdentifier().logicallyEquals(proposedIdentifier)) { // there is a node with that ID but it's the same node. resolvedNodeId = proposedIdentifier; - logger.debug("No existing node with ID {}; resolved node ID is as-proposed", proposedIdentifier.getId()); + logger.debug("A node already exists with ID {} and is logically equivalent; resolved node ID is as-proposed: {}", proposedIdentifier.getId(), proposedIdentifier.getFullDescription()); } else { // there is a node with that ID and it's a different node resolvedNodeId = new NodeIdentifier(UUID.randomUUID().toString(), proposedIdentifier.getApiAddress(), proposedIdentifier.getApiPort(), - proposedIdentifier.getSocketAddress(), proposedIdentifier.getSocketPort(), proposedIdentifier.getSiteToSiteAddress(), - proposedIdentifier.getSiteToSitePort(), proposedIdentifier.getSiteToSiteHttpApiPort(), proposedIdentifier.isSiteToSiteSecure()); + proposedIdentifier.getSocketAddress(), proposedIdentifier.getSocketPort(), proposedIdentifier.getLoadBalanceAddress(), proposedIdentifier.getLoadBalancePort(), + proposedIdentifier.getSiteToSiteAddress(), proposedIdentifier.getSiteToSitePort(), proposedIdentifier.getSiteToSiteHttpApiPort(), proposedIdentifier.isSiteToSiteSecure()); + logger.debug("A node already exists with ID {}. Proposed Node Identifier was {}; existing Node Identifier is {}; Resolved Node Identifier is {}", - proposedIdentifier.getId(), proposedIdentifier, getNodeIdentifier(proposedIdentifier.getId()), resolvedNodeId); + proposedIdentifier.getId(), proposedIdentifier.getFullDescription(), getNodeIdentifier(proposedIdentifier.getId()).getFullDescription(), resolvedNodeId.getFullDescription()); } return resolvedNodeId; @@ -1117,7 +1201,7 @@ private ConnectionResponseMessage createConnectionResponse(final ConnectionReque addNodeEvent(resolvedNodeIdentifier, "Connection requested from existing node. Setting status to connecting."); } - status = new NodeConnectionStatus(resolvedNodeIdentifier, NodeConnectionState.CONNECTING, null, null, System.currentTimeMillis()); + status = new NodeConnectionStatus(resolvedNodeIdentifier, NodeConnectionState.CONNECTING, null, null, null, System.currentTimeMillis()); updateNodeStatus(status); final ConnectionResponse response = new ConnectionResponse(resolvedNodeIdentifier, clusterDataFlow, instanceId, getConnectionStatuses(), diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java index b44f8d67410b..0043ca068522 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java @@ -61,6 +61,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -731,7 +732,7 @@ private static List mergeGarbageCollectionDiagn gcDiagnosticsDto.setMemoryManagerName(memoryManagerName); final List gcDiagnosticsSnapshots = new ArrayList<>(snapshotMap.values()); - Collections.sort(gcDiagnosticsSnapshots, (a, b) -> a.getTimestamp().compareTo(b.getTimestamp())); + gcDiagnosticsSnapshots.sort(Comparator.comparing(GCDiagnosticsSnapshotDTO::getTimestamp).reversed()); gcDiagnosticsDto.setSnapshots(gcDiagnosticsSnapshots); gcDiagnosticsDtos.add(gcDiagnosticsDto); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/IllegalNodeOffloadException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/IllegalNodeOffloadException.java new file mode 100644 index 000000000000..f1bc6694d0e5 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/IllegalNodeOffloadException.java @@ -0,0 +1,38 @@ +/* + * 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.cluster.manager.exception; + +/** + * Represents the exceptional case when an offload request is issued to a node that cannot be offloaded (e.g., not currently disconnected). + */ +public class IllegalNodeOffloadException extends IllegalClusterStateException { + + public IllegalNodeOffloadException() { + } + + public IllegalNodeOffloadException(String msg) { + super(msg); + } + + public IllegalNodeOffloadException(Throwable cause) { + super(cause); + } + + public IllegalNodeOffloadException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/OffloadedNodeMutableRequestException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/OffloadedNodeMutableRequestException.java new file mode 100644 index 000000000000..36633497357a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/exception/OffloadedNodeMutableRequestException.java @@ -0,0 +1,39 @@ +/* + * 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.cluster.manager.exception; + +/** + * Represents the exceptional case when a HTTP request that may change a node's dataflow is to be replicated while one or more nodes are offloaded. + * + */ +public class OffloadedNodeMutableRequestException extends MutableRequestException { + + public OffloadedNodeMutableRequestException() { + } + + public OffloadedNodeMutableRequestException(String msg) { + super(msg); + } + + public OffloadedNodeMutableRequestException(Throwable cause) { + super(cause); + } + + public OffloadedNodeMutableRequestException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/NodeClusterCoordinatorFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/NodeClusterCoordinatorFactoryBean.java index ac79a42d687c..4946d269760e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/NodeClusterCoordinatorFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/NodeClusterCoordinatorFactoryBean.java @@ -24,6 +24,7 @@ import org.apache.nifi.cluster.protocol.impl.ClusterCoordinationProtocolSenderListener; import org.apache.nifi.controller.leader.election.LeaderElectionManager; import org.apache.nifi.events.EventReporter; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.revision.RevisionManager; import org.springframework.beans.BeansException; @@ -34,6 +35,7 @@ public class NodeClusterCoordinatorFactoryBean implements FactoryBean, ApplicationContextAware { private ApplicationContext applicationContext; private NiFiProperties properties; + private ExtensionManager extensionManager; private NodeClusterCoordinator nodeClusterCoordinator = null; @@ -48,7 +50,8 @@ public NodeClusterCoordinator getObject() throws Exception { final LeaderElectionManager electionManager = applicationContext.getBean("leaderElectionManager", LeaderElectionManager.class); final FlowElection flowElection = applicationContext.getBean("flowElection", FlowElection.class); final NodeProtocolSender nodeProtocolSender = applicationContext.getBean("nodeProtocolSender", NodeProtocolSender.class); - nodeClusterCoordinator = new NodeClusterCoordinator(protocolSenderListener, eventReporter, electionManager, flowElection, clusterFirewall, revisionManager, properties, nodeProtocolSender); + nodeClusterCoordinator = new NodeClusterCoordinator(protocolSenderListener, eventReporter, electionManager, flowElection, clusterFirewall, + revisionManager, properties, extensionManager, nodeProtocolSender); } return nodeClusterCoordinator; @@ -73,4 +76,8 @@ public void setProperties(NiFiProperties properties) { this.properties = properties; } + public void setExtensionManager(ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/resources/nifi-cluster-manager-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/resources/nifi-cluster-manager-context.xml index d261590ef292..c1a76657911d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/resources/nifi-cluster-manager-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/resources/nifi-cluster-manager-context.xml @@ -43,11 +43,13 @@ + + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinatorSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinatorSpec.groovy new file mode 100644 index 000000000000..2751b3ead18e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/node/NodeClusterCoordinatorSpec.groovy @@ -0,0 +1,99 @@ +/* + * 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.cluster.coordination.node + +import org.apache.nifi.cluster.coordination.flow.FlowElection +import org.apache.nifi.cluster.firewall.ClusterNodeFirewall +import org.apache.nifi.cluster.protocol.NodeIdentifier +import org.apache.nifi.cluster.protocol.NodeProtocolSender +import org.apache.nifi.cluster.protocol.impl.ClusterCoordinationProtocolSenderListener +import org.apache.nifi.cluster.protocol.message.OffloadMessage +import org.apache.nifi.components.state.Scope +import org.apache.nifi.components.state.StateManager +import org.apache.nifi.components.state.StateManagerProvider +import org.apache.nifi.controller.leader.election.LeaderElectionManager +import org.apache.nifi.events.EventReporter +import org.apache.nifi.reporting.Severity +import org.apache.nifi.state.MockStateMap +import org.apache.nifi.util.NiFiProperties +import org.apache.nifi.web.revision.RevisionManager +import spock.lang.Specification +import spock.util.concurrent.BlockingVariable + +import java.util.concurrent.TimeUnit + +class NodeClusterCoordinatorSpec extends Specification { + def "requestNodeOffload"() { + given: 'mocked collaborators' + def clusterCoordinationProtocolSenderListener = Mock(ClusterCoordinationProtocolSenderListener) + def eventReporter = Mock EventReporter + def stateManager = Mock StateManager + def stateMap = new MockStateMap([:], 1) + stateManager.getState(_ as Scope) >> stateMap + def stateManagerProvider = Mock StateManagerProvider + stateManagerProvider.getStateManager(_ as String) >> stateManager + + and: 'a NodeClusterCoordinator that manages node status in a synchronized list' + List nodeStatuses = [].asSynchronized() + def clusterCoordinator = new NodeClusterCoordinator(clusterCoordinationProtocolSenderListener, eventReporter, Mock(LeaderElectionManager), + Mock(FlowElection), Mock(ClusterNodeFirewall), + Mock(RevisionManager), NiFiProperties.createBasicNiFiProperties('src/test/resources/conf/nifi.properties', [:]), + Mock(NodeProtocolSender), stateManagerProvider) { + @Override + void notifyOthersOfNodeStatusChange(NodeConnectionStatus updatedStatus, boolean notifyAllNodes, boolean waitForCoordinator) { + nodeStatuses.add(updatedStatus) + } + } + + and: 'two nodes' + def nodeIdentifier1 = createNodeIdentifier 1 + def nodeIdentifier2 = createNodeIdentifier 2 + + and: 'node 1 is connected, node 2 is disconnected' + clusterCoordinator.updateNodeStatus new NodeConnectionStatus(nodeIdentifier1, NodeConnectionState.CONNECTED) + clusterCoordinator.updateNodeStatus new NodeConnectionStatus(nodeIdentifier2, NodeConnectionState.DISCONNECTED) + while (nodeStatuses.size() < 2) { + Thread.sleep(10) + } + nodeStatuses.clear() + + def waitForReportEvent = new BlockingVariable(5, TimeUnit.SECONDS) + + when: 'a node is requested to offload' + clusterCoordinator.requestNodeOffload nodeIdentifier2, OffloadCode.OFFLOADED, 'unit test for offloading node' + waitForReportEvent.get() + + then: 'no exceptions are thrown' + noExceptionThrown() + + and: 'expected methods on collaborators are invoked' + 1 * clusterCoordinationProtocolSenderListener.offload({ OffloadMessage msg -> msg.nodeId == nodeIdentifier2 } as OffloadMessage) + 1 * eventReporter.reportEvent(Severity.INFO, 'Clustering', { msg -> msg.contains "$nodeIdentifier2.apiAddress:$nodeIdentifier2.apiPort" } as String) >> { + waitForReportEvent.set(it) + } + + and: 'the status of the offloaded node is known by the cluster coordinator to be offloading' + nodeStatuses[0].nodeIdentifier == nodeIdentifier2 + nodeStatuses[0].state == NodeConnectionState.OFFLOADING + } + + private static NodeIdentifier createNodeIdentifier(final int index) { + new NodeIdentifier("node-id-$index", "localhost", 8000 + index, "localhost", 9000 + index, + "localhost", 10000 + index, 11000 + index, false) + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/integration/OffloadNodeITSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/integration/OffloadNodeITSpec.groovy new file mode 100644 index 000000000000..a8dd15835150 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/integration/OffloadNodeITSpec.groovy @@ -0,0 +1,50 @@ +/* + * 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.cluster.integration + +import org.apache.nifi.cluster.coordination.node.DisconnectionCode +import org.apache.nifi.cluster.coordination.node.OffloadCode +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +class OffloadNodeITSpec extends Specification { + def "requestNodeOffload"() { + given: 'a cluster with 3 nodes' + System.setProperty 'nifi.properties.file.path', 'src/test/resources/conf/nifi.properties' + def cluster = new Cluster() + cluster.start() + cluster.createNode() + def nodeToOffload = cluster.createNode() + cluster.createNode() + cluster.waitUntilAllNodesConnected 20, TimeUnit.SECONDS + + when: 'the node to offload is disconnected successfully' + cluster.currentClusterCoordinator.clusterCoordinator.requestNodeDisconnect nodeToOffload.identifier, DisconnectionCode.USER_DISCONNECTED, + 'integration test user disconnect' + cluster.currentClusterCoordinator.assertNodeDisconnects nodeToOffload.identifier, 10, TimeUnit.SECONDS + + and: 'the node to offload is requested to offload' + nodeToOffload.getClusterCoordinator().requestNodeOffload nodeToOffload.identifier, OffloadCode.OFFLOADED, 'integration test offload' + + then: 'the node has been successfully offloaded' + cluster.currentClusterCoordinator.assertNodeIsOffloaded nodeToOffload.identifier, 10, TimeUnit.SECONDS + + cleanup: + cluster.stop() + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/flow/TestPopularVoteFlowElection.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/flow/TestPopularVoteFlowElection.java index 9c833b5d0efe..240fe4976ab4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/flow/TestPopularVoteFlowElection.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/flow/TestPopularVoteFlowElection.java @@ -37,6 +37,8 @@ import org.apache.nifi.cluster.protocol.StandardDataFlow; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.fingerprint.FingerprintFactory; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.util.NiFiProperties; import org.junit.Test; import org.mockito.Mockito; @@ -153,7 +155,8 @@ public void testEmptyFlowIgnoredIfNonEmptyFlowExists() throws IOException { @Test public void testAutoGeneratedVsPopulatedFlowElection() throws IOException { - final FingerprintFactory fingerprintFactory = new FingerprintFactory(createEncryptorFromProperties(getNiFiProperties())); + final ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + final FingerprintFactory fingerprintFactory = new FingerprintFactory(createEncryptorFromProperties(getNiFiProperties()), extensionManager); final PopularVoteFlowElection election = new PopularVoteFlowElection(1, TimeUnit.MINUTES, 4, fingerprintFactory); final byte[] emptyFlow = Files.readAllBytes(Paths.get("src/test/resources/conf/auto-generated-empty-flow.xml")); final byte[] nonEmptyFlow = Files.readAllBytes(Paths.get("src/test/resources/conf/reporting-task-flow.xml")); @@ -182,7 +185,8 @@ public void testAutoGeneratedVsPopulatedFlowElection() throws IOException { @Test public void testDifferentPopulatedFlowsElection() throws IOException { - final FingerprintFactory fingerprintFactory = new FingerprintFactory(createEncryptorFromProperties(getNiFiProperties())); + final ExtensionManager extensionManager = new StandardExtensionDiscoveringManager(); + final FingerprintFactory fingerprintFactory = new FingerprintFactory(createEncryptorFromProperties(getNiFiProperties()), extensionManager); final PopularVoteFlowElection election = new PopularVoteFlowElection(1, TimeUnit.MINUTES, 4, fingerprintFactory); final byte[] nonEmptyCandidateA = Files.readAllBytes(Paths.get("src/test/resources/conf/controller-service-flow.xml")); final byte[] nonEmptyCandidateB = Files.readAllBytes(Paths.get("src/test/resources/conf/reporting-task-flow.xml")); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/heartbeat/TestAbstractHeartbeatMonitor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/heartbeat/TestAbstractHeartbeatMonitor.java index 6ea019d9476a..4aeff7b3eb95 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/heartbeat/TestAbstractHeartbeatMonitor.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/heartbeat/TestAbstractHeartbeatMonitor.java @@ -20,6 +20,7 @@ import org.apache.nifi.cluster.ReportedEvent; import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.coordination.ClusterTopologyEventListener; +import org.apache.nifi.cluster.coordination.node.OffloadCode; import org.apache.nifi.cluster.coordination.node.DisconnectionCode; import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; @@ -244,6 +245,16 @@ public synchronized void finishNodeConnection(NodeIdentifier nodeId) { statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTED)); } + @Override + public synchronized void finishNodeOffload(NodeIdentifier nodeId) { + statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADED)); + } + + @Override + public synchronized void requestNodeOffload(NodeIdentifier nodeId, OffloadCode offloadCode, String explanation) { + statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADED)); + } + @Override public synchronized void requestNodeDisconnect(NodeIdentifier nodeId, DisconnectionCode disconnectionCode, String explanation) { statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.DISCONNECTED)); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/node/TestNodeClusterCoordinator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/node/TestNodeClusterCoordinator.java index fb06a15dec79..5ce2985c03d7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/node/TestNodeClusterCoordinator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/node/TestNodeClusterCoordinator.java @@ -280,7 +280,7 @@ public void testStatusChangesReplicated() throws InterruptedException, IOExcepti assertNotNull(statusChange); assertEquals(createNodeId(1), statusChange.getNodeIdentifier()); assertEquals(DisconnectionCode.NODE_SHUTDOWN, statusChange.getDisconnectCode()); - assertEquals("Unit Test", statusChange.getDisconnectReason()); + assertEquals("Unit Test", statusChange.getReason()); } @Test @@ -407,7 +407,7 @@ public void testUpdateNodeStatusOutOfOrder() throws InterruptedException { nodeStatuses.clear(); final NodeConnectionStatus oldStatus = new NodeConnectionStatus(-1L, nodeId1, NodeConnectionState.DISCONNECTED, - DisconnectionCode.BLOCKED_BY_FIREWALL, null, 0L); + null, DisconnectionCode.BLOCKED_BY_FIREWALL, null, 0L); final NodeStatusChangeMessage msg = new NodeStatusChangeMessage(); msg.setNodeId(nodeId1); msg.setNodeConnectionStatus(oldStatus); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Cluster.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Cluster.java index dab073da85d3..48c363991463 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Cluster.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Cluster.java @@ -17,13 +17,6 @@ package org.apache.nifi.cluster.integration; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; @@ -34,10 +27,20 @@ import org.apache.nifi.cluster.coordination.node.ClusterRoles; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.fingerprint.FingerprintFactory; +import org.apache.nifi.nar.ExtensionDiscoveringManager; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + public class Cluster { private static final Logger logger = LoggerFactory.getLogger(Cluster.class); @@ -134,16 +137,21 @@ public Node createNode() { final String provider = nifiProperties.getProperty(NiFiProperties.SENSITIVE_PROPS_PROVIDER); final String password = nifiProperties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY); final StringEncryptor encryptor = StringEncryptor.createEncryptor(algorithm, provider, password); - final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor); + final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager(); + final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor, extensionManager); final FlowElection flowElection = new PopularVoteFlowElection(flowElectionTimeoutMillis, TimeUnit.MILLISECONDS, flowElectionMaxNodes, fingerprintFactory); - final Node node = new Node(nifiProperties, flowElection); + final Node node = new Node(nifiProperties, extensionManager, flowElection); node.start(); nodes.add(node); return node; } + public Node getCurrentClusterCoordinator() { + return getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null); + } + public Node waitForClusterCoordinator(final long time, final TimeUnit timeUnit) { return ClusterUtils.waitUntilNonNull(time, timeUnit, () -> getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null)); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java index 3133736fbae0..ea181acf2305 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java @@ -51,6 +51,7 @@ import org.apache.nifi.events.EventReporter; import org.apache.nifi.io.socket.ServerSocketConfiguration; import org.apache.nifi.io.socket.SocketConfiguration; +import org.apache.nifi.nar.ExtensionDiscoveringManager; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.SystemBundle; import org.apache.nifi.registry.VariableRegistry; @@ -77,6 +78,7 @@ public class Node { private final NodeIdentifier nodeId; private final NiFiProperties nodeProperties; + private final ExtensionManager extensionManager; private final List reportedEvents = Collections.synchronizedList(new ArrayList()); private final RevisionManager revisionManager; @@ -95,11 +97,11 @@ public class Node { private ScheduledExecutorService executor = new FlowEngine(8, "Node tasks", true); - public Node(final NiFiProperties properties, final FlowElection flowElection) { - this(createNodeId(), properties, flowElection); + public Node(final NiFiProperties properties, final ExtensionDiscoveringManager extensionManager, final FlowElection flowElection) { + this(createNodeId(), properties, extensionManager, flowElection); } - public Node(final NodeIdentifier nodeId, final NiFiProperties properties, final FlowElection flowElection) { + public Node(final NodeIdentifier nodeId, final NiFiProperties properties, final ExtensionDiscoveringManager extensionManager, final FlowElection flowElection) { this.nodeId = nodeId; this.nodeProperties = new NiFiProperties() { @Override @@ -108,6 +110,8 @@ public String getProperty(String key) { return String.valueOf(nodeId.getSocketPort()); }else if(key.equals(NiFiProperties.WEB_HTTP_PORT)){ return String.valueOf(nodeId.getApiPort()); + }else if(key.equals(NiFiProperties.LOAD_BALANCE_PORT)){ + return String.valueOf(nodeId.getLoadBalancePort()); }else { return properties.getProperty(key); } @@ -123,7 +127,8 @@ public Set getPropertyKeys() { }; final Bundle systemBundle = SystemBundle.create(properties); - ExtensionManager.discoverExtensions(systemBundle, Collections.emptySet()); + extensionManager.discoverExtensions(systemBundle, Collections.emptySet()); + this.extensionManager = extensionManager; revisionManager = Mockito.mock(RevisionManager.class); Mockito.when(revisionManager.getAllRevisions()).thenReturn(Collections.emptyList()); @@ -161,7 +166,7 @@ public synchronized void start() { final HeartbeatMonitor heartbeatMonitor = createHeartbeatMonitor(); flowController = FlowController.createClusteredInstance(Mockito.mock(FlowFileEventRepository.class), nodeProperties, null, null, createEncryptorFromProperties(nodeProperties), protocolSender, Mockito.mock(BulletinRepository.class), clusterCoordinator, - heartbeatMonitor, electionManager, VariableRegistry.EMPTY_REGISTRY, Mockito.mock(FlowRegistryClient.class)); + heartbeatMonitor, electionManager, VariableRegistry.EMPTY_REGISTRY, Mockito.mock(FlowRegistryClient.class), extensionManager); try { flowController.initializeFlow(); @@ -299,7 +304,7 @@ public void reportEvent(Severity severity, String category, String message) { final ClusterCoordinationProtocolSenderListener protocolSenderListener = new ClusterCoordinationProtocolSenderListener(createCoordinatorProtocolSender(), protocolListener); try { return new NodeClusterCoordinator(protocolSenderListener, eventReporter, electionManager, flowElection, null, - revisionManager, nodeProperties, protocolSender); + revisionManager, nodeProperties, extensionManager, protocolSender); } catch (IOException e) { Assert.fail(e.toString()); return null; @@ -386,4 +391,17 @@ public void assertNodeDisconnects(final NodeIdentifier nodeId, final long time, public void assertNodeIsConnected(final NodeIdentifier nodeId) { Assert.assertEquals(NodeConnectionState.CONNECTED, getClusterCoordinator().getConnectionStatus(nodeId).getState()); } + + /** + * Assert that the node with the given ID is offloaded (according to this node!) within the given amount of time + * + * @param nodeId id of the node + * @param time how long to wait + * @param timeUnit unit of time provided by the 'time' argument + */ + public void assertNodeIsOffloaded(final NodeIdentifier nodeId, final long time, final TimeUnit timeUnit) { + ClusterUtils.waitUntilConditionMet(time, timeUnit, + () -> getClusterCoordinator().getConnectionStatus(nodeId).getState() == NodeConnectionState.OFFLOADED, + () -> "Connection Status is " + getClusterCoordinator().getConnectionStatus(nodeId).toString()); + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/resources/conf/nifi.properties index d8242310b54a..20f259d27f82 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/resources/conf/nifi.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/resources/conf/nifi.properties @@ -95,7 +95,6 @@ nifi.security.keyPasswd= nifi.security.truststore= nifi.security.truststoreType= nifi.security.truststorePasswd= -nifi.security.needClientAuth= nifi.security.authorizedUsers.file=./target/conf/authorized-users.xml nifi.security.user.credential.cache.duration=24 hours nifi.security.user.authority.provider=nifi.authorization.FileAuthorizationProvider diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml index 1af6154f6124..0afd093fba6d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml @@ -14,7 +14,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-core-api @@ -25,7 +25,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -42,7 +42,7 @@ language governing permissions and limitations under the License. --> org.apache.nifi nifi-site-to-site-client - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.commons diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java index 42214a9ab967..e17682eeed76 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java @@ -16,26 +16,6 @@ */ package org.apache.nifi.controller; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; - import org.apache.commons.lang3.StringUtils; import org.apache.nifi.attribute.expression.language.StandardPropertyValue; import org.apache.nifi.bundle.Bundle; @@ -58,6 +38,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + public abstract class AbstractComponentNode implements ComponentNode { private static final Logger logger = LoggerFactory.getLogger(AbstractComponentNode.class); @@ -71,6 +71,7 @@ public abstract class AbstractComponentNode implements ComponentNode { private final String componentCanonicalClass; private final ComponentVariableRegistry variableRegistry; private final ReloadComponent reloadComponent; + private final ExtensionManager extensionManager; private final AtomicBoolean isExtensionMissing; @@ -82,18 +83,19 @@ public abstract class AbstractComponentNode implements ComponentNode { private volatile boolean triggerValidation = true; public AbstractComponentNode(final String id, - final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, - final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry, - final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { + final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, + final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry, + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { this.id = id; this.validationContextFactory = validationContextFactory; this.serviceProvider = serviceProvider; this.name = new AtomicReference<>(componentType); this.componentType = componentType; this.componentCanonicalClass = componentCanonicalClass; + this.reloadComponent = reloadComponent; this.variableRegistry = variableRegistry; this.validationTrigger = validationTrigger; - this.reloadComponent = reloadComponent; + this.extensionManager = extensionManager; this.isExtensionMissing = new AtomicBoolean(isExtensionMissing); } @@ -171,7 +173,7 @@ public void setProperties(final Map properties, final boolean al try { verifyModifiable(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), id)) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), id)) { boolean classpathChanged = false; for (final Map.Entry entry : properties.entrySet()) { // determine if any of the property changes require resetting the InstanceClassLoader @@ -285,7 +287,7 @@ private boolean removeProperty(final String name, final boolean allowRemovalOfRe @Override public Map getProperties() { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getIdentifier())) { final List supported = getComponent().getPropertyDescriptors(); if (supported == null || supported.isEmpty()) { return Collections.unmodifiableMap(properties); @@ -365,20 +367,19 @@ public boolean equals(final Object obj) { @Override public String toString() { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().toString(); } } @Override - public final void performValidation() { - boolean replaced = false; - do { + public final ValidationStatus performValidation() { + while (true) { final ValidationState validationState = getValidationState(); final ValidationContext validationContext = getValidationContext(); final Collection results = new ArrayList<>(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getIdentifier())) { final Collection validationResults = computeValidationErrors(validationContext); results.addAll(validationResults); @@ -389,8 +390,11 @@ public final void performValidation() { final ValidationStatus status = results.isEmpty() ? ValidationStatus.VALID : ValidationStatus.INVALID; final ValidationState updatedState = new ValidationState(status, results); - replaced = replaceValidationState(validationState, updatedState); - } while (!replaced); + final boolean replaced = replaceValidationState(validationState, updatedState); + if (replaced) { + return status; + } + } } protected Collection computeValidationErrors(final ValidationContext validationContext) { @@ -467,23 +471,24 @@ protected final Collection validateReferencedControllerService private ValidationResult validateControllerServiceApi(final PropertyDescriptor descriptor, final ControllerServiceNode controllerServiceNode) { final Class controllerServiceApiClass = descriptor.getControllerServiceDefinition(); final ClassLoader controllerServiceApiClassLoader = controllerServiceApiClass.getClassLoader(); + final ExtensionManager extensionManager = serviceProvider.getExtensionManager(); final String serviceId = controllerServiceNode.getIdentifier(); final String propertyName = descriptor.getDisplayName(); - final Bundle controllerServiceApiBundle = ExtensionManager.getBundle(controllerServiceApiClassLoader); + final Bundle controllerServiceApiBundle = extensionManager.getBundle(controllerServiceApiClassLoader); if (controllerServiceApiBundle == null) { return createInvalidResult(serviceId, propertyName, "Unable to find bundle for ControllerService API class " + controllerServiceApiClass.getCanonicalName()); } final BundleCoordinate controllerServiceApiCoordinate = controllerServiceApiBundle.getBundleDetails().getCoordinate(); - final Bundle controllerServiceBundle = ExtensionManager.getBundle(controllerServiceNode.getBundleCoordinate()); + final Bundle controllerServiceBundle = extensionManager.getBundle(controllerServiceNode.getBundleCoordinate()); if (controllerServiceBundle == null) { return createInvalidResult(serviceId, propertyName, "Unable to find bundle for coordinate " + controllerServiceNode.getBundleCoordinate()); } final BundleCoordinate controllerServiceCoordinate = controllerServiceBundle.getBundleDetails().getCoordinate(); - final boolean matchesApi = matchesApi(controllerServiceBundle, controllerServiceApiCoordinate); + final boolean matchesApi = matchesApi(extensionManager, controllerServiceBundle, controllerServiceApiCoordinate); if (!matchesApi) { final String controllerServiceType = controllerServiceNode.getComponentType(); @@ -518,7 +523,7 @@ private ValidationResult createInvalidResult(final String serviceId, final Strin * @param requiredApiCoordinate the controller service API required by the processor * @return true if the controller service node has the require API as an ancestor, false otherwise */ - private boolean matchesApi(final Bundle controllerServiceImplBundle, final BundleCoordinate requiredApiCoordinate) { + private boolean matchesApi(final ExtensionManager extensionManager, final Bundle controllerServiceImplBundle, final BundleCoordinate requiredApiCoordinate) { // start with the coordinate of the controller service for cases where the API and service are in the same bundle BundleCoordinate controllerServiceDependencyCoordinate = controllerServiceImplBundle.getBundleDetails().getCoordinate(); @@ -531,7 +536,7 @@ private boolean matchesApi(final Bundle controllerServiceImplBundle, final Bundl } // move to the next dependency in the chain, or stop if null - final Bundle controllerServiceDependencyBundle = ExtensionManager.getBundle(controllerServiceDependencyCoordinate); + final Bundle controllerServiceDependencyBundle = extensionManager.getBundle(controllerServiceDependencyCoordinate); if (controllerServiceDependencyBundle == null) { controllerServiceDependencyCoordinate = null; } else { @@ -544,21 +549,21 @@ private boolean matchesApi(final Bundle controllerServiceImplBundle, final Bundl @Override public PropertyDescriptor getPropertyDescriptor(final String name) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().getPropertyDescriptor(name); } } @Override public List getPropertyDescriptors() { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().getPropertyDescriptors(); } } private final void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, getComponent().getClass(), getComponent().getIdentifier())) { getComponent().onPropertyModified(descriptor, oldValue, newValue); } } @@ -734,6 +739,10 @@ protected ReloadComponent getReloadComponent() { return this.reloadComponent; } + protected ExtensionManager getExtensionManager() { + return this.extensionManager; + } + @Override public void verifyCanUpdateBundle(final BundleCoordinate incomingCoordinate) throws IllegalArgumentException { final BundleCoordinate existingCoordinate = getBundleCoordinate(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ComponentNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ComponentNode.java index 707bb7525e8e..2357d41cd0c4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ComponentNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ComponentNode.java @@ -16,13 +16,6 @@ */ package org.apache.nifi.controller; -import java.net.URL; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - import org.apache.nifi.authorization.AccessDeniedException; import org.apache.nifi.authorization.AuthorizationResult; import org.apache.nifi.authorization.AuthorizationResult.Result; @@ -39,6 +32,13 @@ import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.registry.ComponentVariableRegistry; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + public interface ComponentNode extends ComponentAuthorizable { @Override @@ -179,7 +179,7 @@ public default void setProperties(Map properties) { /** * Asynchronously begins the validation process */ - public abstract void performValidation(); + public abstract ValidationStatus performValidation(); /** * Returns a {@link List} of all {@link PropertyDescriptor}s that this diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java index 0da86d38141c..12eeb88f23b8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java @@ -16,15 +16,8 @@ */ package org.apache.nifi.controller; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; +import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.components.validation.ValidationTrigger; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.controller.scheduling.LifecycleState; @@ -32,6 +25,7 @@ import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.logging.LogLevel; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.Relationship; @@ -39,6 +33,14 @@ import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.scheduling.SchedulingStrategy; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + public abstract class ProcessorNode extends AbstractComponentNode implements Connectable { protected final AtomicReference scheduledState; @@ -46,8 +48,10 @@ public abstract class ProcessorNode extends AbstractComponentNode implements Con public ProcessorNode(final String id, final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry, - final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { - super(id, validationContextFactory, serviceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, validationTrigger, isExtensionMissing); + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, + final boolean isExtensionMissing) { + super(id, validationContextFactory, serviceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, + extensionManager, validationTrigger, isExtensionMissing); this.scheduledState = new AtomicReference<>(ScheduledState.STOPPED); } @@ -141,7 +145,13 @@ public ProcessorNode(final String id, public ScheduledState getScheduledState() { ScheduledState sc = this.scheduledState.get(); if (sc == ScheduledState.STARTING) { - return ScheduledState.RUNNING; + final ValidationStatus validationStatus = getValidationStatus(); + + if (validationStatus == ValidationStatus.INVALID) { + return ScheduledState.STOPPED; + } else { + return ScheduledState.RUNNING; + } } else if (sc == ScheduledState.STOPPING) { return ScheduledState.STOPPED; } @@ -173,6 +183,8 @@ public ScheduledState getPhysicalScheduledState() { * initiate processor start task * @param administrativeYieldMillis * the amount of milliseconds to wait for administrative yield + * @param timeoutMillis the number of milliseconds to wait after triggering the Processor's @OnScheduled methods before timing out and considering + * the startup a failure. This will result in the thread being interrupted and trying again. * @param processContext * the instance of {@link ProcessContext} * @param schedulingAgentCallback @@ -183,8 +195,8 @@ public ScheduledState getPhysicalScheduledState() { * value is true or if the Processor is in any state other than 'STOPPING' or 'RUNNING', then this method * will throw an {@link IllegalStateException}. */ - public abstract void start(ScheduledExecutorService scheduler, - long administrativeYieldMillis, ProcessContext processContext, SchedulingAgentCallback schedulingAgentCallback, boolean failIfStopping); + public abstract void start(ScheduledExecutorService scheduler, long administrativeYieldMillis, long timeoutMillis, ProcessContext processContext, + SchedulingAgentCallback schedulingAgentCallback, boolean failIfStopping); /** * Will stop the {@link Processor} represented by this {@link ProcessorNode}. @@ -235,4 +247,12 @@ public abstract CompletableFuture stop(ProcessScheduler processScheduler, * will result in the WARN message if processor can not be enabled. */ public abstract void disable(); + + /** + * Returns the Scheduled State that is desired for this Processor. This may vary from the current state if the Processor is not + * currently valid, is in the process of stopping but should then transition to Running, etc. + * + * @return the desired state for this Processor + */ + public abstract ScheduledState getDesiredState(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java new file mode 100644 index 000000000000..c741f3388fbc --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/flow/FlowManager.java @@ -0,0 +1,290 @@ +/* + * 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.controller.flow; + +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.connectable.Funnel; +import org.apache.nifi.connectable.Port; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.label.Label; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.web.api.dto.FlowSnippetDTO; + +import java.net.URL; +import java.util.Collection; +import java.util.Set; + +public interface FlowManager { + String ROOT_GROUP_ID_ALIAS = "root"; + String DEFAULT_ROOT_GROUP_NAME = "NiFi Flow"; + + /** + * Creates a Port to use as an Input Port for receiving data via Site-to-Site communications + * + * @param id port id + * @param name port name + * @return new port + * @throws NullPointerException if the ID or name is not unique + * @throws IllegalStateException if a Port already exists with the same id. + */ + Port createRemoteInputPort(String id, String name); + + /** + * Creates a Port to use as an Output Port for transferring data via Site-to-Site communications + * + * @param id port id + * @param name port name + * @return new port + * @throws NullPointerException if the ID or name is not unique + * @throws IllegalStateException if a Port already exists with the same id. + */ + Port createRemoteOutputPort(String id, String name); + + /** + * Creates a new Remote Process Group with the given ID that points to the given URI + * + * @param id Remote Process Group ID + * @param uris group uris, multiple url can be specified in comma-separated format + * @return new remote process group + * @throws NullPointerException if either argument is null + * @throws IllegalArgumentException if any of the uris is not a valid URI. + */ + RemoteProcessGroup createRemoteProcessGroup(String id, String uris); + + /** + * @return the ProcessGroup that is currently assigned as the Root Group + */ + ProcessGroup getRootGroup(); + + String getRootGroupId(); + + /** + * Creates an instance of the given snippet and adds the components to the given group + * + * @param group group + * @param dto dto + * + * @throws NullPointerException if either argument is null + * @throws IllegalStateException if the snippet is not valid because a + * component in the snippet has an ID that is not unique to this flow, or + * because it shares an Input Port or Output Port at the root level whose + * name already exists in the given ProcessGroup, or because the Template + * contains a Processor or a Prioritizer whose class is not valid within + * this instance of NiFi. + * @throws ProcessorInstantiationException if unable to instantiate a + * processor + */ + void instantiateSnippet(ProcessGroup group, FlowSnippetDTO dto) throws ProcessorInstantiationException; + + /** + * Indicates whether or not the two ID's point to the same ProcessGroup. If + * either id is null, will return false. + * + * @param id1 group id + * @param id2 other group id + * @return true if same + */ + boolean areGroupsSame(String id1, String id2); + + /** + * Creates a new instance of the FlowFilePrioritizer with the given type + * @param type the type of the prioritizer (fully qualified class name) + * @return the newly created FlowFile Prioritizer + */ + FlowFilePrioritizer createPrioritizer(String type) throws InstantiationException, IllegalAccessException, ClassNotFoundException; + + /** + * Returns the ProcessGroup with the given ID, or null if no group exists with the given ID. + * @param id id of the group + * @return the ProcessGroup with the given ID or null if none can be found + */ + ProcessGroup getGroup(String id); + + void onProcessGroupAdded(ProcessGroup group); + + void onProcessGroupRemoved(ProcessGroup group); + + + /** + * Finds the Connectable with the given ID, or null if no such Connectable exists + * @param id the ID of the Connectable + * @return the Connectable with the given ID, or null if no such Connectable exists + */ + Connectable findConnectable(String id); + + /** + * Returns the ProcessorNode with the given ID + * @param id the ID of the Processor + * @return the ProcessorNode with the given ID or null if no such Processor exists + */ + ProcessorNode getProcessorNode(String id); + + void onProcessorAdded(ProcessorNode processor); + + void onProcessorRemoved(ProcessorNode processor); + + + /** + *

+ * Creates a new ProcessorNode with the given type and identifier and + * initializes it invoking the methods annotated with {@link org.apache.nifi.annotation.lifecycle.OnAdded}. + *

+ * + * @param type processor type + * @param id processor id + * @param coordinate the coordinate of the bundle for this processor + * @return new processor + * @throws NullPointerException if either arg is null + */ + ProcessorNode createProcessor(String type, String id, BundleCoordinate coordinate); + + /** + *

+ * Creates a new ProcessorNode with the given type and identifier and + * optionally initializes it. + *

+ * + * @param type the fully qualified Processor class name + * @param id the unique ID of the Processor + * @param coordinate the bundle coordinate for this processor + * @param firstTimeAdded whether or not this is the first time this + * Processor is added to the graph. If {@code true}, will invoke methods + * annotated with the {@link org.apache.nifi.annotation.lifecycle.OnAdded} annotation. + * @return new processor node + * @throws NullPointerException if either arg is null + */ + ProcessorNode createProcessor(String type, String id, BundleCoordinate coordinate, boolean firstTimeAdded); + + /** + *

+ * Creates a new ProcessorNode with the given type and identifier and + * optionally initializes it. + *

+ * + * @param type the fully qualified Processor class name + * @param id the unique ID of the Processor + * @param coordinate the bundle coordinate for this processor + * @param firstTimeAdded whether or not this is the first time this + * Processor is added to the graph. If {@code true}, will invoke methods + * annotated with the {@link org.apache.nifi.annotation.lifecycle.OnAdded} annotation. + * @return new processor node + * @throws NullPointerException if either arg is null + */ + ProcessorNode createProcessor(String type, String id, BundleCoordinate coordinate, Set additionalUrls, boolean firstTimeAdded, boolean registerLogObserver); + + + + Label createLabel(String id, String text); + + Funnel createFunnel(String id); + + Port createLocalInputPort(String id, String name); + + Port createLocalOutputPort(String id, String name); + + ProcessGroup createProcessGroup(String id); + + + + void onConnectionAdded(Connection connection); + + void onConnectionRemoved(Connection connection); + + Connection getConnection(String id); + + Set findAllConnections(); + + /** + * Creates a connection between two Connectable objects. + * + * @param id required ID of the connection + * @param name the name of the connection, or null to leave the connection unnamed + * @param source required source + * @param destination required destination + * @param relationshipNames required collection of relationship names + * @return the created Connection + * + * @throws NullPointerException if the ID, source, destination, or set of relationships is null. + * @throws IllegalArgumentException if relationships is an empty collection + */ + Connection createConnection(final String id, final String name, final Connectable source, final Connectable destination, final Collection relationshipNames); + + + + void onInputPortAdded(Port inputPort); + + void onInputPortRemoved(Port inputPort); + + Port getInputPort(String id); + + + + void onOutputPortAdded(Port outputPort); + + void onOutputPortRemoved(Port outputPort); + + Port getOutputPort(String id); + + + + void onFunnelAdded(Funnel funnel); + + void onFunnelRemoved(Funnel funnel); + + Funnel getFunnel(String id); + + + + ReportingTaskNode createReportingTask(String type, BundleCoordinate bundleCoordinate); + + ReportingTaskNode createReportingTask(String type, BundleCoordinate bundleCoordinate, boolean firstTimeAdded); + + ReportingTaskNode createReportingTask(String type, String id, BundleCoordinate bundleCoordinate, boolean firstTimeAdded); + + ReportingTaskNode createReportingTask(String type, String id, BundleCoordinate bundleCoordinate, Set additionalUrls, boolean firstTimeAdded, boolean register); + + ReportingTaskNode getReportingTaskNode(String taskId); + + void removeReportingTask(ReportingTaskNode reportingTask); + + Set getAllReportingTasks(); + + + + Set getAllControllerServices(); + + ControllerServiceNode getControllerServiceNode(String id); + + ControllerServiceNode createControllerService(String type, String id, BundleCoordinate bundleCoordinate, Set additionalUrls, boolean firstTimeAdded, + boolean registerLogObserver); + + + Set getRootControllerServices(); + + void addRootControllerService(ControllerServiceNode serviceNode); + + ControllerServiceNode getRootControllerService(String serviceIdentifier); + + void removeRootControllerService(final ControllerServiceNode service); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/reporting/ReportingTaskProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/reporting/ReportingTaskProvider.java index c8267756c743..4e310b753525 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/reporting/ReportingTaskProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/reporting/ReportingTaskProvider.java @@ -20,6 +20,7 @@ import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.nar.ExtensionManager; /** * A ReportingTaskProvider is responsible for providing management of, and @@ -112,4 +113,9 @@ public interface ReportingTaskProvider { */ void disableReportingTask(ReportingTaskNode reportingTask); + /** + * @return the ExtensionManager instance used by this provider + */ + ExtensionManager getExtensionManager(); + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java index 56276f43a9b7..15033b94f658 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java @@ -16,36 +16,26 @@ */ package org.apache.nifi.controller.service; -import java.net.URL; +import org.apache.nifi.controller.ComponentNode; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.controller.ControllerServiceLookup; +import org.apache.nifi.nar.ExtensionManager; + import java.util.Collection; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; -import org.apache.nifi.annotation.lifecycle.OnAdded; -import org.apache.nifi.bundle.BundleCoordinate; -import org.apache.nifi.controller.ComponentNode; -import org.apache.nifi.controller.ControllerService; -import org.apache.nifi.controller.ControllerServiceLookup; - /** * */ public interface ControllerServiceProvider extends ControllerServiceLookup { /** - * Creates a new Controller Service of the specified type and assigns it the - * given id. If firstTimeadded is true, calls any methods that - * are annotated with {@link OnAdded} - * - * @param type of service - * @param id of service - * @param bundleCoordinate the coordinate of the bundle for the service - * @param additionalUrls optional additional URL resources to add to the class loader of the component - * @param firstTimeAdded for service - * @return the service node + * Notifies the ControllerServiceProvider that the given Controller Service has been added to the flow + * @param serviceNode the Controller Service Node */ - ControllerServiceNode createControllerService(String type, String id, BundleCoordinate bundleCoordinate, Set additionalUrls, boolean firstTimeAdded); + void onControllerServiceAdded(ControllerServiceNode serviceNode); /** * @param id of the service @@ -113,10 +103,9 @@ public interface ControllerServiceProvider extends ControllerServiceLookup { Future disableControllerServicesAsync(Collection serviceNodes); /** - * @return a Set of all Controller Services that exist for this service - * provider + * @return a Set of all Controller Services that exist for this service provider */ - Set getAllControllerServices(); + Collection getNonRootControllerServices(); /** * Verifies that all running Processors and Reporting Tasks referencing the @@ -225,4 +214,10 @@ public interface ControllerServiceProvider extends ControllerServiceLookup { * identifier */ ControllerService getControllerServiceForComponent(String serviceIdentifier, String componentId); + + /** + * @return the ExtensionManager used by this provider + */ + ExtensionManager getExtensionManager(); + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/logging/LogRepositoryFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/logging/LogRepositoryFactory.java index 530b6ef297dd..3b3a07283da5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/logging/LogRepositoryFactory.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/logging/LogRepositoryFactory.java @@ -16,12 +16,12 @@ */ package org.apache.nifi.logging; -import static java.util.Objects.requireNonNull; +import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import org.slf4j.LoggerFactory; +import static java.util.Objects.requireNonNull; @SuppressWarnings("unchecked") public class LogRepositoryFactory { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java index 77c2761404bb..06d8ff567b1b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java @@ -23,9 +23,19 @@ public interface FlowRegistryClient { FlowRegistry getFlowRegistry(String registryId); default String getFlowRegistryId(String registryUrl) { + if (registryUrl.endsWith("/")) { + registryUrl = registryUrl.substring(0, registryUrl.length() - 1); + } + for (final String registryClientId : getRegistryIdentifiers()) { final FlowRegistry registry = getFlowRegistry(registryClientId); - if (registry.getURL().equals(registryUrl)) { + + String registryClientUrl = registry.getURL(); + if (registryClientUrl.endsWith("/")) { + registryClientUrl = registryClientUrl.substring(0, registryClientUrl.length() - 1); + } + + if (registryClientUrl.equals(registryUrl)) { return registryClientId; } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/reporting/UserAwareEventAccess.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/reporting/UserAwareEventAccess.java new file mode 100644 index 000000000000..32b2e018408c --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/reporting/UserAwareEventAccess.java @@ -0,0 +1,69 @@ +/* + * 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.reporting; + +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.controller.repository.RepositoryStatusReport; +import org.apache.nifi.controller.status.ProcessGroupStatus; + +public interface UserAwareEventAccess extends EventAccess { + /** + * Returns the status for components in the specified group. This request is + * made by the specified user so the results will be filtered accordingly. + * + * @param groupId group id + * @param user user making request + * @return the component status + */ + ProcessGroupStatus getGroupStatus(String groupId, NiFiUser user, int recursiveStatusDepth); + + + /** + * Returns the status for the components in the specified group with the + * specified report. This request is made by the specified user so the + * results will be filtered accordingly. + * + * @param groupId group id + * @param statusReport report + * @param user user making request + * @return the component status + */ + ProcessGroupStatus getGroupStatus(String groupId, RepositoryStatusReport statusReport, NiFiUser user); + + /** + * Returns the status for components in the specified group. This request is + * made by the specified user so the results will be filtered accordingly. + * + * @param groupId group id + * @param user user making request + * @return the component status + */ + ProcessGroupStatus getGroupStatus(String groupId, NiFiUser user); + + /** + * Returns the status for the components in the specified group with the + * specified report. This request is made by the specified user so the + * results will be filtered accordingly. + * + * @param groupId group id + * @param statusReport report + * @param user user making request + * @param recursiveStatusDepth the number of levels deep we should recurse and still include the the processors' statuses, the groups' statuses, etc. in the returned ProcessGroupStatus + * @return the component status + */ + ProcessGroupStatus getGroupStatus(String groupId, RepositoryStatusReport statusReport, NiFiUser user, int recursiveStatusDepth); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/controller/TestAbstractComponentNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/controller/TestAbstractComponentNode.java index 3fa5d2c3e253..49dfb8b612e7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/controller/TestAbstractComponentNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/controller/TestAbstractComponentNode.java @@ -35,6 +35,7 @@ import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.components.validation.ValidationTrigger; import org.apache.nifi.controller.service.ControllerServiceProvider; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; import org.apache.nifi.registry.ComponentVariableRegistry; import org.junit.Test; import org.mockito.Mockito; @@ -85,7 +86,7 @@ private static class ValidationControlledAbstractComponentNode extends AbstractC public ValidationControlledAbstractComponentNode(final long pauseMillis, final ValidationTrigger validationTrigger) { super("id", Mockito.mock(ValidationContextFactory.class), Mockito.mock(ControllerServiceProvider.class), "unit test component", ValidationControlledAbstractComponentNode.class.getCanonicalName(), Mockito.mock(ComponentVariableRegistry.class), Mockito.mock(ReloadComponent.class), - validationTrigger, false); + Mockito.mock(StandardExtensionDiscoveringManager.class), validationTrigger, false); this.pauseMillis = pauseMillis; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/registry/flow/TestFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/registry/flow/TestFlowRegistryClient.java new file mode 100644 index 000000000000..208558c8f787 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/registry/flow/TestFlowRegistryClient.java @@ -0,0 +1,109 @@ +/* + * 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.registry.flow; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestFlowRegistryClient { + + private FlowRegistryClient flowRegistryClient; + + @Before + public void setup() { + flowRegistryClient = new MockFlowRegistryClient(); + } + + @Test + public void testParamWithTrailingSlash() { + flowRegistryClient.addFlowRegistry("1", "Registry 1", "http://localhost:1111", "NA"); + flowRegistryClient.addFlowRegistry("2", "Registry 2", "http://localhost:2222", "NA"); + flowRegistryClient.addFlowRegistry("3", "Registry 3", "http://localhost:3333", "NA"); + + final String flowRegistryId = flowRegistryClient.getFlowRegistryId("http://localhost:1111/"); + assertNotNull(flowRegistryId); + assertEquals("1", flowRegistryId); + } + + @Test + public void testClientWithTrailingSlash() { + flowRegistryClient.addFlowRegistry("1", "Registry 1", "http://localhost:1111", "NA"); + flowRegistryClient.addFlowRegistry("2", "Registry 2", "http://localhost:2222/", "NA"); + flowRegistryClient.addFlowRegistry("3", "Registry 3", "http://localhost:3333", "NA"); + + final String flowRegistryId = flowRegistryClient.getFlowRegistryId("http://localhost:2222"); + assertNotNull(flowRegistryId); + assertEquals("2", flowRegistryId); + } + + @Test + public void testNoTrailingSlash() { + flowRegistryClient.addFlowRegistry("1", "Registry 1", "http://localhost:1111", "NA"); + flowRegistryClient.addFlowRegistry("2", "Registry 2", "http://localhost:2222", "NA"); + flowRegistryClient.addFlowRegistry("3", "Registry 3", "http://localhost:3333", "NA"); + + final String flowRegistryId = flowRegistryClient.getFlowRegistryId("http://localhost:3333"); + assertNotNull(flowRegistryId); + assertEquals("3", flowRegistryId); + } + + + private static class MockFlowRegistryClient implements FlowRegistryClient { + + private Map registryMap = new HashMap<>(); + + @Override + public FlowRegistry getFlowRegistry(String registryId) { + return registryMap.get(registryId); + } + + @Override + public Set getRegistryIdentifiers() { + return registryMap.keySet(); + } + + @Override + public void addFlowRegistry(FlowRegistry registry) { + registryMap.put(registry.getIdentifier(), registry); + } + + @Override + public FlowRegistry addFlowRegistry(String registryId, String registryName, String registryUrl, String description) { + final FlowRegistry flowRegistry = mock(FlowRegistry.class); + when(flowRegistry.getIdentifier()).thenReturn(registryId); + when(flowRegistry.getName()).thenReturn(registryName); + when(flowRegistry.getURL()).thenReturn(registryUrl); + when(flowRegistry.getDescription()).thenReturn(description); + registryMap.put(flowRegistry.getIdentifier(), flowRegistry); + return flowRegistry; + } + + @Override + public FlowRegistry removeFlowRegistry(String registryId) { + return registryMap.remove(registryId); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml index 90c87fc83dcf..6bb1ea82efd0 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml @@ -18,7 +18,7 @@ org.apache.nifi nifi-framework - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT nifi-framework-core jar @@ -46,22 +46,22 @@ org.apache.nifi nifi-expression-language - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-schema-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-repository-models - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -82,7 +82,7 @@ org.apache.nifi nifi-logging-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -91,7 +91,7 @@ org.apache.nifi nifi-site-to-site-client - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi @@ -136,17 +136,17 @@ org.apache.nifi nifi-data-provenance-utils - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-write-ahead-log - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.nifi nifi-flowfile-repo-serialization - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT org.apache.zookeeper @@ -197,7 +197,7 @@ org.apache.nifi nifi-mock - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test @@ -222,7 +222,7 @@ org.apache.nifi nifi-mock-authorizer - 1.8.0-SNAPSHOT + 1.9.0-SNAPSHOT test
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java index 0665dd2c2b3d..b49d52e238aa 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/validation/TriggerValidationTask.java @@ -18,18 +18,18 @@ package org.apache.nifi.components.validation; import org.apache.nifi.controller.ComponentNode; -import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.flow.FlowManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TriggerValidationTask implements Runnable { private static final Logger logger = LoggerFactory.getLogger(TriggerValidationTask.class); - private final FlowController controller; + private final FlowManager flowManager; private final ValidationTrigger validationTrigger; - public TriggerValidationTask(final FlowController controller, final ValidationTrigger validationTrigger) { - this.controller = controller; + public TriggerValidationTask(final FlowManager flowManager, final ValidationTrigger validationTrigger) { + this.flowManager = flowManager; this.validationTrigger = validationTrigger; } @@ -38,15 +38,15 @@ public void run() { try { logger.debug("Triggering validation of all components"); - for (final ComponentNode node : controller.getAllControllerServices()) { + for (final ComponentNode node : flowManager.getAllControllerServices()) { validationTrigger.trigger(node); } - for (final ComponentNode node : controller.getAllReportingTasks()) { + for (final ComponentNode node : flowManager.getAllReportingTasks()) { validationTrigger.trigger(node); } - for (final ComponentNode node : controller.getRootGroup().findAllProcessors()) { + for (final ComponentNode node : flowManager.getRootGroup().findAllProcessors()) { validationTrigger.trigger(node); } } catch (final Throwable t) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/EventDrivenWorkerQueue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/EventDrivenWorkerQueue.java index f36a45951425..25e8a86f1114 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/EventDrivenWorkerQueue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/EventDrivenWorkerQueue.java @@ -16,6 +16,11 @@ */ package org.apache.nifi.controller; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.util.Connectables; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -25,11 +30,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.nifi.connectable.Connectable; -import org.apache.nifi.connectable.Connection; -import org.apache.nifi.processor.Relationship; -import org.apache.nifi.util.Connectables; - public class EventDrivenWorkerQueue implements WorkerQueue { private final Object workMonitor = new Object(); @@ -69,6 +69,8 @@ public Worker poll(final long timeout, final TimeUnit timeUnit) { try { workMonitor.wait(timeLeft); } catch (final InterruptedException ignored) { + Thread.currentThread().interrupt(); + return null; } } else { // Decrement the amount of work there is to do for this worker. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java new file mode 100644 index 000000000000..e897dd2086b0 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java @@ -0,0 +1,470 @@ +/* + * 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.controller; + +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.configuration.DefaultSettings; +import org.apache.nifi.bundle.Bundle; +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.components.ConfigurableComponent; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateManagerProvider; +import org.apache.nifi.components.validation.ValidationTrigger; +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.kerberos.KerberosConfig; +import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException; +import org.apache.nifi.controller.reporting.StandardReportingInitializationContext; +import org.apache.nifi.controller.reporting.StandardReportingTaskNode; +import org.apache.nifi.controller.service.ControllerServiceInvocationHandler; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceProvider; +import org.apache.nifi.controller.service.GhostControllerService; +import org.apache.nifi.controller.service.StandardControllerServiceInitializationContext; +import org.apache.nifi.controller.service.StandardControllerServiceInvocationHandler; +import org.apache.nifi.controller.service.StandardControllerServiceNode; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.processor.GhostProcessor; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.SimpleProcessLogger; +import org.apache.nifi.processor.StandardProcessorInitializationContext; +import org.apache.nifi.processor.StandardValidationContextFactory; +import org.apache.nifi.registry.ComponentVariableRegistry; +import org.apache.nifi.registry.VariableRegistry; +import org.apache.nifi.registry.variable.StandardComponentVariableRegistry; +import org.apache.nifi.reporting.GhostReportingTask; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.reporting.ReportingInitializationContext; +import org.apache.nifi.reporting.ReportingTask; +import org.apache.nifi.scheduling.SchedulingStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Proxy; +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ExtensionBuilder { + private static final Logger logger = LoggerFactory.getLogger(ExtensionBuilder.class); + + private String type; + private String identifier; + private BundleCoordinate bundleCoordinate; + private ExtensionManager extensionManager; + private Set classpathUrls; + private KerberosConfig kerberosConfig = KerberosConfig.NOT_CONFIGURED; + private ControllerServiceProvider serviceProvider; + private NodeTypeProvider nodeTypeProvider; + private VariableRegistry variableRegistry; + private ProcessScheduler processScheduler; + private ValidationTrigger validationTrigger; + private ReloadComponent reloadComponent; + private FlowController flowController; + private StateManagerProvider stateManagerProvider; + + public ExtensionBuilder type(final String type) { + this.type = type; + return this; + } + + public ExtensionBuilder identifier(final String identifier) { + this.identifier = identifier; + return this; + } + + public ExtensionBuilder bundleCoordinate(final BundleCoordinate coordinate) { + this.bundleCoordinate = coordinate; + return this; + } + + public ExtensionBuilder addClasspathUrls(final Set urls) { + if (urls == null || urls.isEmpty()) { + return this; + } + + if (this.classpathUrls == null) { + this.classpathUrls = new HashSet<>(); + } + + this.classpathUrls.addAll(urls); + return this; + } + + public ExtensionBuilder kerberosConfig(final KerberosConfig kerberosConfig) { + this.kerberosConfig = kerberosConfig; + return this; + } + + public ExtensionBuilder controllerServiceProvider(final ControllerServiceProvider serviceProvider) { + this.serviceProvider = serviceProvider; + return this; + } + + public ExtensionBuilder nodeTypeProvider(final NodeTypeProvider nodeTypeProvider) { + this.nodeTypeProvider = nodeTypeProvider; + return this; + } + + public ExtensionBuilder variableRegistry(final VariableRegistry variableRegistry) { + this.variableRegistry = variableRegistry; + return this; + } + + public ExtensionBuilder processScheduler(final ProcessScheduler scheduler) { + this.processScheduler = scheduler; + return this; + } + + public ExtensionBuilder validationTrigger(final ValidationTrigger validationTrigger) { + this.validationTrigger = validationTrigger; + return this; + } + + public ExtensionBuilder reloadComponent(final ReloadComponent reloadComponent) { + this.reloadComponent = reloadComponent; + return this; + } + + public ExtensionBuilder flowController(final FlowController flowController) { + this.flowController = flowController; + return this; + } + + public ExtensionBuilder stateManagerProvider(final StateManagerProvider stateManagerProvider) { + this.stateManagerProvider = stateManagerProvider; + return this; + } + + public ExtensionBuilder extensionManager(final ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + return this; + } + + public ProcessorNode buildProcessor() { + if (identifier == null) { + throw new IllegalStateException("Processor ID must be specified"); + } + if (type == null) { + throw new IllegalStateException("Processor Type must be specified"); + } + if (bundleCoordinate == null) { + throw new IllegalStateException("Bundle Coordinate must be specified"); + } + if (extensionManager == null) { + throw new IllegalStateException("Extension Manager must be specified"); + } + if (serviceProvider == null) { + throw new IllegalStateException("Controller Service Provider must be specified"); + } + if (nodeTypeProvider == null) { + throw new IllegalStateException("Node Type Provider must be specified"); + } + if (variableRegistry == null) { + throw new IllegalStateException("Variable Registry must be specified"); + } + if (reloadComponent == null) { + throw new IllegalStateException("Reload Component must be specified"); + } + + boolean creationSuccessful = true; + LoggableComponent loggableComponent; + try { + loggableComponent = createLoggableProcessor(); + } catch (final ProcessorInstantiationException pie) { + logger.error("Could not create Processor of type " + type + " for ID " + identifier + "; creating \"Ghost\" implementation", pie); + final GhostProcessor ghostProc = new GhostProcessor(); + ghostProc.setIdentifier(identifier); + ghostProc.setCanonicalClassName(type); + loggableComponent = new LoggableComponent<>(ghostProc, bundleCoordinate, null); + creationSuccessful = false; + } + + final ProcessorNode processorNode = createProcessorNode(loggableComponent, creationSuccessful); + return processorNode; + } + + public ReportingTaskNode buildReportingTask() { + if (identifier == null) { + throw new IllegalStateException("ReportingTask ID must be specified"); + } + if (type == null) { + throw new IllegalStateException("ReportingTask Type must be specified"); + } + if (bundleCoordinate == null) { + throw new IllegalStateException("Bundle Coordinate must be specified"); + } + if (extensionManager == null) { + throw new IllegalStateException("Extension Manager must be specified"); + } + if (serviceProvider == null) { + throw new IllegalStateException("Controller Service Provider must be specified"); + } + if (nodeTypeProvider == null) { + throw new IllegalStateException("Node Type Provider must be specified"); + } + if (variableRegistry == null) { + throw new IllegalStateException("Variable Registry must be specified"); + } + if (reloadComponent == null) { + throw new IllegalStateException("Reload Component must be specified"); + } + if (flowController == null) { + throw new IllegalStateException("FlowController must be specified"); + } + + boolean creationSuccessful = true; + LoggableComponent loggableComponent; + try { + loggableComponent = createLoggableReportingTask(); + } catch (final ReportingTaskInstantiationException rtie) { + logger.error("Could not create ReportingTask of type " + type + " for ID " + identifier + "; creating \"Ghost\" implementation", rtie); + final GhostReportingTask ghostReportingTask = new GhostReportingTask(); + ghostReportingTask.setIdentifier(identifier); + ghostReportingTask.setCanonicalClassName(type); + loggableComponent = new LoggableComponent<>(ghostReportingTask, bundleCoordinate, null); + creationSuccessful = false; + } + + final ReportingTaskNode taskNode = createReportingTaskNode(loggableComponent, creationSuccessful); + return taskNode; + } + + public ControllerServiceNode buildControllerService() { + if (identifier == null) { + throw new IllegalStateException("ReportingTask ID must be specified"); + } + if (type == null) { + throw new IllegalStateException("ReportingTask Type must be specified"); + } + if (bundleCoordinate == null) { + throw new IllegalStateException("Bundle Coordinate must be specified"); + } + if (extensionManager == null) { + throw new IllegalStateException("Extension Manager must be specified"); + } + if (serviceProvider == null) { + throw new IllegalStateException("Controller Service Provider must be specified"); + } + if (nodeTypeProvider == null) { + throw new IllegalStateException("Node Type Provider must be specified"); + } + if (variableRegistry == null) { + throw new IllegalStateException("Variable Registry must be specified"); + } + if (reloadComponent == null) { + throw new IllegalStateException("Reload Component must be specified"); + } + if (stateManagerProvider == null) { + throw new IllegalStateException("State Manager Provider must be specified"); + } + + try { + return createControllerServiceNode(); + } catch (final Exception e) { + logger.error("Could not create Controller Service of type " + type + " for ID " + identifier + "; creating \"Ghost\" implementation", e); + return createGhostControllerServiceNode(); + } + } + + + private ProcessorNode createProcessorNode(final LoggableComponent processor, final boolean creationSuccessful) { + final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); + final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry); + + final ProcessorNode procNode; + if (creationSuccessful) { + procNode = new StandardProcessorNode(processor, identifier, validationContextFactory, processScheduler, serviceProvider, + componentVarRegistry, reloadComponent, extensionManager, validationTrigger); + } else { + final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; + final String componentType = "(Missing) " + simpleClassName; + procNode = new StandardProcessorNode(processor, identifier, validationContextFactory, processScheduler, serviceProvider, + componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, true); + } + + applyDefaultSettings(procNode); + return procNode; + } + + + private ReportingTaskNode createReportingTaskNode(final LoggableComponent reportingTask, final boolean creationSuccessful) { + final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); + final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry); + final ReportingTaskNode taskNode; + if (creationSuccessful) { + taskNode = new StandardReportingTaskNode(reportingTask, identifier, flowController, processScheduler, + validationContextFactory, componentVarRegistry, reloadComponent, extensionManager, validationTrigger); + taskNode.setName(taskNode.getReportingTask().getClass().getSimpleName()); + } else { + final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; + final String componentType = "(Missing) " + simpleClassName; + + taskNode = new StandardReportingTaskNode(reportingTask, identifier, flowController, processScheduler, validationContextFactory, + componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, true); + taskNode.setName(componentType); + } + + return taskNode; + } + + private void applyDefaultSettings(final ProcessorNode processorNode) { + try { + final Class procClass = processorNode.getProcessor().getClass(); + + final DefaultSettings ds = procClass.getAnnotation(DefaultSettings.class); + if (ds != null) { + processorNode.setYieldPeriod(ds.yieldDuration()); + processorNode.setPenalizationPeriod(ds.penaltyDuration()); + processorNode.setBulletinLevel(ds.bulletinLevel()); + } + } catch (final Exception ex) { + logger.error("Error while setting default settings from DefaultSettings annotation: {}", ex.toString(), ex); + } + } + + private ControllerServiceNode createControllerServiceNode() throws ClassNotFoundException, IllegalAccessException, InstantiationException, InitializationException { + final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); + try { + final Bundle bundle = extensionManager.getBundle(bundleCoordinate); + if (bundle == null) { + throw new IllegalStateException("Unable to find bundle for coordinate " + bundleCoordinate.getCoordinate()); + } + + final ClassLoader detectedClassLoader = extensionManager.createInstanceClassLoader(type, identifier, bundle, classpathUrls == null ? Collections.emptySet() : classpathUrls); + final Class rawClass = Class.forName(type, true, detectedClassLoader); + Thread.currentThread().setContextClassLoader(detectedClassLoader); + + final Class controllerServiceClass = rawClass.asSubclass(ControllerService.class); + final ControllerService serviceImpl = controllerServiceClass.newInstance(); + final StandardControllerServiceInvocationHandler invocationHandler = new StandardControllerServiceInvocationHandler(extensionManager, serviceImpl); + + // extract all interfaces... controllerServiceClass is non null so getAllInterfaces is non null + final List> interfaceList = ClassUtils.getAllInterfaces(controllerServiceClass); + final Class[] interfaces = interfaceList.toArray(new Class[0]); + + final ControllerService proxiedService; + if (detectedClassLoader == null) { + proxiedService = (ControllerService) Proxy.newProxyInstance(getClass().getClassLoader(), interfaces, invocationHandler); + } else { + proxiedService = (ControllerService) Proxy.newProxyInstance(detectedClassLoader, interfaces, invocationHandler); + } + + logger.info("Created Controller Service of type {} with identifier {}", type, identifier); + final ComponentLog serviceLogger = new SimpleProcessLogger(identifier, serviceImpl); + final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(serviceLogger); + + final StateManager stateManager = stateManagerProvider.getStateManager(identifier); + final ControllerServiceInitializationContext initContext = new StandardControllerServiceInitializationContext(identifier, terminationAwareLogger, + serviceProvider, stateManager, kerberosConfig); + serviceImpl.initialize(initContext); + + final LoggableComponent originalLoggableComponent = new LoggableComponent<>(serviceImpl, bundleCoordinate, terminationAwareLogger); + final LoggableComponent proxiedLoggableComponent = new LoggableComponent<>(proxiedService, bundleCoordinate, terminationAwareLogger); + + final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); + final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, componentVarRegistry); + final ControllerServiceNode serviceNode = new StandardControllerServiceNode(originalLoggableComponent, proxiedLoggableComponent, invocationHandler, + identifier, validationContextFactory, serviceProvider, componentVarRegistry, reloadComponent, extensionManager, validationTrigger); + serviceNode.setName(rawClass.getSimpleName()); + + invocationHandler.setServiceNode(serviceNode); + return serviceNode; + } finally { + if (ctxClassLoader != null) { + Thread.currentThread().setContextClassLoader(ctxClassLoader); + } + } + } + + private ControllerServiceNode createGhostControllerServiceNode() { + final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; + final String componentType = "(Missing) " + simpleClassName; + + final GhostControllerService ghostService = new GhostControllerService(identifier, type); + final LoggableComponent proxiedLoggableComponent = new LoggableComponent<>(ghostService, bundleCoordinate, null); + + final ControllerServiceInvocationHandler invocationHandler = new StandardControllerServiceInvocationHandler(extensionManager, ghostService); + + final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); + final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(serviceProvider, variableRegistry); + final ControllerServiceNode serviceNode = new StandardControllerServiceNode(proxiedLoggableComponent, proxiedLoggableComponent, invocationHandler, identifier, + validationContextFactory, serviceProvider, componentType, type, componentVarRegistry, reloadComponent, extensionManager, validationTrigger, true); + + return serviceNode; + } + + private LoggableComponent createLoggableProcessor() throws ProcessorInstantiationException { + try { + final LoggableComponent processorComponent = createLoggableComponent(Processor.class); + + final ProcessorInitializationContext initiContext = new StandardProcessorInitializationContext(identifier, processorComponent.getLogger(), + serviceProvider, nodeTypeProvider, kerberosConfig); + processorComponent.getComponent().initialize(initiContext); + + return processorComponent; + } catch (final Exception e) { + throw new ProcessorInstantiationException(type, e); + } + } + + + private LoggableComponent createLoggableReportingTask() throws ReportingTaskInstantiationException { + try { + final LoggableComponent taskComponent = createLoggableComponent(ReportingTask.class); + + final String taskName = taskComponent.getComponent().getClass().getSimpleName(); + final ReportingInitializationContext config = new StandardReportingInitializationContext(identifier, taskName, + SchedulingStrategy.TIMER_DRIVEN, "1 min", taskComponent.getLogger(), serviceProvider, kerberosConfig, nodeTypeProvider); + + taskComponent.getComponent().initialize(config); + + return taskComponent; + } catch (final Exception e) { + throw new ReportingTaskInstantiationException(type, e); + } + } + + private LoggableComponent createLoggableComponent(Class nodeType) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); + try { + final Bundle bundle = extensionManager.getBundle(bundleCoordinate); + if (bundle == null) { + throw new IllegalStateException("Unable to find bundle for coordinate " + bundleCoordinate.getCoordinate()); + } + + final ClassLoader detectedClassLoader = extensionManager.createInstanceClassLoader(type, identifier, bundle, classpathUrls == null ? Collections.emptySet() : classpathUrls); + final Class rawClass = Class.forName(type, true, detectedClassLoader); + Thread.currentThread().setContextClassLoader(detectedClassLoader); + + final Object extensionInstance = rawClass.newInstance(); + final ComponentLog componentLog = new SimpleProcessLogger(identifier, extensionInstance); + final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLog); + + final T cast = nodeType.cast(extensionInstance); + return new LoggableComponent<>(cast, bundleCoordinate, terminationAwareLogger); + } finally { + if (ctxClassLoader != null) { + Thread.currentThread().setContextClassLoader(ctxClassLoader); + } + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java index ebd809c57230..8ab7e69d5456 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java @@ -16,28 +16,17 @@ */ package org.apache.nifi.controller; -import org.apache.commons.collections4.Predicate; import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.action.Action; import org.apache.nifi.admin.service.AuditService; -import org.apache.nifi.annotation.configuration.DefaultSettings; -import org.apache.nifi.annotation.lifecycle.OnAdded; import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored; -import org.apache.nifi.annotation.lifecycle.OnRemoved; import org.apache.nifi.annotation.lifecycle.OnShutdown; import org.apache.nifi.annotation.notification.OnPrimaryNodeStateChange; import org.apache.nifi.annotation.notification.PrimaryNodeState; import org.apache.nifi.authorization.Authorizer; -import org.apache.nifi.authorization.RequestAction; import org.apache.nifi.authorization.Resource; import org.apache.nifi.authorization.resource.Authorizable; -import org.apache.nifi.authorization.resource.DataAuthorizable; -import org.apache.nifi.authorization.resource.ProvenanceDataAuthorizable; import org.apache.nifi.authorization.resource.ResourceFactory; import org.apache.nifi.authorization.user.NiFiUser; -import org.apache.nifi.authorization.util.IdentityMapping; -import org.apache.nifi.authorization.util.IdentityMappingUtil; -import org.apache.nifi.bundle.Bundle; import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.coordination.heartbeat.HeartbeatMonitor; @@ -52,8 +41,6 @@ import org.apache.nifi.cluster.protocol.NodeProtocolSender; import org.apache.nifi.cluster.protocol.UnknownServiceAddressException; import org.apache.nifi.cluster.protocol.message.HeartbeatMessage; -import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.components.state.StateManager; import org.apache.nifi.components.state.StateManagerProvider; import org.apache.nifi.components.validation.StandardValidationTrigger; import org.apache.nifi.components.validation.TriggerValidationTask; @@ -63,19 +50,15 @@ import org.apache.nifi.connectable.ConnectableType; import org.apache.nifi.connectable.Connection; import org.apache.nifi.connectable.Funnel; -import org.apache.nifi.connectable.LocalPort; import org.apache.nifi.connectable.Port; import org.apache.nifi.connectable.Position; -import org.apache.nifi.connectable.Size; import org.apache.nifi.connectable.StandardConnection; import org.apache.nifi.controller.cluster.ClusterProtocolHeartbeater; import org.apache.nifi.controller.cluster.Heartbeater; import org.apache.nifi.controller.exception.CommunicationsException; -import org.apache.nifi.controller.exception.ComponentLifeCycleException; -import org.apache.nifi.controller.exception.ControllerServiceInstantiationException; -import org.apache.nifi.controller.exception.ProcessorInstantiationException; -import org.apache.nifi.controller.label.Label; -import org.apache.nifi.controller.label.StandardLabel; +import org.apache.nifi.controller.flow.FlowManager; +import org.apache.nifi.controller.flow.StandardFlowManager; +import org.apache.nifi.controller.kerberos.KerberosConfig; import org.apache.nifi.controller.leader.election.LeaderElectionManager; import org.apache.nifi.controller.leader.election.LeaderElectionStateChangeListener; import org.apache.nifi.controller.queue.ConnectionEventListener; @@ -97,19 +80,15 @@ import org.apache.nifi.controller.queue.clustered.server.StandardLoadBalanceProtocol; import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException; import org.apache.nifi.controller.reporting.ReportingTaskProvider; -import org.apache.nifi.controller.reporting.StandardReportingInitializationContext; -import org.apache.nifi.controller.reporting.StandardReportingTaskNode; import org.apache.nifi.controller.repository.ContentRepository; import org.apache.nifi.controller.repository.CounterRepository; -import org.apache.nifi.controller.repository.FlowFileEvent; import org.apache.nifi.controller.repository.FlowFileEventRepository; import org.apache.nifi.controller.repository.FlowFileRecord; import org.apache.nifi.controller.repository.FlowFileRepository; import org.apache.nifi.controller.repository.FlowFileSwapManager; -import org.apache.nifi.controller.repository.QueueProvider; -import org.apache.nifi.controller.repository.RepositoryStatusReport; import org.apache.nifi.controller.repository.StandardCounterRepository; import org.apache.nifi.controller.repository.StandardFlowFileRecord; +import org.apache.nifi.controller.repository.StandardQueueProvider; import org.apache.nifi.controller.repository.StandardRepositoryRecord; import org.apache.nifi.controller.repository.SwapManagerInitializationContext; import org.apache.nifi.controller.repository.SwapSummary; @@ -120,7 +99,6 @@ import org.apache.nifi.controller.repository.claim.StandardContentClaim; import org.apache.nifi.controller.repository.claim.StandardResourceClaimManager; import org.apache.nifi.controller.repository.io.LimitedInputStream; -import org.apache.nifi.controller.repository.metrics.EmptyFlowFileEvent; import org.apache.nifi.controller.scheduling.EventDrivenSchedulingAgent; import org.apache.nifi.controller.scheduling.QuartzSchedulingAgent; import org.apache.nifi.controller.scheduling.RepositoryContextFactory; @@ -131,20 +109,12 @@ import org.apache.nifi.controller.serialization.FlowSynchronizationException; import org.apache.nifi.controller.serialization.FlowSynchronizer; import org.apache.nifi.controller.serialization.ScheduledStateLookup; -import org.apache.nifi.controller.service.ControllerServiceInvocationHandler; import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.StandardConfigurationContext; import org.apache.nifi.controller.service.StandardControllerServiceProvider; import org.apache.nifi.controller.state.manager.StandardStateManagerProvider; import org.apache.nifi.controller.state.server.ZooKeeperStateServer; -import org.apache.nifi.controller.status.ConnectionStatus; -import org.apache.nifi.controller.status.PortStatus; -import org.apache.nifi.controller.status.ProcessGroupStatus; -import org.apache.nifi.controller.status.ProcessorStatus; -import org.apache.nifi.controller.status.RemoteProcessGroupStatus; -import org.apache.nifi.controller.status.RunStatus; -import org.apache.nifi.controller.status.TransmissionStatus; import org.apache.nifi.controller.status.history.ComponentStatusRepository; import org.apache.nifi.controller.status.history.GarbageCollectionHistory; import org.apache.nifi.controller.status.history.GarbageCollectionStatus; @@ -162,92 +132,47 @@ import org.apache.nifi.framework.security.util.SslContextFactory; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.groups.RemoteProcessGroup; -import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor; import org.apache.nifi.groups.StandardProcessGroup; -import org.apache.nifi.history.History; -import org.apache.nifi.logging.ComponentLog; -import org.apache.nifi.logging.ControllerServiceLogObserver; -import org.apache.nifi.logging.LogLevel; -import org.apache.nifi.logging.LogRepository; -import org.apache.nifi.logging.LogRepositoryFactory; -import org.apache.nifi.logging.ProcessorLogObserver; -import org.apache.nifi.logging.ReportingTaskLogObserver; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.nar.NarThreadContextClassLoader; -import org.apache.nifi.processor.GhostProcessor; import org.apache.nifi.processor.Processor; -import org.apache.nifi.processor.ProcessorInitializationContext; import org.apache.nifi.processor.Relationship; -import org.apache.nifi.processor.SimpleProcessLogger; -import org.apache.nifi.processor.StandardProcessContext; -import org.apache.nifi.processor.StandardProcessorInitializationContext; -import org.apache.nifi.processor.StandardValidationContextFactory; +import org.apache.nifi.provenance.ComponentIdentifierLookup; import org.apache.nifi.provenance.IdentifierLookup; import org.apache.nifi.provenance.ProvenanceAuthorizableFactory; import org.apache.nifi.provenance.ProvenanceEventRecord; import org.apache.nifi.provenance.ProvenanceEventType; import org.apache.nifi.provenance.ProvenanceRepository; +import org.apache.nifi.provenance.StandardProvenanceAuthorizableFactory; import org.apache.nifi.provenance.StandardProvenanceEventRecord; -import org.apache.nifi.registry.ComponentVariableRegistry; import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.registry.flow.FlowRegistryClient; -import org.apache.nifi.registry.flow.StandardVersionControlInformation; -import org.apache.nifi.registry.flow.VersionControlInformation; import org.apache.nifi.registry.flow.VersionedConnection; import org.apache.nifi.registry.flow.VersionedProcessGroup; import org.apache.nifi.registry.variable.MutableVariableRegistry; -import org.apache.nifi.registry.variable.StandardComponentVariableRegistry; import org.apache.nifi.remote.HttpRemoteSiteListener; import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.remote.RemoteResourceManager; import org.apache.nifi.remote.RemoteSiteListener; -import org.apache.nifi.remote.RootGroupPort; import org.apache.nifi.remote.SocketRemoteSiteListener; -import org.apache.nifi.remote.StandardRemoteProcessGroup; -import org.apache.nifi.remote.StandardRemoteProcessGroupPortDescriptor; -import org.apache.nifi.remote.StandardRootGroupPort; -import org.apache.nifi.remote.TransferDirection; import org.apache.nifi.remote.cluster.NodeInformant; -import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; import org.apache.nifi.remote.protocol.socket.SocketFlowFileServerProtocol; import org.apache.nifi.reporting.Bulletin; import org.apache.nifi.reporting.BulletinRepository; -import org.apache.nifi.reporting.EventAccess; -import org.apache.nifi.reporting.GhostReportingTask; -import org.apache.nifi.reporting.InitializationException; -import org.apache.nifi.reporting.ReportingInitializationContext; import org.apache.nifi.reporting.ReportingTask; import org.apache.nifi.reporting.Severity; -import org.apache.nifi.scheduling.ExecutionNode; +import org.apache.nifi.reporting.StandardEventAccess; +import org.apache.nifi.reporting.UserAwareEventAccess; import org.apache.nifi.scheduling.SchedulingStrategy; import org.apache.nifi.stream.io.LimitingInputStream; import org.apache.nifi.stream.io.StreamUtils; -import org.apache.nifi.util.BundleUtils; import org.apache.nifi.util.ComponentIdGenerator; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.ReflectionUtils; -import org.apache.nifi.util.SnippetUtils; import org.apache.nifi.util.concurrency.TimedLock; -import org.apache.nifi.web.ResourceNotFoundException; -import org.apache.nifi.web.api.dto.BatchSettingsDTO; -import org.apache.nifi.web.api.dto.BundleDTO; -import org.apache.nifi.web.api.dto.ConnectableDTO; -import org.apache.nifi.web.api.dto.ConnectionDTO; -import org.apache.nifi.web.api.dto.ControllerServiceDTO; -import org.apache.nifi.web.api.dto.FlowSnippetDTO; -import org.apache.nifi.web.api.dto.FunnelDTO; -import org.apache.nifi.web.api.dto.LabelDTO; -import org.apache.nifi.web.api.dto.PortDTO; import org.apache.nifi.web.api.dto.PositionDTO; -import org.apache.nifi.web.api.dto.ProcessGroupDTO; -import org.apache.nifi.web.api.dto.ProcessorConfigDTO; -import org.apache.nifi.web.api.dto.ProcessorDTO; -import org.apache.nifi.web.api.dto.RelationshipDTO; -import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO; -import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO; -import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO; import org.apache.nifi.web.api.dto.status.StatusHistoryDTO; import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException; import org.slf4j.Logger; @@ -255,28 +180,25 @@ import javax.net.ssl.SSLContext; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -288,8 +210,7 @@ import static java.util.Objects.requireNonNull; -public class FlowController implements EventAccess, ControllerServiceProvider, ReportingTaskProvider, - QueueProvider, Authorizable, ProvenanceAuthorizableFactory, NodeTypeProvider, IdentifierLookup, ReloadComponent { +public class FlowController implements ReportingTaskProvider, Authorizable, NodeTypeProvider { // default repository implementations public static final String DEFAULT_FLOWFILE_REPO_IMPLEMENTATION = "org.apache.nifi.controller.repository.WriteAheadFlowFileRepository"; @@ -303,8 +224,6 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R public static final long DEFAULT_GRACEFUL_SHUTDOWN_SECONDS = 10; public static final int METRICS_RESERVOIR_SIZE = 288; // 1 day worth of 5-minute captures - public static final String ROOT_GROUP_ID_ALIAS = "root"; - public static final String DEFAULT_ROOT_GROUP_NAME = "NiFi Flow"; // default properties for scaling the positions of components from pre-1.0 flow encoding versions. public static final double DEFAULT_POSITION_SCALE_FACTOR_X = 1.5; @@ -338,9 +257,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R private final ComponentStatusRepository componentStatusRepository; private final StateManagerProvider stateManagerProvider; private final long systemStartTime = System.currentTimeMillis(); // time at which the node was started - private final ConcurrentMap reportingTasks = new ConcurrentHashMap<>(); private final VariableRegistry variableRegistry; - private final ConcurrentMap rootControllerServices = new ConcurrentHashMap<>(); private final ConnectionLoadBalanceServer loadBalanceServer; private final NioAsyncLoadBalanceClientRegistry loadBalanceClientRegistry; @@ -366,7 +283,6 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R private final Integer remoteInputHttpPort; private final Boolean isSiteToSiteSecure; - private final AtomicReference rootGroupRef = new AtomicReference<>(); private final List startConnectablesAfterInitialization; private final List startRemoteGroupPortsAfterInitialization; private final LeaderElectionManager leaderElectionManager; @@ -374,6 +290,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R private final FlowRegistryClient flowRegistryClient; private final FlowEngine validationThreadPool; private final ValidationTrigger validationTrigger; + private final ReloadComponent reloadComponent; + private final ProvenanceAuthorizableFactory provenanceAuthorizableFactory; + private final UserAwareEventAccess eventAccess; + private final StandardFlowManager flowManager; /** * true if controller is configured to operate in a clustered environment @@ -441,7 +361,8 @@ public static FlowController createStandaloneInstance( final StringEncryptor encryptor, final BulletinRepository bulletinRepo, final VariableRegistry variableRegistry, - final FlowRegistryClient flowRegistryClient) { + final FlowRegistryClient flowRegistryClient, + final ExtensionManager extensionManager) { return new FlowController( flowFileEventRepo, @@ -456,7 +377,8 @@ public static FlowController createStandaloneInstance( /* heartbeat monitor */ null, /* leader election manager */ null, /* variable registry */ variableRegistry, - flowRegistryClient); + flowRegistryClient, + extensionManager); } public static FlowController createClusteredInstance( @@ -471,7 +393,8 @@ public static FlowController createClusteredInstance( final HeartbeatMonitor heartbeatMonitor, final LeaderElectionManager leaderElectionManager, final VariableRegistry variableRegistry, - final FlowRegistryClient flowRegistryClient) { + final FlowRegistryClient flowRegistryClient, + final ExtensionManager extensionManager) { final FlowController flowController = new FlowController( flowFileEventRepo, @@ -486,7 +409,8 @@ public static FlowController createClusteredInstance( heartbeatMonitor, leaderElectionManager, variableRegistry, - flowRegistryClient); + flowRegistryClient, + extensionManager); return flowController; } @@ -505,7 +429,8 @@ private FlowController( final HeartbeatMonitor heartbeatMonitor, final LeaderElectionManager leaderElectionManager, final VariableRegistry variableRegistry, - final FlowRegistryClient flowRegistryClient) { + final FlowRegistryClient flowRegistryClient, + final ExtensionManager extensionManager) { maxTimerDrivenThreads = new AtomicInteger(10); maxEventDrivenThreads = new AtomicInteger(5); @@ -513,14 +438,19 @@ private FlowController( this.encryptor = encryptor; this.nifiProperties = nifiProperties; this.heartbeatMonitor = heartbeatMonitor; - sslContext = SslContextFactory.createSslContext(nifiProperties, false); - extensionManager = new ExtensionManager(); + this.leaderElectionManager = leaderElectionManager; + this.sslContext = SslContextFactory.createSslContext(nifiProperties); + this.extensionManager = extensionManager; this.clusterCoordinator = clusterCoordinator; + this.authorizer = authorizer; + this.auditService = auditService; + this.configuredForClustering = configuredForClustering; + this.flowRegistryClient = flowRegistryClient; timerDrivenEngineRef = new AtomicReference<>(new FlowEngine(maxTimerDrivenThreads.get(), "Timer-Driven Process")); eventDrivenEngineRef = new AtomicReference<>(new FlowEngine(maxEventDrivenThreads.get(), "Event-Driven Process")); - final FlowFileRepository flowFileRepo = createFlowFileRepository(nifiProperties, resourceClaimManager); + final FlowFileRepository flowFileRepo = createFlowFileRepository(nifiProperties, extensionManager, resourceClaimManager); flowFileRepository = flowFileRepo; flowFileEventRepository = flowFileEventRepo; counterRepositoryRef = new AtomicReference<>(new StandardCounterRepository()); @@ -529,8 +459,12 @@ private FlowController( this.variableRegistry = variableRegistry == null ? VariableRegistry.EMPTY_REGISTRY : variableRegistry; try { + this.provenanceAuthorizableFactory = new StandardProvenanceAuthorizableFactory(this); this.provenanceRepository = createProvenanceRepository(nifiProperties); - this.provenanceRepository.initialize(createEventReporter(bulletinRepository), authorizer, this, this); + + final IdentifierLookup identifierLookup = new ComponentIdentifierLookup(this); + + this.provenanceRepository.initialize(createEventReporter(), authorizer, provenanceAuthorizableFactory, identifierLookup); } catch (final Exception e) { throw new RuntimeException("Unable to create Provenance Repository", e); } @@ -542,7 +476,7 @@ private FlowController( } try { - this.stateManagerProvider = StandardStateManagerProvider.create(nifiProperties, this.variableRegistry); + this.stateManagerProvider = StandardStateManagerProvider.create(nifiProperties, this.variableRegistry, extensionManager); } catch (final IOException e) { throw new RuntimeException(e); } @@ -552,8 +486,12 @@ private FlowController( final RepositoryContextFactory contextFactory = new RepositoryContextFactory(contentRepository, flowFileRepository, flowFileEventRepository, counterRepositoryRef.get(), provenanceRepository); + this.flowManager = new StandardFlowManager(nifiProperties, sslContext, this, flowFileEventRepository); + + controllerServiceProvider = new StandardControllerServiceProvider(this, processScheduler, bulletinRepository); + eventDrivenSchedulingAgent = new EventDrivenSchedulingAgent( - eventDrivenEngineRef.get(), this, stateManagerProvider, eventDrivenWorkerQueue, contextFactory, maxEventDrivenThreads.get(), encryptor); + eventDrivenEngineRef.get(), controllerServiceProvider, stateManagerProvider, eventDrivenWorkerQueue, contextFactory, maxEventDrivenThreads.get(), encryptor, extensionManager); processScheduler.setSchedulingAgent(SchedulingStrategy.EVENT_DRIVEN, eventDrivenSchedulingAgent); final QuartzSchedulingAgent quartzSchedulingAgent = new QuartzSchedulingAgent(this, timerDrivenEngineRef.get(), contextFactory, encryptor); @@ -565,9 +503,6 @@ private FlowController( startConnectablesAfterInitialization = new ArrayList<>(); startRemoteGroupPortsAfterInitialization = new ArrayList<>(); - this.authorizer = authorizer; - this.auditService = auditService; - this.flowRegistryClient = flowRegistryClient; final String gracefulShutdownSecondsVal = nifiProperties.getProperty(GRACEFUL_SHUTDOWN_PERIOD); long shutdownSecs; @@ -589,23 +524,20 @@ private FlowController( throw new IllegalStateException("NiFi Configured to allow Secure Site-to-Site communications but the Keystore/Truststore properties are not configured"); } - this.configuredForClustering = configuredForClustering; this.heartbeatDelaySeconds = (int) FormatUtils.getTimeDuration(nifiProperties.getNodeHeartbeatInterval(), TimeUnit.SECONDS); this.snippetManager = new SnippetManager(); + this.reloadComponent = new StandardReloadComponent(this); - final ProcessGroup rootGroup = new StandardProcessGroup(ComponentIdGenerator.generateId().toString(), this, processScheduler, + final ProcessGroup rootGroup = new StandardProcessGroup(ComponentIdGenerator.generateId().toString(), controllerServiceProvider, processScheduler, nifiProperties, encryptor, this, new MutableVariableRegistry(this.variableRegistry)); - rootGroup.setName(DEFAULT_ROOT_GROUP_NAME); + rootGroup.setName(FlowManager.DEFAULT_ROOT_GROUP_NAME); setRootGroup(rootGroup); instanceId = ComponentIdGenerator.generateId().toString(); this.validationThreadPool = new FlowEngine(5, "Validate Components", true); this.validationTrigger = new StandardValidationTrigger(validationThreadPool, this::isInitialized); - controllerServiceProvider = new StandardControllerServiceProvider(this, processScheduler, bulletinRepository, stateManagerProvider, - this.variableRegistry, this.nifiProperties, validationTrigger); - if (remoteInputSocketPort == null) { LOG.info("Not enabling RAW Socket Site-to-Site functionality because nifi.remote.input.socket.port is not set"); } else if (isSiteToSiteSecure && sslContext == null) { @@ -650,12 +582,13 @@ private FlowController( zooKeeperStateServer = null; } + eventAccess = new StandardEventAccess(this, flowFileEventRepository); componentStatusRepository = createComponentStatusRepository(); timerDrivenEngineRef.get().scheduleWithFixedDelay(new Runnable() { @Override public void run() { try { - componentStatusRepository.capture(getControllerStatus(), getGarbageCollectionStatus()); + componentStatusRepository.capture(eventAccess.getControllerStatus(), getGarbageCollectionStatus()); } catch (final Exception e) { LOG.error("Failed to capture component stats for Stats History", e); } @@ -664,7 +597,6 @@ public void run() { this.connectionStatus = new NodeConnectionStatus(nodeId, DisconnectionCode.NOT_YET_CONNECTED); heartbeatBeanRef.set(new HeartbeatBean(rootGroup, false)); - this.leaderElectionManager = leaderElectionManager; if (configuredForClustering) { heartbeater = new ClusterProtocolHeartbeater(protocolSender, clusterCoordinator, leaderElectionManager); @@ -699,8 +631,8 @@ public void run() { final InetSocketAddress loadBalanceAddress = nifiProperties.getClusterLoadBalanceAddress(); // Setup Load Balancing Server - final EventReporter eventReporter = createEventReporter(bulletinRepository); - final List identityMappings = IdentityMappingUtil.getIdentityMappings(nifiProperties); + final EventReporter eventReporter = createEventReporter(); + final LoadBalanceAuthorizer authorizeConnection = new ClusterLoadBalanceAuthorizer(clusterCoordinator, eventReporter); final LoadBalanceProtocol loadBalanceProtocol = new StandardLoadBalanceProtocol(flowFileRepo, contentRepository, provenanceRepository, this, authorizeConnection); @@ -743,7 +675,7 @@ public Resource getResource() { return ResourceFactory.getControllerResource(); } - private static FlowFileRepository createFlowFileRepository(final NiFiProperties properties, final ResourceClaimManager contentClaimManager) { + private static FlowFileRepository createFlowFileRepository(final NiFiProperties properties, final ExtensionManager extensionManager, final ResourceClaimManager contentClaimManager) { final String implementationClassName = properties.getProperty(NiFiProperties.FLOWFILE_REPOSITORY_IMPLEMENTATION, DEFAULT_FLOWFILE_REPO_IMPLEMENTATION); if (implementationClassName == null) { throw new RuntimeException("Cannot create FlowFile Repository because the NiFi Properties is missing the following property: " @@ -751,7 +683,7 @@ private static FlowFileRepository createFlowFileRepository(final NiFiProperties } try { - final FlowFileRepository created = NarThreadContextClassLoader.createInstance(implementationClassName, FlowFileRepository.class, properties); + final FlowFileRepository created = NarThreadContextClassLoader.createInstance(extensionManager, implementationClassName, FlowFileRepository.class, properties); synchronized (created) { created.initialize(contentClaimManager); } @@ -761,20 +693,44 @@ private static FlowFileRepository createFlowFileRepository(final NiFiProperties } } - private static FlowFileSwapManager createSwapManager(final NiFiProperties properties) { - final String implementationClassName = properties.getProperty(NiFiProperties.FLOWFILE_SWAP_MANAGER_IMPLEMENTATION, DEFAULT_SWAP_MANAGER_IMPLEMENTATION); + public FlowFileSwapManager createSwapManager() { + final String implementationClassName = nifiProperties.getProperty(NiFiProperties.FLOWFILE_SWAP_MANAGER_IMPLEMENTATION, DEFAULT_SWAP_MANAGER_IMPLEMENTATION); if (implementationClassName == null) { return null; } try { - return NarThreadContextClassLoader.createInstance(implementationClassName, FlowFileSwapManager.class, properties); + final FlowFileSwapManager swapManager = NarThreadContextClassLoader.createInstance(extensionManager, implementationClassName, FlowFileSwapManager.class, nifiProperties); + + final EventReporter eventReporter = createEventReporter(); + try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { + final SwapManagerInitializationContext initializationContext = new SwapManagerInitializationContext() { + @Override + public ResourceClaimManager getResourceClaimManager() { + return resourceClaimManager; + } + + @Override + public FlowFileRepository getFlowFileRepository() { + return flowFileRepository; + } + + @Override + public EventReporter getEventReporter() { + return eventReporter; + } + }; + + swapManager.initialize(initializationContext); + } + + return swapManager; } catch (final Exception e) { throw new RuntimeException(e); } } - private static EventReporter createEventReporter(final BulletinRepository bulletinRepository) { + public EventReporter createEventReporter() { return new EventReporter() { private static final long serialVersionUID = 1L; @@ -790,7 +746,7 @@ public void initializeFlow() throws IOException { writeLock.lock(); try { // get all connections/queues and recover from swap files. - final List connections = getGroup(getRootGroupId()).findAllConnections(); + final List connections = flowManager.getRootGroup().findAllConnections(); long maxIdFromSwapFiles = -1L; if (flowFileRepository.isVolatile()) { @@ -815,7 +771,7 @@ public void initializeFlow() throws IOException { } } - flowFileRepository.loadFlowFiles(this, maxIdFromSwapFiles + 1); + flowFileRepository.loadFlowFiles(new StandardQueueProvider(this), maxIdFromSwapFiles + 1); // Begin expiring FlowFiles that are old final RepositoryContextFactory contextFactory = new RepositoryContextFactory(contentRepository, flowFileRepository, @@ -853,7 +809,7 @@ public void run() { timerDrivenEngineRef.get().scheduleWithFixedDelay(new Runnable() { @Override public void run() { - final ProcessGroup rootGroup = getRootGroup(); + final ProcessGroup rootGroup = flowManager.getRootGroup(); final List allGroups = rootGroup.findAllProcessGroups(); allGroups.add(rootGroup); @@ -874,17 +830,17 @@ public void run() { } private void notifyComponentsConfigurationRestored() { - for (final ProcessorNode procNode : getGroup(getRootGroupId()).findAllProcessors()) { + for (final ProcessorNode procNode : flowManager.getRootGroup().findAllProcessors()) { final Processor processor = procNode.getProcessor(); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, processor.getClass(), processor.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, processor); } } - for (final ControllerServiceNode serviceNode : getAllControllerServices()) { + for (final ControllerServiceNode serviceNode : flowManager.getAllControllerServices()) { final ControllerService service = serviceNode.getControllerServiceImplementation(); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(service.getClass(), service.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, service.getClass(), service.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, service); } } @@ -892,7 +848,7 @@ private void notifyComponentsConfigurationRestored() { for (final ReportingTaskNode taskNode : getAllReportingTasks()) { final ReportingTask task = taskNode.getReportingTask(); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(task.getClass(), task.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, task.getClass(), task.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, task); } } @@ -939,13 +895,13 @@ public void trigger(final ComponentNode component) { } }; - new TriggerValidationTask(this, triggerIfValidating).run(); + new TriggerValidationTask(flowManager, triggerIfValidating).run(); final long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); LOG.info("Performed initial validation of all components in {} milliseconds", millis); // Trigger component validation to occur every 5 seconds. - validationThreadPool.scheduleWithFixedDelay(new TriggerValidationTask(this, validationTrigger), 5, 5, TimeUnit.SECONDS); + validationThreadPool.scheduleWithFixedDelay(new TriggerValidationTask(flowManager, validationTrigger), 5, 5, TimeUnit.SECONDS); if (startDelayedComponents) { LOG.info("Starting {} processors/ports/funnels", startConnectablesAfterInitialization.size() + startRemoteGroupPortsAfterInitialization.size()); @@ -956,7 +912,6 @@ public void trigger(final ComponentNode component) { try { if (connectable instanceof ProcessorNode) { - ((ProcessorNode) connectable).getValidationStatus(5, TimeUnit.SECONDS); connectable.getProcessGroup().startProcessor((ProcessorNode) connectable, true); } else { startConnectable(connectable); @@ -1000,7 +955,7 @@ public void trigger(final ComponentNode component) { startRemoteGroupPortsAfterInitialization.clear(); } - for (final Connection connection : getRootGroup().findAllConnections()) { + for (final Connection connection : flowManager.getRootGroup().findAllConnections()) { connection.getFlowFileQueue().startLoadBalancing(); } } finally { @@ -1020,7 +975,7 @@ private ContentRepository createContentRepository(final NiFiProperties propertie } try { - final ContentRepository contentRepo = NarThreadContextClassLoader.createInstance(implementationClassName, ContentRepository.class, properties); + final ContentRepository contentRepo = NarThreadContextClassLoader.createInstance(extensionManager, implementationClassName, ContentRepository.class, properties); synchronized (contentRepo) { contentRepo.initialize(resourceClaimManager); } @@ -1038,7 +993,7 @@ private ProvenanceRepository createProvenanceRepository(final NiFiProperties pro } try { - return NarThreadContextClassLoader.createInstance(implementationClassName, ProvenanceRepository.class, properties); + return NarThreadContextClassLoader.createInstance(extensionManager, implementationClassName, ProvenanceRepository.class, properties); } catch (final Exception e) { throw new RuntimeException(e); } @@ -1052,383 +1007,35 @@ private ComponentStatusRepository createComponentStatusRepository() { } try { - return NarThreadContextClassLoader.createInstance(implementationClassName, ComponentStatusRepository.class, nifiProperties); + return NarThreadContextClassLoader.createInstance(extensionManager, implementationClassName, ComponentStatusRepository.class, nifiProperties); } catch (final Exception e) { throw new RuntimeException(e); } } - /** - * Creates a connection between two Connectable objects. - * - * @param id required ID of the connection - * @param name the name of the connection, or null to leave the - * connection unnamed - * @param source required source - * @param destination required destination - * @param relationshipNames required collection of relationship names - * @return - * - * @throws NullPointerException if the ID, source, destination, or set of - * relationships is null. - * @throws IllegalArgumentException if relationships is an - * empty collection - */ - public Connection createConnection(final String id, final String name, final Connectable source, final Connectable destination, final Collection relationshipNames) { - final StandardConnection.Builder builder = new StandardConnection.Builder(processScheduler); - - final List relationships = new ArrayList<>(); - for (final String relationshipName : requireNonNull(relationshipNames)) { - relationships.add(new Relationship.Builder().name(relationshipName).build()); - } - - // Create and initialize a FlowFileSwapManager for this connection - final FlowFileSwapManager swapManager = createSwapManager(nifiProperties); - final EventReporter eventReporter = createEventReporter(getBulletinRepository()); - - try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { - final SwapManagerInitializationContext initializationContext = new SwapManagerInitializationContext() { - @Override - public ResourceClaimManager getResourceClaimManager() { - return resourceClaimManager; - } - - @Override - public FlowFileRepository getFlowFileRepository() { - return flowFileRepository; - } - - @Override - public EventReporter getEventReporter() { - return eventReporter; - } - }; - - swapManager.initialize(initializationContext); - } - - final FlowFileQueueFactory flowFileQueueFactory = new FlowFileQueueFactory() { - @Override - public FlowFileQueue createFlowFileQueue(final LoadBalanceStrategy loadBalanceStrategy, final String partitioningAttribute, final ConnectionEventListener eventListener) { - final FlowFileQueue flowFileQueue; - - if (clusterCoordinator == null) { - flowFileQueue = new StandardFlowFileQueue(id, eventListener, flowFileRepository, provenanceRepository, resourceClaimManager, processScheduler, swapManager, - eventReporter, nifiProperties.getQueueSwapThreshold(), nifiProperties.getDefaultBackPressureObjectThreshold(), nifiProperties.getDefaultBackPressureDataSizeThreshold()); - } else { - flowFileQueue = new SocketLoadBalancedFlowFileQueue(id, eventListener, processScheduler, flowFileRepository, provenanceRepository, contentRepository, resourceClaimManager, - clusterCoordinator, loadBalanceClientRegistry, swapManager, nifiProperties.getQueueSwapThreshold(), eventReporter); - - flowFileQueue.setBackPressureObjectThreshold(nifiProperties.getDefaultBackPressureObjectThreshold()); - flowFileQueue.setBackPressureDataSizeThreshold(nifiProperties.getDefaultBackPressureDataSizeThreshold()); - } - - return flowFileQueue; - } - }; - - final Connection connection = builder.id(requireNonNull(id).intern()) - .name(name == null ? null : name.intern()) - .relationships(relationships) - .source(requireNonNull(source)) - .destination(destination) - .flowFileQueueFactory(flowFileQueueFactory) - .build(); - - return connection; - } - - /** - * Creates a new Label - * - * @param id identifier - * @param text label text - * @return new label - * @throws NullPointerException if either argument is null - */ - public Label createLabel(final String id, final String text) { - return new StandardLabel(requireNonNull(id).intern(), text); - } - - /** - * Creates a funnel - * - * @param id funnel id - * @return new funnel - */ - public Funnel createFunnel(final String id) { - return new StandardFunnel(id.intern(), null, processScheduler); - } - - /** - * Creates a Port to use as an Input Port for a Process Group - * - * @param id port identifier - * @param name port name - * @return new port - * @throws NullPointerException if the ID or name is not unique - * @throws IllegalStateException if an Input Port already exists with the - * same name or id. - */ - public Port createLocalInputPort(String id, String name) { - id = requireNonNull(id).intern(); - name = requireNonNull(name).intern(); - verifyPortIdDoesNotExist(id); - return new LocalPort(id, name, null, ConnectableType.INPUT_PORT, processScheduler); - } - - /** - * Creates a Port to use as an Output Port for a Process Group - * - * @param id port id - * @param name port name - * @return new port - * @throws NullPointerException if the ID or name is not unique - * @throws IllegalStateException if an Input Port already exists with the - * same name or id. - */ - public Port createLocalOutputPort(String id, String name) { - id = requireNonNull(id).intern(); - name = requireNonNull(name).intern(); - verifyPortIdDoesNotExist(id); - return new LocalPort(id, name, null, ConnectableType.OUTPUT_PORT, processScheduler); - } - - /** - * Creates a ProcessGroup with the given ID - * - * @param id group id - * @return new group - * @throws NullPointerException if the argument is null - */ - public ProcessGroup createProcessGroup(final String id) { - return new StandardProcessGroup(requireNonNull(id).intern(), this, processScheduler, nifiProperties, encryptor, this, new MutableVariableRegistry(variableRegistry)); - } - - /** - *

- * Creates a new ProcessorNode with the given type and identifier and - * initializes it invoking the methods annotated with {@link OnAdded}. - *

- * - * @param type processor type - * @param id processor id - * @param coordinate the coordinate of the bundle for this processor - * @return new processor - * @throws NullPointerException if either arg is null - * @throws ProcessorInstantiationException if the processor cannot be - * instantiated for any reason - */ - public ProcessorNode createProcessor(final String type, final String id, final BundleCoordinate coordinate) throws ProcessorInstantiationException { - return createProcessor(type, id, coordinate, true); - } - - /** - *

- * Creates a new ProcessorNode with the given type and identifier and - * optionally initializes it. - *

- * - * @param type the fully qualified Processor class name - * @param id the unique ID of the Processor - * @param coordinate the bundle coordinate for this processor - * @param firstTimeAdded whether or not this is the first time this - * Processor is added to the graph. If {@code true}, will invoke methods - * annotated with the {@link OnAdded} annotation. - * @return new processor node - * @throws NullPointerException if either arg is null - * @throws ProcessorInstantiationException if the processor cannot be - * instantiated for any reason - */ - public ProcessorNode createProcessor(final String type, String id, final BundleCoordinate coordinate, final boolean firstTimeAdded) throws ProcessorInstantiationException { - return createProcessor(type, id, coordinate, Collections.emptySet(), firstTimeAdded, true); - } - - /** - *

- * Creates a new ProcessorNode with the given type and identifier and - * optionally initializes it. - *

- * - * @param type the fully qualified Processor class name - * @param id the unique ID of the Processor - * @param coordinate the bundle coordinate for this processor - * @param firstTimeAdded whether or not this is the first time this - * Processor is added to the graph. If {@code true}, will invoke methods - * annotated with the {@link OnAdded} annotation. - * @return new processor node - * @throws NullPointerException if either arg is null - * @throws ProcessorInstantiationException if the processor cannot be - * instantiated for any reason - */ - public ProcessorNode createProcessor(final String type, String id, final BundleCoordinate coordinate, final Set additionalUrls, - final boolean firstTimeAdded, final boolean registerLogObserver) throws ProcessorInstantiationException { - id = id.intern(); - - boolean creationSuccessful; - LoggableComponent processor; - - // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader - final LogRepository logRepository = LogRepositoryFactory.getRepository(id); - - try { - processor = instantiateProcessor(type, id, coordinate, additionalUrls); - creationSuccessful = true; - } catch (final ProcessorInstantiationException pie) { - LOG.error("Could not create Processor of type " + type + " for ID " + id + "; creating \"Ghost\" implementation", pie); - final GhostProcessor ghostProc = new GhostProcessor(); - ghostProc.setIdentifier(id); - ghostProc.setCanonicalClassName(type); - processor = new LoggableComponent<>(ghostProc, coordinate, null); - creationSuccessful = false; - } - - final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); - final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(controllerServiceProvider, componentVarRegistry); - final ProcessorNode procNode; - if (creationSuccessful) { - procNode = new StandardProcessorNode(processor, id, validationContextFactory, processScheduler, controllerServiceProvider, - nifiProperties, componentVarRegistry, this, validationTrigger); - } else { - final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; - final String componentType = "(Missing) " + simpleClassName; - procNode = new StandardProcessorNode(processor, id, validationContextFactory, processScheduler, controllerServiceProvider, - componentType, type, nifiProperties, componentVarRegistry, this, validationTrigger, true); - } - - if (registerLogObserver) { - logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, LogLevel.WARN, new ProcessorLogObserver(getBulletinRepository(), procNode)); - } - - try { - final Class procClass = procNode.getProcessor().getClass(); - if(procClass.isAnnotationPresent(DefaultSettings.class)) { - DefaultSettings ds = procClass.getAnnotation(DefaultSettings.class); - try { - procNode.setYieldPeriod(ds.yieldDuration()); - } catch(Throwable ex) { - LOG.error(String.format("Error while setting yield period from DefaultSettings annotation:%s",ex.getMessage()),ex); - } - try { - procNode.setPenalizationPeriod(ds.penaltyDuration()); - } catch(Throwable ex) { - LOG.error(String.format("Error while setting penalty duration from DefaultSettings annotation:%s",ex.getMessage()),ex); - } - // calling setBulletinLevel changes the level in the LogRepository so we only want to do this when - // the caller said to register the log observer, otherwise we could be changing the level when we didn't mean to - if (registerLogObserver) { - try { - procNode.setBulletinLevel(ds.bulletinLevel()); - } catch (Throwable ex) { - LOG.error(String.format("Error while setting bulletin level from DefaultSettings annotation:%s", ex.getMessage()), ex); - } - } - } - } catch (Throwable ex) { - LOG.error(String.format("Error while setting default settings from DefaultSettings annotation: %s",ex.getMessage()),ex); - } - if (firstTimeAdded) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(procNode.getProcessor().getClass(), procNode.getProcessor().getIdentifier())) { - ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, procNode.getProcessor()); - } catch (final Exception e) { - if (registerLogObserver) { - logRepository.removeObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID); - } - throw new ComponentLifeCycleException("Failed to invoke @OnAdded methods of " + procNode.getProcessor(), e); - } + public KerberosConfig createKerberosConfig(final NiFiProperties nifiProperties) { + final String principal = nifiProperties.getKerberosServicePrincipal(); + final String keytabLocation = nifiProperties.getKerberosServiceKeytabLocation(); + final File kerberosConfigFile = nifiProperties.getKerberosConfigurationFile(); - if (firstTimeAdded) { - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(procNode.getProcessor().getClass(), procNode.getProcessor().getIdentifier())) { - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, procNode.getProcessor()); - } - } + if (principal == null && keytabLocation == null && kerberosConfigFile == null) { + return KerberosConfig.NOT_CONFIGURED; } - return procNode; + final File keytabFile = keytabLocation == null ? null : new File(keytabLocation); + return new KerberosConfig(principal, keytabFile, kerberosConfigFile); } - private LoggableComponent instantiateProcessor(final String type, final String identifier, final BundleCoordinate bundleCoordinate, final Set additionalUrls) - throws ProcessorInstantiationException { - - final Bundle processorBundle = ExtensionManager.getBundle(bundleCoordinate); - if (processorBundle == null) { - throw new ProcessorInstantiationException("Unable to find bundle for coordinate " + bundleCoordinate.getCoordinate()); - } - - final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); - try { - final ClassLoader detectedClassLoaderForInstance = ExtensionManager.createInstanceClassLoader(type, identifier, processorBundle, additionalUrls); - final Class rawClass = Class.forName(type, true, detectedClassLoaderForInstance); - Thread.currentThread().setContextClassLoader(detectedClassLoaderForInstance); - - final Class processorClass = rawClass.asSubclass(Processor.class); - final Processor processor = processorClass.newInstance(); - - final ComponentLog componentLogger = new SimpleProcessLogger(identifier, processor); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); - final ProcessorInitializationContext ctx = new StandardProcessorInitializationContext(identifier, terminationAwareLogger, this, this, nifiProperties); - processor.initialize(ctx); - LogRepositoryFactory.getRepository(identifier).setLogger(terminationAwareLogger); - - return new LoggableComponent<>(processor, bundleCoordinate, terminationAwareLogger); - } catch (final Throwable t) { - throw new ProcessorInstantiationException(type, t); - } finally { - if (ctxClassLoader != null) { - Thread.currentThread().setContextClassLoader(ctxClassLoader); - } - } + public ValidationTrigger getValidationTrigger() { + return validationTrigger; } - @Override - public void reload(final ProcessorNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) - throws ProcessorInstantiationException { - if (existingNode == null) { - throw new IllegalStateException("Existing ProcessorNode cannot be null"); - } - - final String id = existingNode.getProcessor().getIdentifier(); - - // ghost components will have a null logger - if (existingNode.getLogger() != null) { - existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); - } - - // createProcessor will create a new instance class loader for the same id so - // save the instance class loader to use it for calling OnRemoved on the existing processor - final ClassLoader existingInstanceClassLoader = ExtensionManager.getInstanceClassLoader(id); - - // create a new node with firstTimeAdded as true so lifecycle methods get fired - // attempt the creation to make sure it works before firing the OnRemoved methods below - final ProcessorNode newNode = createProcessor(newType, id, bundleCoordinate, additionalUrls, true, false); - - // call OnRemoved for the existing processor using the previous instance class loader - try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { - final StateManager stateManager = getStateManagerProvider().getStateManager(id); - final StandardProcessContext processContext = new StandardProcessContext(existingNode, controllerServiceProvider, encryptor, stateManager, () -> false); - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getProcessor(), processContext); - } finally { - ExtensionManager.closeURLClassLoader(id, existingInstanceClassLoader); - } - - // set the new processor in the existing node - final ComponentLog componentLogger = new SimpleProcessLogger(id, newNode.getProcessor()); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); - LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); - - final LoggableComponent newProcessor = new LoggableComponent<>(newNode.getProcessor(), newNode.getBundleCoordinate(), terminationAwareLogger); - existingNode.setProcessor(newProcessor); - existingNode.setExtensionMissing(newNode.isExtensionMissing()); - - // need to refresh the properties in case we are changing from ghost component to real component - existingNode.refreshProperties(); - - LOG.debug("Triggering async validation of {} due to processor reload", existingNode); - validationTrigger.triggerAsync(existingNode); + public StringEncryptor getEncryptor() { + return encryptor; } /** @@ -1471,117 +1078,6 @@ public Authorizer getAuthorizer() { return authorizer; } - /** - * Creates a Port to use as an Input Port for the root Process Group, which - * is used for Site-to-Site communications - * - * @param id port id - * @param name port name - * @return new port - * @throws NullPointerException if the ID or name is not unique - * @throws IllegalStateException if an Input Port already exists with the - * same name or id. - */ - public Port createRemoteInputPort(String id, String name) { - id = requireNonNull(id).intern(); - name = requireNonNull(name).intern(); - verifyPortIdDoesNotExist(id); - return new StandardRootGroupPort(id, name, null, TransferDirection.RECEIVE, ConnectableType.INPUT_PORT, - authorizer, getBulletinRepository(), processScheduler, Boolean.TRUE.equals(isSiteToSiteSecure), nifiProperties); - } - - /** - * Creates a Port to use as an Output Port for the root Process Group, which - * is used for Site-to-Site communications and will queue flow files waiting - * to be delivered to remote instances - * - * @param id port id - * @param name port name - * @return new port - * @throws NullPointerException if the ID or name is not unique - * @throws IllegalStateException if an Input Port already exists with the - * same name or id. - */ - public Port createRemoteOutputPort(String id, String name) { - id = requireNonNull(id).intern(); - name = requireNonNull(name).intern(); - verifyPortIdDoesNotExist(id); - return new StandardRootGroupPort(id, name, null, TransferDirection.SEND, ConnectableType.OUTPUT_PORT, - authorizer, getBulletinRepository(), processScheduler, Boolean.TRUE.equals(isSiteToSiteSecure), nifiProperties); - } - - /** - * Creates a new Remote Process Group with the given ID that points to the - * given URI - * - * @param id group id - * @param uris group uris, multiple url can be specified in comma-separated format - * @return new group - * @throws NullPointerException if either argument is null - * @throws IllegalArgumentException if uri is not a valid URI. - */ - public RemoteProcessGroup createRemoteProcessGroup(final String id, final String uris) { - return new StandardRemoteProcessGroup(requireNonNull(id).intern(), uris, null, this, sslContext, nifiProperties); - } - - public ProcessGroup getRootGroup() { - return rootGroupRef.get(); - } - - /** - * Verifies that no output port exists with the given id or name. If this - * does not hold true, throws an IllegalStateException - * - * @param id port identifier - * @throws IllegalStateException port already exists - */ - private void verifyPortIdDoesNotExist(final String id) { - final ProcessGroup rootGroup = getRootGroup(); - Port port = rootGroup.findOutputPort(id); - if (port != null) { - throw new IllegalStateException("An Input Port already exists with ID " + id); - } - port = rootGroup.findInputPort(id); - if (port != null) { - throw new IllegalStateException("An Input Port already exists with ID " + id); - } - } - - /** - * @return the name of this controller, which is also the name of the Root - * Group. - */ - public String getName() { - return getRootGroup().getName(); - } - - /** - * Sets the name for the Root Group, which also changes the name for the - * controller. - * - * @param name of root group - */ - public void setName(final String name) { - getRootGroup().setName(name); - } - - /** - * @return the comments of this controller, which is also the comment of the - * Root Group - */ - public String getComments() { - return getRootGroup().getComments(); - } - - /** - * Sets the comments - * - * @param comments for the Root Group, which also changes the comment for - * the controller - */ - public void setComments(final String comments) { - getRootGroup().setComments(comments); - } /** * @return true if the scheduling engine for this controller @@ -1610,7 +1106,7 @@ public boolean isTerminated() { */ public void shutdown(final boolean kill) { this.shutdown = true; - stopAllProcessors(); + flowManager.getRootGroup().stopProcessing(); readLock.lock(); try { @@ -1649,13 +1145,14 @@ public void shutdown(final boolean kill) { loadBalanceClientTasks.forEach(NioAsyncLoadBalanceClientTask::stop); // Trigger any processors' methods marked with @OnShutdown to be called - getRootGroup().shutdown(); + flowManager.getRootGroup().shutdown(); stateManagerProvider.shutdown(); // invoke any methods annotated with @OnShutdown on Controller Services - for (final ControllerServiceNode serviceNode : getAllControllerServices()) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(serviceNode.getControllerServiceImplementation().getClass(), serviceNode.getIdentifier())) { + for (final ControllerServiceNode serviceNode : flowManager.getAllControllerServices()) { + final Class serviceImplClass = serviceNode.getControllerServiceImplementation().getClass(); + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, serviceImplClass, serviceNode.getIdentifier())) { final ConfigurationContext configContext = new StandardConfigurationContext(serviceNode, controllerServiceProvider, null, variableRegistry); ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnShutdown.class, serviceNode.getControllerServiceImplementation(), configContext); } @@ -1664,7 +1161,7 @@ public void shutdown(final boolean kill) { // invoke any methods annotated with @OnShutdown on Reporting Tasks for (final ReportingTaskNode taskNode : getAllReportingTasks()) { final ConfigurationContext configContext = taskNode.getConfigurationContext(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnShutdown.class, taskNode.getReportingTask(), configContext); } } @@ -1744,7 +1241,7 @@ public ScheduledState getScheduledState(final ProcessorNode procNode) { return ScheduledState.RUNNING; } - return procNode.getScheduledState(); + return procNode.getDesiredState(); } @Override @@ -1858,11 +1355,8 @@ private void setMaxThreadCount(final int maxThreadCount, final FlowEngine engine } } - /** - * @return the ID of the root group - */ - public String getRootGroupId() { - return getRootGroup().getIdentifier(); + public UserAwareEventAccess getEventAccess() { + return eventAccess; } /** @@ -1881,7 +1375,7 @@ void setRootGroup(final ProcessGroup group) { writeLock.lock(); try { - rootGroupRef.set(group); + flowManager.setRootGroup(group); for (final RemoteSiteListener listener : externalSiteListeners) { listener.setRootGroup(group); } @@ -1889,7 +1383,7 @@ void setRootGroup(final ProcessGroup group) { // update the heartbeat bean this.heartbeatBeanRef.set(new HeartbeatBean(group, isPrimary())); allProcessGroups.put(group.getIdentifier(), group); - allProcessGroups.put(ROOT_GROUP_ID_ALIAS, group); + allProcessGroups.put(FlowManager.ROOT_GROUP_ID_ALIAS, group); } finally { writeLock.unlock("setRootGroup"); } @@ -1915,38 +1409,6 @@ public String getProvenanceRepoFileStoreName(final String containerName) { // // ProcessGroup access // - /** - * Updates the process group corresponding to the specified DTO. Any field - * in DTO that is null (with the exception of the required ID) - * will be ignored. - * - * @param dto group - * @throws ProcessorInstantiationException - * - * @throws IllegalStateException if no process group can be found with the - * ID of DTO or with the ID of the DTO's parentGroupId, if the template ID - * specified is invalid, or if the DTO's Parent Group ID changes but the - * parent group has incoming or outgoing connections - * - * @throws NullPointerException if the DTO or its ID is null - */ - public void updateProcessGroup(final ProcessGroupDTO dto) throws ProcessorInstantiationException { - final ProcessGroup group = lookupGroup(requireNonNull(dto).getId()); - - final String name = dto.getName(); - final PositionDTO position = dto.getPosition(); - final String comments = dto.getComments(); - - if (name != null) { - group.setName(name); - } - if (position != null) { - group.setPosition(toPosition(position)); - } - if (comments != null) { - group.setComments(comments); - } - } private Position toPosition(final PositionDTO dto) { return new Position(dto.getX(), dto.getY()); @@ -1955,1549 +1417,153 @@ private Position toPosition(final PositionDTO dto) { // // Snippet // - /** - * Creates an instance of the given snippet and adds the components to the - * given group - * - * @param group group - * @param dto dto - * - * @throws NullPointerException if either argument is null - * @throws IllegalStateException if the snippet is not valid because a - * component in the snippet has an ID that is not unique to this flow, or - * because it shares an Input Port or Output Port at the root level whose - * name already exists in the given ProcessGroup, or because the Template - * contains a Processor or a Prioritizer whose class is not valid within - * this instance of NiFi. - * @throws ProcessorInstantiationException if unable to instantiate a - * processor - */ - public void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto) throws ProcessorInstantiationException { - instantiateSnippet(group, dto, true); - group.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize); + + private void verifyBundleInVersionedFlow(final org.apache.nifi.registry.flow.Bundle requiredBundle, final Set supportedBundles) { + final BundleCoordinate requiredCoordinate = new BundleCoordinate(requiredBundle.getGroup(), requiredBundle.getArtifact(), requiredBundle.getVersion()); + if (!supportedBundles.contains(requiredCoordinate)) { + throw new IllegalStateException("Unsupported bundle: " + requiredCoordinate); + } } - private void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto, final boolean topLevel) throws ProcessorInstantiationException { - validateSnippetContents(requireNonNull(group), dto); - writeLock.lock(); - try { - // - // Instantiate Controller Services - // - final List serviceNodes = new ArrayList<>(); - try { - for (final ControllerServiceDTO controllerServiceDTO : dto.getControllerServices()) { - final BundleCoordinate bundleCoordinate = BundleUtils.getBundle(controllerServiceDTO.getType(), controllerServiceDTO.getBundle()); - final ControllerServiceNode serviceNode = createControllerService(controllerServiceDTO.getType(), controllerServiceDTO.getId(), bundleCoordinate, Collections.emptySet(), true); - serviceNode.pauseValidationTrigger(); - serviceNodes.add(serviceNode); - - serviceNode.setAnnotationData(controllerServiceDTO.getAnnotationData()); - serviceNode.setComments(controllerServiceDTO.getComments()); - serviceNode.setName(controllerServiceDTO.getName()); - if (!topLevel) { - serviceNode.setVersionedComponentId(controllerServiceDTO.getVersionedComponentId()); - } - - group.addControllerService(serviceNode); - } - - // configure controller services. We do this after creating all of them in case 1 service - // references another service. - for (final ControllerServiceDTO controllerServiceDTO : dto.getControllerServices()) { - final String serviceId = controllerServiceDTO.getId(); - final ControllerServiceNode serviceNode = getControllerServiceNode(serviceId); - serviceNode.setProperties(controllerServiceDTO.getProperties()); - } - } finally { - serviceNodes.stream().forEach(ControllerServiceNode::resumeValidationTrigger); - } - - // - // Instantiate the labels - // - for (final LabelDTO labelDTO : dto.getLabels()) { - final Label label = createLabel(labelDTO.getId(), labelDTO.getLabel()); - label.setPosition(toPosition(labelDTO.getPosition())); - if (labelDTO.getWidth() != null && labelDTO.getHeight() != null) { - label.setSize(new Size(labelDTO.getWidth(), labelDTO.getHeight())); - } - - label.setStyle(labelDTO.getStyle()); - if (!topLevel) { - label.setVersionedComponentId(labelDTO.getVersionedComponentId()); - } - - group.addLabel(label); - } - - // Instantiate the funnels - for (final FunnelDTO funnelDTO : dto.getFunnels()) { - final Funnel funnel = createFunnel(funnelDTO.getId()); - funnel.setPosition(toPosition(funnelDTO.getPosition())); - if (!topLevel) { - funnel.setVersionedComponentId(funnelDTO.getVersionedComponentId()); - } - - group.addFunnel(funnel); - } - - // - // Instantiate Input Ports & Output Ports - // - for (final PortDTO portDTO : dto.getInputPorts()) { - final Port inputPort; - if (group.isRootGroup()) { - inputPort = createRemoteInputPort(portDTO.getId(), portDTO.getName()); - inputPort.setMaxConcurrentTasks(portDTO.getConcurrentlySchedulableTaskCount()); - if (portDTO.getGroupAccessControl() != null) { - ((RootGroupPort) inputPort).setGroupAccessControl(portDTO.getGroupAccessControl()); - } - if (portDTO.getUserAccessControl() != null) { - ((RootGroupPort) inputPort).setUserAccessControl(portDTO.getUserAccessControl()); - } - } else { - inputPort = createLocalInputPort(portDTO.getId(), portDTO.getName()); - } - - if (!topLevel) { - inputPort.setVersionedComponentId(portDTO.getVersionedComponentId()); - } - inputPort.setPosition(toPosition(portDTO.getPosition())); - inputPort.setProcessGroup(group); - inputPort.setComments(portDTO.getComments()); - group.addInputPort(inputPort); - } - - for (final PortDTO portDTO : dto.getOutputPorts()) { - final Port outputPort; - if (group.isRootGroup()) { - outputPort = createRemoteOutputPort(portDTO.getId(), portDTO.getName()); - outputPort.setMaxConcurrentTasks(portDTO.getConcurrentlySchedulableTaskCount()); - if (portDTO.getGroupAccessControl() != null) { - ((RootGroupPort) outputPort).setGroupAccessControl(portDTO.getGroupAccessControl()); - } - if (portDTO.getUserAccessControl() != null) { - ((RootGroupPort) outputPort).setUserAccessControl(portDTO.getUserAccessControl()); - } - } else { - outputPort = createLocalOutputPort(portDTO.getId(), portDTO.getName()); - } - - if (!topLevel) { - outputPort.setVersionedComponentId(portDTO.getVersionedComponentId()); - } - outputPort.setPosition(toPosition(portDTO.getPosition())); - outputPort.setProcessGroup(group); - outputPort.setComments(portDTO.getComments()); - group.addOutputPort(outputPort); - } - - // - // Instantiate the processors - // - for (final ProcessorDTO processorDTO : dto.getProcessors()) { - final BundleCoordinate bundleCoordinate = BundleUtils.getBundle(processorDTO.getType(), processorDTO.getBundle()); - final ProcessorNode procNode = createProcessor(processorDTO.getType(), processorDTO.getId(), bundleCoordinate); - procNode.pauseValidationTrigger(); - - try { - procNode.setPosition(toPosition(processorDTO.getPosition())); - procNode.setProcessGroup(group); - if (!topLevel) { - procNode.setVersionedComponentId(processorDTO.getVersionedComponentId()); - } - - final ProcessorConfigDTO config = processorDTO.getConfig(); - procNode.setComments(config.getComments()); - if (config.isLossTolerant() != null) { - procNode.setLossTolerant(config.isLossTolerant()); - } - procNode.setName(processorDTO.getName()); - - procNode.setYieldPeriod(config.getYieldDuration()); - procNode.setPenalizationPeriod(config.getPenaltyDuration()); - procNode.setBulletinLevel(LogLevel.valueOf(config.getBulletinLevel())); - procNode.setAnnotationData(config.getAnnotationData()); - procNode.setStyle(processorDTO.getStyle()); - - if (config.getRunDurationMillis() != null) { - procNode.setRunDuration(config.getRunDurationMillis(), TimeUnit.MILLISECONDS); - } - - if (config.getSchedulingStrategy() != null) { - procNode.setSchedulingStrategy(SchedulingStrategy.valueOf(config.getSchedulingStrategy())); - } - - if (config.getExecutionNode() != null) { - procNode.setExecutionNode(ExecutionNode.valueOf(config.getExecutionNode())); - } - - if (processorDTO.getState().equals(ScheduledState.DISABLED.toString())) { - procNode.disable(); - } - - // ensure that the scheduling strategy is set prior to these values - procNode.setMaxConcurrentTasks(config.getConcurrentlySchedulableTaskCount()); - procNode.setScheduldingPeriod(config.getSchedulingPeriod()); - - final Set relationships = new HashSet<>(); - if (processorDTO.getRelationships() != null) { - for (final RelationshipDTO rel : processorDTO.getRelationships()) { - if (rel.isAutoTerminate()) { - relationships.add(procNode.getRelationship(rel.getName())); - } - } - procNode.setAutoTerminatedRelationships(relationships); - } - - if (config.getProperties() != null) { - procNode.setProperties(config.getProperties()); - } - - group.addProcessor(procNode); - } finally { - procNode.resumeValidationTrigger(); - } - } - - // - // Instantiate Remote Process Groups - // - for (final RemoteProcessGroupDTO remoteGroupDTO : dto.getRemoteProcessGroups()) { - final RemoteProcessGroup remoteGroup = createRemoteProcessGroup(remoteGroupDTO.getId(), remoteGroupDTO.getTargetUris()); - remoteGroup.setComments(remoteGroupDTO.getComments()); - remoteGroup.setPosition(toPosition(remoteGroupDTO.getPosition())); - remoteGroup.setCommunicationsTimeout(remoteGroupDTO.getCommunicationsTimeout()); - remoteGroup.setYieldDuration(remoteGroupDTO.getYieldDuration()); - if (!topLevel) { - remoteGroup.setVersionedComponentId(remoteGroupDTO.getVersionedComponentId()); - } - - if (remoteGroupDTO.getTransportProtocol() == null) { - remoteGroup.setTransportProtocol(SiteToSiteTransportProtocol.RAW); - } else { - remoteGroup.setTransportProtocol(SiteToSiteTransportProtocol.valueOf(remoteGroupDTO.getTransportProtocol())); - } - - remoteGroup.setProxyHost(remoteGroupDTO.getProxyHost()); - remoteGroup.setProxyPort(remoteGroupDTO.getProxyPort()); - remoteGroup.setProxyUser(remoteGroupDTO.getProxyUser()); - remoteGroup.setProxyPassword(remoteGroupDTO.getProxyPassword()); - remoteGroup.setProcessGroup(group); - - // set the input/output ports - if (remoteGroupDTO.getContents() != null) { - final RemoteProcessGroupContentsDTO contents = remoteGroupDTO.getContents(); - - // ensure there are input ports - if (contents.getInputPorts() != null) { - remoteGroup.setInputPorts(convertRemotePort(contents.getInputPorts()), false); - } - - // ensure there are output ports - if (contents.getOutputPorts() != null) { - remoteGroup.setOutputPorts(convertRemotePort(contents.getOutputPorts()), false); - } - } - - group.addRemoteProcessGroup(remoteGroup); - } - - // - // Instantiate ProcessGroups - // - for (final ProcessGroupDTO groupDTO : dto.getProcessGroups()) { - final ProcessGroup childGroup = createProcessGroup(groupDTO.getId()); - childGroup.setParent(group); - childGroup.setPosition(toPosition(groupDTO.getPosition())); - childGroup.setComments(groupDTO.getComments()); - childGroup.setName(groupDTO.getName()); - if (groupDTO.getVariables() != null) { - childGroup.setVariables(groupDTO.getVariables()); - } - - // If this Process Group is 'top level' then we do not set versioned component ID's. - // We do this only if this component is the child of a Versioned Component. - if (!topLevel) { - childGroup.setVersionedComponentId(groupDTO.getVersionedComponentId()); - } - - group.addProcessGroup(childGroup); - - final FlowSnippetDTO contents = groupDTO.getContents(); - - // we want this to be recursive, so we will create a new template that contains only - // the contents of this child group and recursively call ourselves. - final FlowSnippetDTO childTemplateDTO = new FlowSnippetDTO(); - childTemplateDTO.setConnections(contents.getConnections()); - childTemplateDTO.setInputPorts(contents.getInputPorts()); - childTemplateDTO.setLabels(contents.getLabels()); - childTemplateDTO.setOutputPorts(contents.getOutputPorts()); - childTemplateDTO.setProcessGroups(contents.getProcessGroups()); - childTemplateDTO.setProcessors(contents.getProcessors()); - childTemplateDTO.setFunnels(contents.getFunnels()); - childTemplateDTO.setRemoteProcessGroups(contents.getRemoteProcessGroups()); - childTemplateDTO.setControllerServices(contents.getControllerServices()); - instantiateSnippet(childGroup, childTemplateDTO, false); - - if (groupDTO.getVersionControlInformation() != null) { - final VersionControlInformation vci = StandardVersionControlInformation.Builder - .fromDto(groupDTO.getVersionControlInformation()) - .build(); - childGroup.setVersionControlInformation(vci, Collections.emptyMap()); - } - } - - // - // Instantiate Connections - // - for (final ConnectionDTO connectionDTO : dto.getConnections()) { - final ConnectableDTO sourceDTO = connectionDTO.getSource(); - final ConnectableDTO destinationDTO = connectionDTO.getDestination(); - final Connectable source; - final Connectable destination; - - // locate the source and destination connectable. if this is a remote port - // we need to locate the remote process groups. otherwise we need to - // find the connectable given its parent group. - // NOTE: (getConnectable returns ANY connectable, when the parent is - // not this group only input ports or output ports should be returned. if something - // other than a port is returned, an exception will be thrown when adding the - // connection below.) - // see if the source connectable is a remote port - if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceDTO.getType())) { - final RemoteProcessGroup remoteGroup = group.getRemoteProcessGroup(sourceDTO.getGroupId()); - source = remoteGroup.getOutputPort(sourceDTO.getId()); - } else { - final ProcessGroup sourceGroup = getConnectableParent(group, sourceDTO.getGroupId()); - source = sourceGroup.getConnectable(sourceDTO.getId()); - } - - // see if the destination connectable is a remote port - if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationDTO.getType())) { - final RemoteProcessGroup remoteGroup = group.getRemoteProcessGroup(destinationDTO.getGroupId()); - destination = remoteGroup.getInputPort(destinationDTO.getId()); - } else { - final ProcessGroup destinationGroup = getConnectableParent(group, destinationDTO.getGroupId()); - destination = destinationGroup.getConnectable(destinationDTO.getId()); - } - - // determine the selection relationships for this connection - final Set relationships = new HashSet<>(); - if (connectionDTO.getSelectedRelationships() != null) { - relationships.addAll(connectionDTO.getSelectedRelationships()); - } - - final Connection connection = createConnection(connectionDTO.getId(), connectionDTO.getName(), source, destination, relationships); - if (!topLevel) { - connection.setVersionedComponentId(connectionDTO.getVersionedComponentId()); - } - - if (connectionDTO.getBends() != null) { - final List bendPoints = new ArrayList<>(); - for (final PositionDTO bend : connectionDTO.getBends()) { - bendPoints.add(new Position(bend.getX(), bend.getY())); - } - connection.setBendPoints(bendPoints); - } - - final FlowFileQueue queue = connection.getFlowFileQueue(); - queue.setBackPressureDataSizeThreshold(connectionDTO.getBackPressureDataSizeThreshold()); - queue.setBackPressureObjectThreshold(connectionDTO.getBackPressureObjectThreshold()); - queue.setFlowFileExpiration(connectionDTO.getFlowFileExpiration()); - - final List prioritizers = connectionDTO.getPrioritizers(); - if (prioritizers != null) { - final List newPrioritizersClasses = new ArrayList<>(prioritizers); - final List newPrioritizers = new ArrayList<>(); - for (final String className : newPrioritizersClasses) { - try { - newPrioritizers.add(createPrioritizer(className)); - } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e); - } - } - queue.setPriorities(newPrioritizers); - } - - final String loadBalanceStrategyName = connectionDTO.getLoadBalanceStrategy(); - if (loadBalanceStrategyName != null) { - final LoadBalanceStrategy loadBalanceStrategy = LoadBalanceStrategy.valueOf(loadBalanceStrategyName); - final String partitioningAttribute = connectionDTO.getLoadBalancePartitionAttribute(); - queue.setLoadBalanceStrategy(loadBalanceStrategy, partitioningAttribute); - } - - connection.setProcessGroup(group); - group.addConnection(connection); - } - } finally { - writeLock.unlock("instantiateSnippet"); - } - } - - /** - * Converts a set of ports into a set of remote process group ports. - * - * @param ports ports - * @return group descriptors - */ - private Set convertRemotePort(final Set ports) { - Set remotePorts = null; - if (ports != null) { - remotePorts = new LinkedHashSet<>(ports.size()); - for (final RemoteProcessGroupPortDTO port : ports) { - final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor(); - descriptor.setId(port.getId()); - descriptor.setVersionedComponentId(port.getVersionedComponentId()); - descriptor.setTargetId(port.getTargetId()); - descriptor.setName(port.getName()); - descriptor.setComments(port.getComments()); - descriptor.setTargetRunning(port.isTargetRunning()); - descriptor.setConnected(port.isConnected()); - descriptor.setConcurrentlySchedulableTaskCount(port.getConcurrentlySchedulableTaskCount()); - descriptor.setTransmitting(port.isTransmitting()); - descriptor.setUseCompression(port.getUseCompression()); - final BatchSettingsDTO batchSettings = port.getBatchSettings(); - if (batchSettings != null) { - descriptor.setBatchCount(batchSettings.getCount()); - descriptor.setBatchSize(batchSettings.getSize()); - descriptor.setBatchDuration(batchSettings.getDuration()); - } - remotePorts.add(descriptor); - } - } - return remotePorts; - } - - /** - * Returns the parent of the specified Connectable. This only considers this - * group and any direct child sub groups. - * - * @param parentGroupId group id - * @return parent group - */ - private ProcessGroup getConnectableParent(final ProcessGroup group, final String parentGroupId) { - if (areGroupsSame(group.getIdentifier(), parentGroupId)) { - return group; - } else { - return group.getProcessGroup(parentGroupId); - } - } - - private void verifyBundleInSnippet(final BundleDTO requiredBundle, final Set supportedBundles) { - final BundleCoordinate requiredCoordinate = new BundleCoordinate(requiredBundle.getGroup(), requiredBundle.getArtifact(), requiredBundle.getVersion()); - if (!supportedBundles.contains(requiredCoordinate)) { - throw new IllegalStateException("Unsupported bundle: " + requiredCoordinate); - } - } - - private void verifyBundleInVersionedFlow(final org.apache.nifi.registry.flow.Bundle requiredBundle, final Set supportedBundles) { - final BundleCoordinate requiredCoordinate = new BundleCoordinate(requiredBundle.getGroup(), requiredBundle.getArtifact(), requiredBundle.getVersion()); - if (!supportedBundles.contains(requiredCoordinate)) { - throw new IllegalStateException("Unsupported bundle: " + requiredCoordinate); - } - } - - private void verifyProcessorsInSnippet(final FlowSnippetDTO templateContents, final Map> supportedTypes) { - if (templateContents.getProcessors() != null) { - templateContents.getProcessors().forEach(processor -> { - if (processor.getBundle() == null) { - throw new IllegalArgumentException("Processor bundle must be specified."); - } - - if (supportedTypes.containsKey(processor.getType())) { - verifyBundleInSnippet(processor.getBundle(), supportedTypes.get(processor.getType())); - } else { - throw new IllegalStateException("Invalid Processor Type: " + processor.getType()); - } - }); - } - - if (templateContents.getProcessGroups() != null) { - templateContents.getProcessGroups().forEach(processGroup -> { - verifyProcessorsInSnippet(processGroup.getContents(), supportedTypes); - }); - } - } private void verifyProcessorsInVersionedFlow(final VersionedProcessGroup versionedFlow, final Map> supportedTypes) { if (versionedFlow.getProcessors() != null) { versionedFlow.getProcessors().forEach(processor -> { if (processor.getBundle() == null) { throw new IllegalArgumentException("Processor bundle must be specified."); - } - - if (supportedTypes.containsKey(processor.getType())) { - verifyBundleInVersionedFlow(processor.getBundle(), supportedTypes.get(processor.getType())); - } else { - throw new IllegalStateException("Invalid Processor Type: " + processor.getType()); - } - }); - } - - if (versionedFlow.getProcessGroups() != null) { - versionedFlow.getProcessGroups().forEach(processGroup -> { - verifyProcessorsInVersionedFlow(processGroup, supportedTypes); - }); - } - } - - private void verifyControllerServicesInSnippet(final FlowSnippetDTO templateContents, final Map> supportedTypes) { - if (templateContents.getControllerServices() != null) { - templateContents.getControllerServices().forEach(controllerService -> { - if (supportedTypes.containsKey(controllerService.getType())) { - if (controllerService.getBundle() == null) { - throw new IllegalArgumentException("Controller Service bundle must be specified."); - } - - verifyBundleInSnippet(controllerService.getBundle(), supportedTypes.get(controllerService.getType())); - } else { - throw new IllegalStateException("Invalid Controller Service Type: " + controllerService.getType()); - } - }); - } - - if (templateContents.getProcessGroups() != null) { - templateContents.getProcessGroups().forEach(processGroup -> { - verifyControllerServicesInSnippet(processGroup.getContents(), supportedTypes); - }); - } - } - - private void verifyControllerServicesInVersionedFlow(final VersionedProcessGroup versionedFlow, final Map> supportedTypes) { - if (versionedFlow.getControllerServices() != null) { - versionedFlow.getControllerServices().forEach(controllerService -> { - if (supportedTypes.containsKey(controllerService.getType())) { - if (controllerService.getBundle() == null) { - throw new IllegalArgumentException("Controller Service bundle must be specified."); - } - - verifyBundleInVersionedFlow(controllerService.getBundle(), supportedTypes.get(controllerService.getType())); - } else { - throw new IllegalStateException("Invalid Controller Service Type: " + controllerService.getType()); - } - }); - } - - if (versionedFlow.getProcessGroups() != null) { - versionedFlow.getProcessGroups().forEach(processGroup -> { - verifyControllerServicesInVersionedFlow(processGroup, supportedTypes); - }); - } - } - - public void verifyComponentTypesInSnippet(final FlowSnippetDTO templateContents) { - final Map> processorClasses = new HashMap<>(); - for (final Class c : ExtensionManager.getExtensions(Processor.class)) { - final String name = c.getName(); - processorClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); - } - verifyProcessorsInSnippet(templateContents, processorClasses); - - final Map> controllerServiceClasses = new HashMap<>(); - for (final Class c : ExtensionManager.getExtensions(ControllerService.class)) { - final String name = c.getName(); - controllerServiceClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); - } - verifyControllerServicesInSnippet(templateContents, controllerServiceClasses); - - final Set prioritizerClasses = new HashSet<>(); - for (final Class c : ExtensionManager.getExtensions(FlowFilePrioritizer.class)) { - prioritizerClasses.add(c.getName()); - } - - final Set allConns = new HashSet<>(); - allConns.addAll(templateContents.getConnections()); - for (final ProcessGroupDTO childGroup : templateContents.getProcessGroups()) { - allConns.addAll(findAllConnections(childGroup)); - } - - for (final ConnectionDTO conn : allConns) { - final List prioritizers = conn.getPrioritizers(); - if (prioritizers != null) { - for (final String prioritizer : prioritizers) { - if (!prioritizerClasses.contains(prioritizer)) { - throw new IllegalStateException("Invalid FlowFile Prioritizer Type: " + prioritizer); - } - } - } - } - } - - public void verifyComponentTypesInSnippet(final VersionedProcessGroup versionedFlow) { - final Map> processorClasses = new HashMap<>(); - for (final Class c : ExtensionManager.getExtensions(Processor.class)) { - final String name = c.getName(); - processorClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); - } - verifyProcessorsInVersionedFlow(versionedFlow, processorClasses); - - final Map> controllerServiceClasses = new HashMap<>(); - for (final Class c : ExtensionManager.getExtensions(ControllerService.class)) { - final String name = c.getName(); - controllerServiceClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); - } - verifyControllerServicesInVersionedFlow(versionedFlow, controllerServiceClasses); - - final Set prioritizerClasses = new HashSet<>(); - for (final Class c : ExtensionManager.getExtensions(FlowFilePrioritizer.class)) { - prioritizerClasses.add(c.getName()); - } - - final Set allConns = new HashSet<>(); - allConns.addAll(versionedFlow.getConnections()); - for (final VersionedProcessGroup childGroup : versionedFlow.getProcessGroups()) { - allConns.addAll(findAllConnections(childGroup)); - } - - for (final VersionedConnection conn : allConns) { - final List prioritizers = conn.getPrioritizers(); - if (prioritizers != null) { - for (final String prioritizer : prioritizers) { - if (!prioritizerClasses.contains(prioritizer)) { - throw new IllegalStateException("Invalid FlowFile Prioritizer Type: " + prioritizer); - } - } - } - } - } - - /** - *

- * Verifies that the given DTO is valid, according to the following: - * - *

    - *
  • None of the ID's in any component of the DTO can be used in this - * flow.
  • - *
  • The ProcessGroup to which the template's contents will be added must - * not contain any InputPort or OutputPort with the same name as one of the - * corresponding components in the root level of the template.
  • - *
  • All Processors' classes must exist in this instance.
  • - *
  • All Flow File Prioritizers' classes must exist in this instance.
  • - *
- *

- * - *

- * If any of the above statements does not hold true, an - * {@link IllegalStateException} or a - * {@link ProcessorInstantiationException} will be thrown. - *

- * - * @param group group - * @param snippetContents contents - */ - private void validateSnippetContents(final ProcessGroup group, final FlowSnippetDTO snippetContents) { - // validate the names of Input Ports - for (final PortDTO port : snippetContents.getInputPorts()) { - if (group.getInputPortByName(port.getName()) != null) { - throw new IllegalStateException("One or more of the proposed Port names is not available in the process group"); - } - } - - // validate the names of Output Ports - for (final PortDTO port : snippetContents.getOutputPorts()) { - if (group.getOutputPortByName(port.getName()) != null) { - throw new IllegalStateException("One or more of the proposed Port names is not available in the process group"); - } - } - - verifyComponentTypesInSnippet(snippetContents); - - SnippetUtils.verifyNoVersionControlConflicts(snippetContents, group); - } - - - /** - * Recursively finds all ConnectionDTO's - * - * @param group group - * @return connection dtos - */ - private Set findAllConnections(final ProcessGroupDTO group) { - final Set conns = new HashSet<>(); - for (final ConnectionDTO dto : group.getContents().getConnections()) { - conns.add(dto); - } - - for (final ProcessGroupDTO childGroup : group.getContents().getProcessGroups()) { - conns.addAll(findAllConnections(childGroup)); - } - return conns; - } - - private Set findAllConnections(final VersionedProcessGroup group) { - final Set conns = new HashSet<>(); - for (final VersionedConnection connection : group.getConnections()) { - conns.add(connection); - } - - for (final VersionedProcessGroup childGroup : group.getProcessGroups()) { - conns.addAll(findAllConnections(childGroup)); - } - return conns; - } - - // - // Processor access - // - /** - * Indicates whether or not the two ID's point to the same ProcessGroup. If - * either id is null, will return false. - * - * @param id1 group id - * @param id2 other group id - * @return true if same - */ - public boolean areGroupsSame(final String id1, final String id2) { - if (id1 == null || id2 == null) { - return false; - } else if (id1.equals(id2)) { - return true; - } else { - final String comparable1 = id1.equals(ROOT_GROUP_ID_ALIAS) ? getRootGroupId() : id1; - final String comparable2 = id2.equals(ROOT_GROUP_ID_ALIAS) ? getRootGroupId() : id2; - return comparable1.equals(comparable2); - } - } - - public FlowFilePrioritizer createPrioritizer(final String type) throws InstantiationException, IllegalAccessException, ClassNotFoundException { - FlowFilePrioritizer prioritizer; - - final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); - try { - final List prioritizerBundles = ExtensionManager.getBundles(type); - if (prioritizerBundles.size() == 0) { - throw new IllegalStateException(String.format("The specified class '%s' is not known to this nifi.", type)); - } - if (prioritizerBundles.size() > 1) { - throw new IllegalStateException(String.format("Multiple bundles found for the specified class '%s', only one is allowed.", type)); - } - - final Bundle bundle = prioritizerBundles.get(0); - final ClassLoader detectedClassLoaderForType = bundle.getClassLoader(); - final Class rawClass = Class.forName(type, true, detectedClassLoaderForType); - - Thread.currentThread().setContextClassLoader(detectedClassLoaderForType); - final Class prioritizerClass = rawClass.asSubclass(FlowFilePrioritizer.class); - final Object processorObj = prioritizerClass.newInstance(); - prioritizer = prioritizerClass.cast(processorObj); - - return prioritizer; - } finally { - if (ctxClassLoader != null) { - Thread.currentThread().setContextClassLoader(ctxClassLoader); - } - } - } - - // - // InputPort access - // - public PortDTO updateInputPort(final String parentGroupId, final PortDTO dto) { - final ProcessGroup parentGroup = lookupGroup(parentGroupId); - final Port port = parentGroup.getInputPort(dto.getId()); - if (port == null) { - throw new IllegalStateException("No Input Port with ID " + dto.getId() + " is known as a child of ProcessGroup with ID " + parentGroupId); - } - - final String name = dto.getName(); - if (dto.getPosition() != null) { - port.setPosition(toPosition(dto.getPosition())); - } - - if (name != null) { - port.setName(name); - } - - return createDTO(port); - } - - private PortDTO createDTO(final Port port) { - if (port == null) { - return null; - } - - final PortDTO dto = new PortDTO(); - dto.setId(port.getIdentifier()); - dto.setPosition(new PositionDTO(port.getPosition().getX(), port.getPosition().getY())); - dto.setName(port.getName()); - dto.setParentGroupId(port.getProcessGroup().getIdentifier()); - - return dto; - } - - // - // OutputPort access - // - public PortDTO updateOutputPort(final String parentGroupId, final PortDTO dto) { - final ProcessGroup parentGroup = lookupGroup(parentGroupId); - final Port port = parentGroup.getOutputPort(dto.getId()); - if (port == null) { - throw new IllegalStateException("No Output Port with ID " + dto.getId() + " is known as a child of ProcessGroup with ID " + parentGroupId); - } - - final String name = dto.getName(); - if (name != null) { - port.setName(name); - } - - if (dto.getPosition() != null) { - port.setPosition(toPosition(dto.getPosition())); - } - - return createDTO(port); - } - - // - // Processor/Prioritizer/Filter Class Access - // - @SuppressWarnings("rawtypes") - public Set getFlowFileProcessorClasses() { - return ExtensionManager.getExtensions(Processor.class); - } - - @SuppressWarnings("rawtypes") - public Set getFlowFileComparatorClasses() { - return ExtensionManager.getExtensions(FlowFilePrioritizer.class); - } - - /** - * Returns the ProcessGroup with the given ID - * - * @param id group - * @return the process group or null if not group is found - */ - private ProcessGroup lookupGroup(final String id) { - final ProcessGroup group = getGroup(id); - if (group == null) { - throw new IllegalStateException("No Group with ID " + id + " exists"); - } - return group; - } - - /** - * Returns the ProcessGroup with the given ID - * - * @param id group id - * @return the process group or null if not group is found - */ - public ProcessGroup getGroup(final String id) { - return allProcessGroups.get(requireNonNull(id)); - } - - public void onProcessGroupAdded(final ProcessGroup group) { - allProcessGroups.put(group.getIdentifier(), group); - } - - public void onProcessGroupRemoved(final ProcessGroup group) { - allProcessGroups.remove(group.getIdentifier()); - } - - public void onProcessorAdded(final ProcessorNode procNode) { - allProcessors.put(procNode.getIdentifier(), procNode); - } - - public void onProcessorRemoved(final ProcessorNode procNode) { - String identifier = procNode.getIdentifier(); - flowFileEventRepository.purgeTransferEvents(identifier); - allProcessors.remove(identifier); - } - - public Connectable findLocalConnectable(final String id) { - final ProcessorNode procNode = getProcessorNode(id); - if (procNode != null) { - return procNode; - } - - final Port inPort = getInputPort(id); - if (inPort != null) { - return inPort; - } - - final Port outPort = getOutputPort(id); - if (outPort != null) { - return outPort; - } - - final Funnel funnel = getFunnel(id); - if (funnel != null) { - return funnel; - } - - final RemoteGroupPort remoteGroupPort = getRootGroup().findRemoteGroupPort(id); - if (remoteGroupPort != null) { - return remoteGroupPort; - } - - return null; - } - - public ProcessorNode getProcessorNode(final String id) { - return allProcessors.get(id); - } - - public void onConnectionAdded(final Connection connection) { - allConnections.put(connection.getIdentifier(), connection); - - if (isInitialized()) { - connection.getFlowFileQueue().startLoadBalancing(); - } - } - - public void onConnectionRemoved(final Connection connection) { - String identifier = connection.getIdentifier(); - flowFileEventRepository.purgeTransferEvents(identifier); - allConnections.remove(identifier); - } - - public Connection getConnection(final String id) { - return allConnections.get(id); - } - - public void onInputPortAdded(final Port inputPort) { - allInputPorts.put(inputPort.getIdentifier(), inputPort); - } - - public void onInputPortRemoved(final Port inputPort) { - String identifier = inputPort.getIdentifier(); - flowFileEventRepository.purgeTransferEvents(identifier); - allInputPorts.remove(identifier); - } - - public Port getInputPort(final String id) { - return allInputPorts.get(id); - } - - public void onOutputPortAdded(final Port outputPort) { - allOutputPorts.put(outputPort.getIdentifier(), outputPort); - } - - public void onOutputPortRemoved(final Port outputPort) { - String identifier = outputPort.getIdentifier(); - flowFileEventRepository.purgeTransferEvents(identifier); - allOutputPorts.remove(identifier); - } - - public Port getOutputPort(final String id) { - return allOutputPorts.get(id); - } - - public void onFunnelAdded(final Funnel funnel) { - allFunnels.put(funnel.getIdentifier(), funnel); - } - - public void onFunnelRemoved(final Funnel funnel) { - String identifier = funnel.getIdentifier(); - flowFileEventRepository.purgeTransferEvents(identifier); - allFunnels.remove(identifier); - } - - public Funnel getFunnel(final String id) { - return allFunnels.get(id); - } - - public List getGarbageCollectionStatus() { - final List statuses = new ArrayList<>(); - - final Date now = new Date(); - for (final GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { - final String managerName = mbean.getName(); - final long count = mbean.getCollectionCount(); - final long millis = mbean.getCollectionTime(); - final GarbageCollectionStatus status = new StandardGarbageCollectionStatus(managerName, now, count, millis); - statuses.add(status); - } - - return statuses; - } - - public GarbageCollectionHistory getGarbageCollectionHistory() { - return componentStatusRepository.getGarbageCollectionHistory(new Date(0L), new Date()); - } - - /** - * Returns the status of all components in the controller. This request is - * not in the context of a user so the results will be unfiltered. - * - * @return the component status - */ - @Override - public ProcessGroupStatus getControllerStatus() { - return getGroupStatus(getRootGroupId()); - } - - /** - * Returns the status of all components in the specified group. This request - * is not in the context of a user so the results will be unfiltered. - * - * @param groupId group id - * @return the component status - */ - @Override - public ProcessGroupStatus getGroupStatus(final String groupId) { - return getGroupStatus(groupId, getProcessorStats()); - } - - /** - * Returns the status for components in the specified group. This request is - * made by the specified user so the results will be filtered accordingly. - * - * @param groupId group id - * @param user user making request - * @return the component status - */ - public ProcessGroupStatus getGroupStatus(final String groupId, final NiFiUser user) { - return getGroupStatus(groupId, getProcessorStats(), user); - } - - /** - * Returns the status for components in the specified group. This request is - * made by the specified user so the results will be filtered accordingly. - * - * @param groupId group id - * @param user user making request - * @return the component status - */ - public ProcessGroupStatus getGroupStatus(final String groupId, final NiFiUser user, final int recursiveStatusDepth) { - return getGroupStatus(groupId, getProcessorStats(), user, recursiveStatusDepth); - } + } - /** - * Returns the status for the components in the specified group with the - * specified report. This request is not in the context of a user so the - * results will be unfiltered. - * - * @param groupId group id - * @param statusReport report - * @return the component status - */ - public ProcessGroupStatus getGroupStatus(final String groupId, final RepositoryStatusReport statusReport) { - final ProcessGroup group = getGroup(groupId); + if (supportedTypes.containsKey(processor.getType())) { + verifyBundleInVersionedFlow(processor.getBundle(), supportedTypes.get(processor.getType())); + } else { + throw new IllegalStateException("Invalid Processor Type: " + processor.getType()); + } + }); + } - // this was invoked with no user context so the results will be unfiltered... necessary for aggregating status history - return getGroupStatus(group, statusReport, authorizable -> true, Integer.MAX_VALUE, 1); + if (versionedFlow.getProcessGroups() != null) { + versionedFlow.getProcessGroups().forEach(processGroup -> { + verifyProcessorsInVersionedFlow(processGroup, supportedTypes); + }); + } } - /** - * Returns the status for the components in the specified group with the - * specified report. This request is made by the specified user so the - * results will be filtered accordingly. - * - * @param groupId group id - * @param statusReport report - * @param user user making request - * @return the component status - */ - public ProcessGroupStatus getGroupStatus(final String groupId, final RepositoryStatusReport statusReport, final NiFiUser user) { - final ProcessGroup group = getGroup(groupId); - - // on demand status request for a specific user... require authorization per component and filter results as appropriate - return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(authorizer, RequestAction.READ, user), Integer.MAX_VALUE, 1); - } + private void verifyControllerServicesInVersionedFlow(final VersionedProcessGroup versionedFlow, final Map> supportedTypes) { + if (versionedFlow.getControllerServices() != null) { + versionedFlow.getControllerServices().forEach(controllerService -> { + if (supportedTypes.containsKey(controllerService.getType())) { + if (controllerService.getBundle() == null) { + throw new IllegalArgumentException("Controller Service bundle must be specified."); + } - /** - * Returns the status for the components in the specified group with the - * specified report. This request is made by the specified user so the - * results will be filtered accordingly. - * - * @param groupId group id - * @param statusReport report - * @param user user making request - * @param recursiveStatusDepth the number of levels deep we should recurse and still include the the processors' statuses, the groups' statuses, etc. in the returned ProcessGroupStatus - * @return the component status - */ - public ProcessGroupStatus getGroupStatus(final String groupId, final RepositoryStatusReport statusReport, final NiFiUser user, final int recursiveStatusDepth) { - final ProcessGroup group = getGroup(groupId); + verifyBundleInVersionedFlow(controllerService.getBundle(), supportedTypes.get(controllerService.getType())); + } else { + throw new IllegalStateException("Invalid Controller Service Type: " + controllerService.getType()); + } + }); + } - // on demand status request for a specific user... require authorization per component and filter results as appropriate - return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(authorizer, RequestAction.READ, user), recursiveStatusDepth, 1); + if (versionedFlow.getProcessGroups() != null) { + versionedFlow.getProcessGroups().forEach(processGroup -> { + verifyControllerServicesInVersionedFlow(processGroup, supportedTypes); + }); + } } - /** - * Returns the status for the components in the specified group with the - * specified report. The results will be filtered by executing the specified - * predicate. - * - * @param group group id - * @param statusReport report - * @param isAuthorized is authorized check - * @param recursiveStatusDepth the number of levels deep we should recurse and still include the the processors' statuses, the groups' statuses, etc. in the returned ProcessGroupStatus - * @param currentDepth the current number of levels deep that we have recursed - * @return the component status - */ - private ProcessGroupStatus getGroupStatus(final ProcessGroup group, final RepositoryStatusReport statusReport, final Predicate isAuthorized, - final int recursiveStatusDepth, final int currentDepth) { - if (group == null) { - return null; + public void verifyComponentTypesInSnippet(final VersionedProcessGroup versionedFlow) { + final Map> processorClasses = new HashMap<>(); + for (final Class c : extensionManager.getExtensions(Processor.class)) { + final String name = c.getName(); + processorClasses.put(name, extensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); } + verifyProcessorsInVersionedFlow(versionedFlow, processorClasses); - final ProcessGroupStatus status = new ProcessGroupStatus(); - status.setId(group.getIdentifier()); - status.setName(isAuthorized.evaluate(group) ? group.getName() : group.getIdentifier()); - int activeGroupThreads = 0; - int terminatedGroupThreads = 0; - long bytesRead = 0L; - long bytesWritten = 0L; - int queuedCount = 0; - long queuedContentSize = 0L; - int flowFilesIn = 0; - long bytesIn = 0L; - int flowFilesOut = 0; - long bytesOut = 0L; - int flowFilesReceived = 0; - long bytesReceived = 0L; - int flowFilesSent = 0; - long bytesSent = 0L; - int flowFilesTransferred = 0; - long bytesTransferred = 0; - - final boolean populateChildStatuses = currentDepth <= recursiveStatusDepth; - - // set status for processors - final Collection processorStatusCollection = new ArrayList<>(); - status.setProcessorStatus(processorStatusCollection); - for (final ProcessorNode procNode : group.getProcessors()) { - final ProcessorStatus procStat = getProcessorStatus(statusReport, procNode, isAuthorized); - if (populateChildStatuses) { - processorStatusCollection.add(procStat); - } - activeGroupThreads += procStat.getActiveThreadCount(); - terminatedGroupThreads += procStat.getTerminatedThreadCount(); - bytesRead += procStat.getBytesRead(); - bytesWritten += procStat.getBytesWritten(); - - flowFilesReceived += procStat.getFlowFilesReceived(); - bytesReceived += procStat.getBytesReceived(); - flowFilesSent += procStat.getFlowFilesSent(); - bytesSent += procStat.getBytesSent(); + final Map> controllerServiceClasses = new HashMap<>(); + for (final Class c : extensionManager.getExtensions(ControllerService.class)) { + final String name = c.getName(); + controllerServiceClasses.put(name, extensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); } + verifyControllerServicesInVersionedFlow(versionedFlow, controllerServiceClasses); - // set status for local child groups - final Collection localChildGroupStatusCollection = new ArrayList<>(); - status.setProcessGroupStatus(localChildGroupStatusCollection); - for (final ProcessGroup childGroup : group.getProcessGroups()) { - final ProcessGroupStatus childGroupStatus; - if (populateChildStatuses) { - childGroupStatus = getGroupStatus(childGroup, statusReport, isAuthorized, recursiveStatusDepth, currentDepth + 1); - localChildGroupStatusCollection.add(childGroupStatus); - } else { - // In this case, we don't want to include any of the recursive components' individual statuses. As a result, we can - // avoid performing any sort of authorizations. Because we only care about the numbers that come back, we can just indicate - // that the user is not authorized. This allows us to avoid the expense of both performing the authorization and calculating - // things that we would otherwise need to calculate if the user were in fact authorized. - childGroupStatus = getGroupStatus(childGroup, statusReport, authorizable -> false, recursiveStatusDepth, currentDepth + 1); - } - - activeGroupThreads += childGroupStatus.getActiveThreadCount(); - terminatedGroupThreads += childGroupStatus.getTerminatedThreadCount(); - bytesRead += childGroupStatus.getBytesRead(); - bytesWritten += childGroupStatus.getBytesWritten(); - queuedCount += childGroupStatus.getQueuedCount(); - queuedContentSize += childGroupStatus.getQueuedContentSize(); - - flowFilesReceived += childGroupStatus.getFlowFilesReceived(); - bytesReceived += childGroupStatus.getBytesReceived(); - flowFilesSent += childGroupStatus.getFlowFilesSent(); - bytesSent += childGroupStatus.getBytesSent(); - - flowFilesTransferred += childGroupStatus.getFlowFilesTransferred(); - bytesTransferred += childGroupStatus.getBytesTransferred(); + final Set prioritizerClasses = new HashSet<>(); + for (final Class c : extensionManager.getExtensions(FlowFilePrioritizer.class)) { + prioritizerClasses.add(c.getName()); } - // set status for remote child groups - final Collection remoteProcessGroupStatusCollection = new ArrayList<>(); - status.setRemoteProcessGroupStatus(remoteProcessGroupStatusCollection); - for (final RemoteProcessGroup remoteGroup : group.getRemoteProcessGroups()) { - final RemoteProcessGroupStatus remoteStatus = createRemoteGroupStatus(remoteGroup, statusReport, isAuthorized); - if (remoteStatus != null) { - if (populateChildStatuses) { - remoteProcessGroupStatusCollection.add(remoteStatus); - } - - flowFilesReceived += remoteStatus.getReceivedCount(); - bytesReceived += remoteStatus.getReceivedContentSize(); - flowFilesSent += remoteStatus.getSentCount(); - bytesSent += remoteStatus.getSentContentSize(); - } + final Set allConns = new HashSet<>(); + allConns.addAll(versionedFlow.getConnections()); + for (final VersionedProcessGroup childGroup : versionedFlow.getProcessGroups()) { + allConns.addAll(findAllConnections(childGroup)); } - // connection status - final Collection connectionStatusCollection = new ArrayList<>(); - status.setConnectionStatus(connectionStatusCollection); - - // get the connection and remote port status - for (final Connection conn : group.getConnections()) { - final boolean isConnectionAuthorized = isAuthorized.evaluate(conn); - final boolean isSourceAuthorized = isAuthorized.evaluate(conn.getSource()); - final boolean isDestinationAuthorized = isAuthorized.evaluate(conn.getDestination()); - - final ConnectionStatus connStatus = new ConnectionStatus(); - connStatus.setId(conn.getIdentifier()); - connStatus.setGroupId(conn.getProcessGroup().getIdentifier()); - connStatus.setSourceId(conn.getSource().getIdentifier()); - connStatus.setSourceName(isSourceAuthorized ? conn.getSource().getName() : conn.getSource().getIdentifier()); - connStatus.setDestinationId(conn.getDestination().getIdentifier()); - connStatus.setDestinationName(isDestinationAuthorized ? conn.getDestination().getName() : conn.getDestination().getIdentifier()); - connStatus.setBackPressureDataSizeThreshold(conn.getFlowFileQueue().getBackPressureDataSizeThreshold()); - connStatus.setBackPressureObjectThreshold(conn.getFlowFileQueue().getBackPressureObjectThreshold()); - - final FlowFileEvent connectionStatusReport = statusReport.getReportEntry(conn.getIdentifier()); - if (connectionStatusReport != null) { - connStatus.setInputBytes(connectionStatusReport.getContentSizeIn()); - connStatus.setInputCount(connectionStatusReport.getFlowFilesIn()); - connStatus.setOutputBytes(connectionStatusReport.getContentSizeOut()); - connStatus.setOutputCount(connectionStatusReport.getFlowFilesOut()); - - flowFilesTransferred += connectionStatusReport.getFlowFilesIn() + connectionStatusReport.getFlowFilesOut(); - bytesTransferred += connectionStatusReport.getContentSizeIn() + connectionStatusReport.getContentSizeOut(); - } - - if (isConnectionAuthorized) { - if (StringUtils.isNotBlank(conn.getName())) { - connStatus.setName(conn.getName()); - } else if (conn.getRelationships() != null && !conn.getRelationships().isEmpty()) { - final Collection relationships = new ArrayList<>(conn.getRelationships().size()); - for (final Relationship relationship : conn.getRelationships()) { - relationships.add(relationship.getName()); + for (final VersionedConnection conn : allConns) { + final List prioritizers = conn.getPrioritizers(); + if (prioritizers != null) { + for (final String prioritizer : prioritizers) { + if (!prioritizerClasses.contains(prioritizer)) { + throw new IllegalStateException("Invalid FlowFile Prioritizer Type: " + prioritizer); } - connStatus.setName(StringUtils.join(relationships, ", ")); } - } else { - connStatus.setName(conn.getIdentifier()); - } - - final QueueSize queueSize = conn.getFlowFileQueue().size(); - final int connectionQueuedCount = queueSize.getObjectCount(); - final long connectionQueuedBytes = queueSize.getByteCount(); - if (connectionQueuedCount > 0) { - connStatus.setQueuedBytes(connectionQueuedBytes); - connStatus.setQueuedCount(connectionQueuedCount); - } - - if (populateChildStatuses) { - connectionStatusCollection.add(connStatus); - } - - queuedCount += connectionQueuedCount; - queuedContentSize += connectionQueuedBytes; - - final Connectable source = conn.getSource(); - if (ConnectableType.REMOTE_OUTPUT_PORT.equals(source.getConnectableType())) { - final RemoteGroupPort remoteOutputPort = (RemoteGroupPort) source; - activeGroupThreads += processScheduler.getActiveThreadCount(remoteOutputPort); - } - - final Connectable destination = conn.getDestination(); - if (ConnectableType.REMOTE_INPUT_PORT.equals(destination.getConnectableType())) { - final RemoteGroupPort remoteInputPort = (RemoteGroupPort) destination; - activeGroupThreads += processScheduler.getActiveThreadCount(remoteInputPort); - } - } - - // status for input ports - final Collection inputPortStatusCollection = new ArrayList<>(); - status.setInputPortStatus(inputPortStatusCollection); - - final Set inputPorts = group.getInputPorts(); - for (final Port port : inputPorts) { - final boolean isInputPortAuthorized = isAuthorized.evaluate(port); - - final PortStatus portStatus = new PortStatus(); - portStatus.setId(port.getIdentifier()); - portStatus.setGroupId(port.getProcessGroup().getIdentifier()); - portStatus.setName(isInputPortAuthorized ? port.getName() : port.getIdentifier()); - portStatus.setActiveThreadCount(processScheduler.getActiveThreadCount(port)); - - // determine the run status - if (ScheduledState.RUNNING.equals(port.getScheduledState())) { - portStatus.setRunStatus(RunStatus.Running); - } else if (ScheduledState.DISABLED.equals(port.getScheduledState())) { - portStatus.setRunStatus(RunStatus.Disabled); - } else if (!port.isValid()) { - portStatus.setRunStatus(RunStatus.Invalid); - } else { - portStatus.setRunStatus(RunStatus.Stopped); - } - - // special handling for root group ports - if (port instanceof RootGroupPort) { - final RootGroupPort rootGroupPort = (RootGroupPort) port; - portStatus.setTransmitting(rootGroupPort.isTransmitting()); - } - - final FlowFileEvent entry = statusReport.getReportEntries().get(port.getIdentifier()); - if (entry == null) { - portStatus.setInputBytes(0L); - portStatus.setInputCount(0); - portStatus.setOutputBytes(0L); - portStatus.setOutputCount(0); - } else { - final int processedCount = entry.getFlowFilesOut(); - final long numProcessedBytes = entry.getContentSizeOut(); - portStatus.setOutputBytes(numProcessedBytes); - portStatus.setOutputCount(processedCount); - - final int inputCount = entry.getFlowFilesIn(); - final long inputBytes = entry.getContentSizeIn(); - portStatus.setInputBytes(inputBytes); - portStatus.setInputCount(inputCount); - - flowFilesIn += inputCount; - bytesIn += inputBytes; - bytesWritten += entry.getBytesWritten(); - - flowFilesReceived += entry.getFlowFilesReceived(); - bytesReceived += entry.getBytesReceived(); - } - - if (populateChildStatuses) { - inputPortStatusCollection.add(portStatus); - } - - activeGroupThreads += portStatus.getActiveThreadCount(); - } - - // status for output ports - final Collection outputPortStatusCollection = new ArrayList<>(); - status.setOutputPortStatus(outputPortStatusCollection); - - final Set outputPorts = group.getOutputPorts(); - for (final Port port : outputPorts) { - final boolean isOutputPortAuthorized = isAuthorized.evaluate(port); - - final PortStatus portStatus = new PortStatus(); - portStatus.setId(port.getIdentifier()); - portStatus.setGroupId(port.getProcessGroup().getIdentifier()); - portStatus.setName(isOutputPortAuthorized ? port.getName() : port.getIdentifier()); - portStatus.setActiveThreadCount(processScheduler.getActiveThreadCount(port)); - - // determine the run status - if (ScheduledState.RUNNING.equals(port.getScheduledState())) { - portStatus.setRunStatus(RunStatus.Running); - } else if (ScheduledState.DISABLED.equals(port.getScheduledState())) { - portStatus.setRunStatus(RunStatus.Disabled); - } else if (!port.isValid()) { - portStatus.setRunStatus(RunStatus.Invalid); - } else { - portStatus.setRunStatus(RunStatus.Stopped); - } - - // special handling for root group ports - if (port instanceof RootGroupPort) { - final RootGroupPort rootGroupPort = (RootGroupPort) port; - portStatus.setTransmitting(rootGroupPort.isTransmitting()); - } - - final FlowFileEvent entry = statusReport.getReportEntries().get(port.getIdentifier()); - if (entry == null) { - portStatus.setInputBytes(0L); - portStatus.setInputCount(0); - portStatus.setOutputBytes(0L); - portStatus.setOutputCount(0); - } else { - final int processedCount = entry.getFlowFilesOut(); - final long numProcessedBytes = entry.getContentSizeOut(); - portStatus.setOutputBytes(numProcessedBytes); - portStatus.setOutputCount(processedCount); - - final int inputCount = entry.getFlowFilesIn(); - final long inputBytes = entry.getContentSizeIn(); - portStatus.setInputBytes(inputBytes); - portStatus.setInputCount(inputCount); - - bytesRead += entry.getBytesRead(); - - flowFilesOut += entry.getFlowFilesOut(); - bytesOut += entry.getContentSizeOut(); - - flowFilesSent = entry.getFlowFilesSent(); - bytesSent += entry.getBytesSent(); - } - - if (populateChildStatuses) { - outputPortStatusCollection.add(portStatus); } - - activeGroupThreads += portStatus.getActiveThreadCount(); } + } - for (final Funnel funnel : group.getFunnels()) { - activeGroupThreads += processScheduler.getActiveThreadCount(funnel); + private Set findAllConnections(final VersionedProcessGroup group) { + final Set conns = new HashSet<>(); + for (final VersionedConnection connection : group.getConnections()) { + conns.add(connection); } - status.setActiveThreadCount(activeGroupThreads); - status.setTerminatedThreadCount(terminatedGroupThreads); - status.setBytesRead(bytesRead); - status.setBytesWritten(bytesWritten); - status.setQueuedCount(queuedCount); - status.setQueuedContentSize(queuedContentSize); - status.setInputContentSize(bytesIn); - status.setInputCount(flowFilesIn); - status.setOutputContentSize(bytesOut); - status.setOutputCount(flowFilesOut); - status.setFlowFilesReceived(flowFilesReceived); - status.setBytesReceived(bytesReceived); - status.setFlowFilesSent(flowFilesSent); - status.setBytesSent(bytesSent); - status.setFlowFilesTransferred(flowFilesTransferred); - status.setBytesTransferred(bytesTransferred); - - final VersionControlInformation vci = group.getVersionControlInformation(); - if (vci != null && vci.getStatus() != null && vci.getStatus().getState() != null) { - status.setVersionedFlowState(vci.getStatus().getState()); + for (final VersionedProcessGroup childGroup : group.getProcessGroups()) { + conns.addAll(findAllConnections(childGroup)); } - - return status; + return conns; } - private RemoteProcessGroupStatus createRemoteGroupStatus(final RemoteProcessGroup remoteGroup, final RepositoryStatusReport statusReport, final Predicate isAuthorized) { - final boolean isRemoteProcessGroupAuthorized = isAuthorized.evaluate(remoteGroup); - - int receivedCount = 0; - long receivedContentSize = 0L; - int sentCount = 0; - long sentContentSize = 0L; - int activeThreadCount = 0; - int activePortCount = 0; - int inactivePortCount = 0; - - final RemoteProcessGroupStatus status = new RemoteProcessGroupStatus(); - status.setGroupId(remoteGroup.getProcessGroup().getIdentifier()); - status.setName(isRemoteProcessGroupAuthorized ? remoteGroup.getName() : remoteGroup.getIdentifier()); - status.setTargetUri(isRemoteProcessGroupAuthorized ? remoteGroup.getTargetUri() : null); - - long lineageMillis = 0L; - int flowFilesRemoved = 0; - int flowFilesTransferred = 0; - for (final Port port : remoteGroup.getInputPorts()) { - // determine if this input port is connected - final boolean isConnected = port.hasIncomingConnection(); - - // we only want to consider remote ports that we are connected to - if (isConnected) { - if (port.isRunning()) { - activePortCount++; - } else { - inactivePortCount++; - } + // + // Processor access + // - activeThreadCount += processScheduler.getActiveThreadCount(port); - final FlowFileEvent portEvent = statusReport.getReportEntry(port.getIdentifier()); - if (portEvent != null) { - lineageMillis += portEvent.getAggregateLineageMillis(); - flowFilesRemoved += portEvent.getFlowFilesRemoved(); - flowFilesTransferred += portEvent.getFlowFilesOut(); - sentCount += portEvent.getFlowFilesSent(); - sentContentSize += portEvent.getBytesSent(); - } - } + /** + * Returns the ProcessGroup with the given ID + * + * @param id group + * @return the process group or null if not group is found + */ + private ProcessGroup lookupGroup(final String id) { + final ProcessGroup group = flowManager.getGroup(id); + if (group == null) { + throw new IllegalStateException("No Group with ID " + id + " exists"); } + return group; + } - for (final Port port : remoteGroup.getOutputPorts()) { - // determine if this output port is connected - final boolean isConnected = !port.getConnections().isEmpty(); - // we only want to consider remote ports that we are connected from - if (isConnected) { - if (port.isRunning()) { - activePortCount++; - } else { - inactivePortCount++; - } - activeThreadCount += processScheduler.getActiveThreadCount(port); + public List getGarbageCollectionStatus() { + final List statuses = new ArrayList<>(); - final FlowFileEvent portEvent = statusReport.getReportEntry(port.getIdentifier()); - if (portEvent != null) { - receivedCount += portEvent.getFlowFilesReceived(); - receivedContentSize += portEvent.getBytesReceived(); - } - } + final Date now = new Date(); + for (final GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + final String managerName = mbean.getName(); + final long count = mbean.getCollectionCount(); + final long millis = mbean.getCollectionTime(); + final GarbageCollectionStatus status = new StandardGarbageCollectionStatus(managerName, now, count, millis); + statuses.add(status); } - status.setId(remoteGroup.getIdentifier()); - status.setTransmissionStatus(remoteGroup.isTransmitting() ? TransmissionStatus.Transmitting : TransmissionStatus.NotTransmitting); - status.setActiveThreadCount(activeThreadCount); - status.setReceivedContentSize(receivedContentSize); - status.setReceivedCount(receivedCount); - status.setSentContentSize(sentContentSize); - status.setSentCount(sentCount); - status.setActiveRemotePortCount(activePortCount); - status.setInactiveRemotePortCount(inactivePortCount); - - final int flowFilesOutOrRemoved = flowFilesTransferred + flowFilesRemoved; - status.setAverageLineageDuration(flowFilesOutOrRemoved == 0 ? 0 : lineageMillis / flowFilesOutOrRemoved, TimeUnit.MILLISECONDS); - - return status; + return statuses; } - private ProcessorStatus getProcessorStatus(final RepositoryStatusReport report, final ProcessorNode procNode, final Predicate isAuthorized) { - final boolean isProcessorAuthorized = isAuthorized.evaluate(procNode); - - final ProcessorStatus status = new ProcessorStatus(); - status.setId(procNode.getIdentifier()); - status.setGroupId(procNode.getProcessGroup().getIdentifier()); - status.setName(isProcessorAuthorized ? procNode.getName() : procNode.getIdentifier()); - status.setType(isProcessorAuthorized ? procNode.getComponentType() : "Processor"); - - final FlowFileEvent entry = report.getReportEntries().get(procNode.getIdentifier()); - if (entry != null && entry != EmptyFlowFileEvent.INSTANCE) { - final int processedCount = entry.getFlowFilesOut(); - final long numProcessedBytes = entry.getContentSizeOut(); - status.setOutputBytes(numProcessedBytes); - status.setOutputCount(processedCount); - - final int inputCount = entry.getFlowFilesIn(); - final long inputBytes = entry.getContentSizeIn(); - status.setInputBytes(inputBytes); - status.setInputCount(inputCount); - - final long readBytes = entry.getBytesRead(); - status.setBytesRead(readBytes); - - final long writtenBytes = entry.getBytesWritten(); - status.setBytesWritten(writtenBytes); - - status.setProcessingNanos(entry.getProcessingNanoseconds()); - status.setInvocations(entry.getInvocations()); - - status.setAverageLineageDuration(entry.getAverageLineageMillis()); - - status.setFlowFilesReceived(entry.getFlowFilesReceived()); - status.setBytesReceived(entry.getBytesReceived()); - status.setFlowFilesSent(entry.getFlowFilesSent()); - status.setBytesSent(entry.getBytesSent()); - status.setFlowFilesRemoved(entry.getFlowFilesRemoved()); - - if (isProcessorAuthorized) { - status.setCounters(entry.getCounters()); - } - } - - // Determine the run status and get any validation error... only validating while STOPPED - // is a trade-off we are willing to make, even though processor validity could change due to - // environmental conditions (property configured with a file path and the file being externally - // removed). This saves on validation costs that would be unnecessary most of the time. - if (ScheduledState.DISABLED.equals(procNode.getScheduledState())) { - status.setRunStatus(RunStatus.Disabled); - } else if (ScheduledState.RUNNING.equals(procNode.getScheduledState())) { - status.setRunStatus(RunStatus.Running); - } else if (procNode.getValidationStatus() == ValidationStatus.VALIDATING) { - status.setRunStatus(RunStatus.Validating); - } else if (procNode.getValidationStatus() == ValidationStatus.INVALID) { - status.setRunStatus(RunStatus.Invalid); - } else { - status.setRunStatus(RunStatus.Stopped); - } + public GarbageCollectionHistory getGarbageCollectionHistory() { + return componentStatusRepository.getGarbageCollectionHistory(new Date(0L), new Date()); + } - status.setExecutionNode(procNode.getExecutionNode()); - status.setTerminatedThreadCount(procNode.getTerminatedThreadCount()); - status.setActiveThreadCount(processScheduler.getActiveThreadCount(procNode)); - return status; + public ReloadComponent getReloadComponent() { + return reloadComponent; } public void startProcessor(final String parentGroupId, final String processorId) { @@ -3595,215 +1661,43 @@ public void startTransmitting(final RemoteGroupPort remoteGroupPort) { } else { startRemoteGroupPortsAfterInitialization.add(remoteGroupPort); } - } finally { - writeLock.unlock("startTransmitting"); - } - } - - public void stopTransmitting(final RemoteGroupPort remoteGroupPort) { - writeLock.lock(); - try { - if (initialized.get()) { - remoteGroupPort.getRemoteProcessGroup().stopTransmitting(remoteGroupPort); - } else { - startRemoteGroupPortsAfterInitialization.remove(remoteGroupPort); - } - } finally { - writeLock.unlock("stopTransmitting"); - } - } - - public void stopProcessor(final String parentGroupId, final String processorId) { - final ProcessGroup group = lookupGroup(parentGroupId); - final ProcessorNode node = group.getProcessor(processorId); - if (node == null) { - throw new IllegalStateException("Cannot find ProcessorNode with ID " + processorId + " within ProcessGroup with ID " + parentGroupId); - } - group.stopProcessor(node); - // If we are ready to start the processor upon initialization of the controller, don't. - startConnectablesAfterInitialization.remove(node); - } - - public void stopAllProcessors() { - stopProcessGroup(getRootGroupId()); - } - - public void startProcessGroup(final String groupId) { - lookupGroup(groupId).startProcessing(); - } - - public void stopProcessGroup(final String groupId) { - lookupGroup(groupId).stopProcessing(); - } - - public ReportingTaskNode createReportingTask(final String type, final BundleCoordinate bundleCoordinate) throws ReportingTaskInstantiationException { - return createReportingTask(type, bundleCoordinate, true); - } - - public ReportingTaskNode createReportingTask(final String type, final BundleCoordinate bundleCoordinate, final boolean firstTimeAdded) throws ReportingTaskInstantiationException { - return createReportingTask(type, UUID.randomUUID().toString(), bundleCoordinate, firstTimeAdded); - } - - @Override - public ReportingTaskNode createReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate,final boolean firstTimeAdded) - throws ReportingTaskInstantiationException { - return createReportingTask(type, id, bundleCoordinate, Collections.emptySet(), firstTimeAdded, true); - } - - public ReportingTaskNode createReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls, - final boolean firstTimeAdded, final boolean register) throws ReportingTaskInstantiationException { - if (type == null || id == null || bundleCoordinate == null) { - throw new NullPointerException(); - } - - LoggableComponent task = null; - boolean creationSuccessful = true; - - // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader - final LogRepository logRepository = LogRepositoryFactory.getRepository(id); - - try { - task = instantiateReportingTask(type, id, bundleCoordinate, additionalUrls); - } catch (final Exception e) { - LOG.error("Could not create Reporting Task of type " + type + " for ID " + id + "; creating \"Ghost\" implementation", e); - final GhostReportingTask ghostTask = new GhostReportingTask(); - ghostTask.setIdentifier(id); - ghostTask.setCanonicalClassName(type); - task = new LoggableComponent<>(ghostTask, bundleCoordinate, null); - creationSuccessful = false; - } - - final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); - final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(controllerServiceProvider, componentVarRegistry); - final ReportingTaskNode taskNode; - if (creationSuccessful) { - taskNode = new StandardReportingTaskNode(task, id, this, processScheduler, validationContextFactory, componentVarRegistry, this, validationTrigger); - } else { - final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; - final String componentType = "(Missing) " + simpleClassName; - - taskNode = new StandardReportingTaskNode(task, id, this, processScheduler, validationContextFactory, componentType, type, componentVarRegistry, this, validationTrigger, true); - } - - taskNode.setName(taskNode.getReportingTask().getClass().getSimpleName()); - - if (firstTimeAdded) { - final ReportingInitializationContext config = new StandardReportingInitializationContext(id, taskNode.getName(), - SchedulingStrategy.TIMER_DRIVEN, "1 min", taskNode.getLogger(), this, nifiProperties, this); - - try { - taskNode.getReportingTask().initialize(config); - } catch (final InitializationException ie) { - throw new ReportingTaskInstantiationException("Failed to initialize reporting task of type " + type, ie); - } - - try (final NarCloseable x = NarCloseable.withComponentNarLoader(taskNode.getReportingTask().getClass(), taskNode.getReportingTask().getIdentifier())) { - ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, taskNode.getReportingTask()); - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, taskNode.getReportingTask()); - } catch (final Exception e) { - throw new ComponentLifeCycleException("Failed to invoke On-Added Lifecycle methods of " + taskNode.getReportingTask(), e); - } - } - - if (register) { - reportingTasks.put(id, taskNode); - - // Register log observer to provide bulletins when reporting task logs anything at WARN level or above - logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, LogLevel.WARN, - new ReportingTaskLogObserver(getBulletinRepository(), taskNode)); - } - - return taskNode; - } - - private LoggableComponent instantiateReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls) - throws ReportingTaskInstantiationException { - - final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); - try { - final Bundle reportingTaskBundle = ExtensionManager.getBundle(bundleCoordinate); - if (reportingTaskBundle == null) { - throw new IllegalStateException("Unable to find bundle for coordinate " + bundleCoordinate.getCoordinate()); - } - - final ClassLoader detectedClassLoader = ExtensionManager.createInstanceClassLoader(type, id, reportingTaskBundle, additionalUrls); - final Class rawClass = Class.forName(type, false, detectedClassLoader); - Thread.currentThread().setContextClassLoader(detectedClassLoader); - - final Class reportingTaskClass = rawClass.asSubclass(ReportingTask.class); - final Object reportingTaskObj = reportingTaskClass.newInstance(); - - final ReportingTask reportingTask = reportingTaskClass.cast(reportingTaskObj); - final ComponentLog componentLog = new SimpleProcessLogger(id, reportingTask); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLog); - - return new LoggableComponent<>(reportingTask, bundleCoordinate, terminationAwareLogger); - } catch (final Exception e) { - throw new ReportingTaskInstantiationException(type, e); - } finally { - if (ctxClassLoader != null) { - Thread.currentThread().setContextClassLoader(ctxClassLoader); - } + } finally { + writeLock.unlock("startTransmitting"); } } - @Override - public void reload(final ReportingTaskNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) - throws ReportingTaskInstantiationException { - if (existingNode == null) { - throw new IllegalStateException("Existing ReportingTaskNode cannot be null"); - } - - final String id = existingNode.getReportingTask().getIdentifier(); - - // ghost components will have a null logger - if (existingNode.getLogger() != null) { - existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); - } - - // createReportingTask will create a new instance class loader for the same id so - // save the instance class loader to use it for calling OnRemoved on the existing processor - final ClassLoader existingInstanceClassLoader = ExtensionManager.getInstanceClassLoader(id); - - // set firstTimeAdded to true so lifecycle annotations get fired, but don't register this node - // attempt the creation to make sure it works before firing the OnRemoved methods below - final ReportingTaskNode newNode = createReportingTask(newType, id, bundleCoordinate, additionalUrls, true, false); - - // call OnRemoved for the existing reporting task using the previous instance class loader - try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getReportingTask(), existingNode.getConfigurationContext()); + public void stopTransmitting(final RemoteGroupPort remoteGroupPort) { + writeLock.lock(); + try { + if (initialized.get()) { + remoteGroupPort.getRemoteProcessGroup().stopTransmitting(remoteGroupPort); + } else { + startRemoteGroupPortsAfterInitialization.remove(remoteGroupPort); + } } finally { - ExtensionManager.closeURLClassLoader(id, existingInstanceClassLoader); + writeLock.unlock("stopTransmitting"); } - - // set the new reporting task into the existing node - final ComponentLog componentLogger = new SimpleProcessLogger(id, existingNode.getReportingTask()); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); - LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); - - final LoggableComponent newReportingTask = new LoggableComponent<>(newNode.getReportingTask(), newNode.getBundleCoordinate(), terminationAwareLogger); - existingNode.setReportingTask(newReportingTask); - existingNode.setExtensionMissing(newNode.isExtensionMissing()); - - // need to refresh the properties in case we are changing from ghost component to real component - existingNode.refreshProperties(); - - LOG.debug("Triggering async validation of {} due to reporting task reload", existingNode); - validationTrigger.triggerAsync(existingNode); } - @Override - public ReportingTaskNode getReportingTaskNode(final String taskId) { - return reportingTasks.get(taskId); + public void stopProcessor(final String parentGroupId, final String processorId) { + final ProcessGroup group = lookupGroup(parentGroupId); + final ProcessorNode node = group.getProcessor(processorId); + if (node == null) { + throw new IllegalStateException("Cannot find ProcessorNode with ID " + processorId + " within ProcessGroup with ID " + parentGroupId); + } + group.stopProcessor(node); + // If we are ready to start the processor upon initialization of the controller, don't. + startConnectablesAfterInitialization.remove(node); } + + @Override public void startReportingTask(final ReportingTaskNode reportingTaskNode) { if (isTerminated()) { throw new IllegalStateException("Cannot start reporting task " + reportingTaskNode.getIdentifier() + " because the controller is terminated"); } - reportingTaskNode.performValidation(); // ensure that the reporting task has completed its validation before attempting to start it reportingTaskNode.verifyCanStart(); reportingTaskNode.reloadAdditionalResourcesIfNecessary(); processScheduler.schedule(reportingTaskNode); @@ -3819,301 +1713,144 @@ public void stopReportingTask(final ReportingTaskNode reportingTaskNode) { processScheduler.unschedule(reportingTaskNode); } - @Override - public void removeReportingTask(final ReportingTaskNode reportingTaskNode) { - final ReportingTaskNode existing = reportingTasks.get(reportingTaskNode.getIdentifier()); - if (existing == null || existing != reportingTaskNode) { - throw new IllegalStateException("Reporting Task " + reportingTaskNode + " does not exist in this Flow"); - } - - reportingTaskNode.verifyCanDelete(); - - try (final NarCloseable x = NarCloseable.withComponentNarLoader(reportingTaskNode.getReportingTask().getClass(), reportingTaskNode.getReportingTask().getIdentifier())) { - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, reportingTaskNode.getReportingTask(), reportingTaskNode.getConfigurationContext()); - } - - for (final Map.Entry entry : reportingTaskNode.getProperties().entrySet()) { - final PropertyDescriptor descriptor = entry.getKey(); - if (descriptor.getControllerServiceDefinition() != null) { - final String value = entry.getValue() == null ? descriptor.getDefaultValue() : entry.getValue(); - if (value != null) { - final ControllerServiceNode serviceNode = controllerServiceProvider.getControllerServiceNode(value); - if (serviceNode != null) { - serviceNode.removeReference(reportingTaskNode); - } - } - } - } - - reportingTasks.remove(reportingTaskNode.getIdentifier()); - LogRepositoryFactory.removeRepository(reportingTaskNode.getIdentifier()); - processScheduler.onReportingTaskRemoved(reportingTaskNode); - - ExtensionManager.removeInstanceClassLoader(reportingTaskNode.getIdentifier()); - } - - @Override - public Set getAllReportingTasks() { - return new HashSet<>(reportingTasks.values()); - } - - public FlowRegistryClient getFlowRegistryClient() { - return flowRegistryClient; + public FlowManager getFlowManager() { + return flowManager; } - @Override - public ControllerServiceNode createControllerService(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls, final boolean firstTimeAdded) { - // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader - final LogRepository logRepository = LogRepositoryFactory.getRepository(id); - - final ControllerServiceNode serviceNode = controllerServiceProvider.createControllerService(type, id, bundleCoordinate, additionalUrls, firstTimeAdded); - - // Register log observer to provide bulletins when reporting task logs anything at WARN level or above - logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, LogLevel.WARN, - new ControllerServiceLogObserver(getBulletinRepository(), serviceNode)); - - if (firstTimeAdded) { - final ControllerService service = serviceNode.getControllerServiceImplementation(); - - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(service.getClass(), service.getIdentifier())) { - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, service); - } - } - - return serviceNode; - } + /** + * Creates a connection between two Connectable objects. + * + * @param id required ID of the connection + * @param name the name of the connection, or null to leave the + * connection unnamed + * @param source required source + * @param destination required destination + * @param relationshipNames required collection of relationship names + * @return + * + * @throws NullPointerException if the ID, source, destination, or set of + * relationships is null. + * @throws IllegalArgumentException if relationships is an + * empty collection + */ + public Connection createConnection(final String id, final String name, final Connectable source, final Connectable destination, final Collection relationshipNames) { + final StandardConnection.Builder builder = new StandardConnection.Builder(processScheduler); - @Override - public void reload(final ControllerServiceNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) - throws ControllerServiceInstantiationException { - if (existingNode == null) { - throw new IllegalStateException("Existing ControllerServiceNode cannot be null"); + final List relationships = new ArrayList<>(); + for (final String relationshipName : requireNonNull(relationshipNames)) { + relationships.add(new Relationship.Builder().name(relationshipName).build()); } - final String id = existingNode.getIdentifier(); + // Create and initialize a FlowFileSwapManager for this connection + final FlowFileSwapManager swapManager = createSwapManager(); + final EventReporter eventReporter = createEventReporter(); - // ghost components will have a null logger - if (existingNode.getLogger() != null) { - existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); - } + try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { + final SwapManagerInitializationContext initializationContext = new SwapManagerInitializationContext() { + @Override + public ResourceClaimManager getResourceClaimManager() { + return resourceClaimManager; + } - // createControllerService will create a new instance class loader for the same id so - // save the instance class loader to use it for calling OnRemoved on the existing service - final ClassLoader existingInstanceClassLoader = ExtensionManager.getInstanceClassLoader(id); + @Override + public FlowFileRepository getFlowFileRepository() { + return flowFileRepository; + } - // create a new node with firstTimeAdded as true so lifecycle methods get called - // attempt the creation to make sure it works before firing the OnRemoved methods below - final ControllerServiceNode newNode = controllerServiceProvider.createControllerService(newType, id, bundleCoordinate, additionalUrls, true); + @Override + public EventReporter getEventReporter() { + return eventReporter; + } + }; - // call OnRemoved for the existing service using the previous instance class loader - try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { - final ConfigurationContext configurationContext = new StandardConfigurationContext(existingNode, controllerServiceProvider, null, variableRegistry); - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getControllerServiceImplementation(), configurationContext); - } finally { - ExtensionManager.closeURLClassLoader(id, existingInstanceClassLoader); + swapManager.initialize(initializationContext); } - // take the invocation handler that was created for new proxy and is set to look at the new node, - // and set it to look at the existing node - final ControllerServiceInvocationHandler invocationHandler = newNode.getInvocationHandler(); - invocationHandler.setServiceNode(existingNode); - - // create LoggableComponents for the proxy and implementation - final ComponentLog componentLogger = new SimpleProcessLogger(id, newNode.getControllerServiceImplementation()); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); - LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); - - final LoggableComponent loggableProxy = new LoggableComponent<>(newNode.getProxiedControllerService(), bundleCoordinate, terminationAwareLogger); - final LoggableComponent loggableImplementation = new LoggableComponent<>(newNode.getControllerServiceImplementation(), bundleCoordinate, terminationAwareLogger); - - // set the new impl, proxy, and invocation handler into the existing node - existingNode.setControllerServiceAndProxy(loggableImplementation, loggableProxy, invocationHandler); - existingNode.setExtensionMissing(newNode.isExtensionMissing()); - - // need to refresh the properties in case we are changing from ghost component to real component - existingNode.refreshProperties(); - - LOG.debug("Triggering async validation of {} due to controller service reload", existingNode); - validationTrigger.triggerAsync(existingNode); - } - - @Override - public void enableReportingTask(final ReportingTaskNode reportingTaskNode) { - reportingTaskNode.verifyCanEnable(); - reportingTaskNode.reloadAdditionalResourcesIfNecessary(); - processScheduler.enableReportingTask(reportingTaskNode); - } - - @Override - public void disableReportingTask(final ReportingTaskNode reportingTaskNode) { - reportingTaskNode.verifyCanDisable(); - processScheduler.disableReportingTask(reportingTaskNode); - } - - @Override - public Set disableReferencingServices(final ControllerServiceNode serviceNode) { - return controllerServiceProvider.disableReferencingServices(serviceNode); - } - - @Override - public Set enableReferencingServices(final ControllerServiceNode serviceNode) { - return controllerServiceProvider.enableReferencingServices(serviceNode); - } - - @Override - public Set scheduleReferencingComponents(final ControllerServiceNode serviceNode) { - return controllerServiceProvider.scheduleReferencingComponents(serviceNode); - } - - @Override - public Set unscheduleReferencingComponents(final ControllerServiceNode serviceNode) { - return controllerServiceProvider.unscheduleReferencingComponents(serviceNode); - } - - @Override - public CompletableFuture enableControllerService(final ControllerServiceNode serviceNode) { - return controllerServiceProvider.enableControllerService(serviceNode); - } - - @Override - public void enableControllerServices(final Collection serviceNodes) { - controllerServiceProvider.enableControllerServices(serviceNodes); - } + final FlowFileQueueFactory flowFileQueueFactory = new FlowFileQueueFactory() { + @Override + public FlowFileQueue createFlowFileQueue(final LoadBalanceStrategy loadBalanceStrategy, final String partitioningAttribute, final ConnectionEventListener eventListener) { + final FlowFileQueue flowFileQueue; - @Override - public Future enableControllerServicesAsync(final Collection serviceNodes) { - return controllerServiceProvider.enableControllerServicesAsync(serviceNodes); - } + if (clusterCoordinator == null) { + flowFileQueue = new StandardFlowFileQueue(id, eventListener, flowFileRepository, provenanceRepository, resourceClaimManager, processScheduler, swapManager, + eventReporter, nifiProperties.getQueueSwapThreshold(), nifiProperties.getDefaultBackPressureObjectThreshold(), nifiProperties.getDefaultBackPressureDataSizeThreshold()); + } else { + flowFileQueue = new SocketLoadBalancedFlowFileQueue(id, eventListener, processScheduler, flowFileRepository, provenanceRepository, contentRepository, resourceClaimManager, + clusterCoordinator, loadBalanceClientRegistry, swapManager, nifiProperties.getQueueSwapThreshold(), eventReporter); - @Override - public CompletableFuture disableControllerService(final ControllerServiceNode serviceNode) { - serviceNode.verifyCanDisable(); - return controllerServiceProvider.disableControllerService(serviceNode); - } + flowFileQueue.setBackPressureObjectThreshold(nifiProperties.getDefaultBackPressureObjectThreshold()); + flowFileQueue.setBackPressureDataSizeThreshold(nifiProperties.getDefaultBackPressureDataSizeThreshold()); + } - @Override - public Future disableControllerServicesAsync(final Collection serviceNodes) { - return controllerServiceProvider.disableControllerServicesAsync(serviceNodes); - } + return flowFileQueue; + } + }; - @Override - public void verifyCanEnableReferencingServices(final ControllerServiceNode serviceNode) { - controllerServiceProvider.verifyCanEnableReferencingServices(serviceNode); - } + final Connection connection = builder.id(requireNonNull(id).intern()) + .name(name == null ? null : name.intern()) + .relationships(relationships) + .source(requireNonNull(source)) + .destination(destination) + .flowFileQueueFactory(flowFileQueueFactory) + .build(); - @Override - public void verifyCanScheduleReferencingComponents(final ControllerServiceNode serviceNode) { - controllerServiceProvider.verifyCanScheduleReferencingComponents(serviceNode); + return connection; } - @Override - public void verifyCanDisableReferencingServices(final ControllerServiceNode serviceNode) { - controllerServiceProvider.verifyCanDisableReferencingServices(serviceNode); - } - @Override - public void verifyCanStopReferencingComponents(final ControllerServiceNode serviceNode) { - controllerServiceProvider.verifyCanStopReferencingComponents(serviceNode); - } @Override - public ControllerService getControllerService(final String serviceIdentifier) { - return controllerServiceProvider.getControllerService(serviceIdentifier); + public ReportingTaskNode getReportingTaskNode(final String identifier) { + return flowManager.getReportingTaskNode(identifier); } @Override - public ControllerService getControllerServiceForComponent(final String serviceIdentifier, final String componentId) { - return controllerServiceProvider.getControllerServiceForComponent(serviceIdentifier, componentId); + public ReportingTaskNode createReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate, final boolean firstTimeAdded) throws ReportingTaskInstantiationException { + return flowManager.createReportingTask(type, id, bundleCoordinate, firstTimeAdded); } @Override - public Set getControllerServiceIdentifiers(final Class serviceType) throws IllegalArgumentException { - return controllerServiceProvider.getControllerServiceIdentifiers(serviceType); + public Set getAllReportingTasks() { + return flowManager.getAllReportingTasks(); } @Override - public ControllerServiceNode getControllerServiceNode(final String serviceIdentifier) { - return controllerServiceProvider.getControllerServiceNode(serviceIdentifier); + public void removeReportingTask(final ReportingTaskNode reportingTaskNode) { + flowManager.removeReportingTask(reportingTaskNode); } - public Set getRootControllerServices() { - return new HashSet<>(rootControllerServices.values()); - } - public void addRootControllerService(final ControllerServiceNode serviceNode) { - final ControllerServiceNode existing = rootControllerServices.putIfAbsent(serviceNode.getIdentifier(), serviceNode); - if (existing != null) { - throw new IllegalStateException("Controller Service with ID " + serviceNode.getIdentifier() + " already exists at the Controller level"); - } + public FlowRegistryClient getFlowRegistryClient() { + return flowRegistryClient; } - public ControllerServiceNode getRootControllerService(final String serviceIdentifier) { - return rootControllerServices.get(serviceIdentifier); + public ControllerServiceProvider getControllerServiceProvider() { + return controllerServiceProvider; } - public void removeRootControllerService(final ControllerServiceNode service) { - final ControllerServiceNode existing = rootControllerServices.get(requireNonNull(service).getIdentifier()); - if (existing == null) { - throw new IllegalStateException(service + " is not a member of this Process Group"); - } - - service.verifyCanDelete(); - - try (final NarCloseable x = NarCloseable.withComponentNarLoader(service.getControllerServiceImplementation().getClass(), service.getIdentifier())) { - final ConfigurationContext configurationContext = new StandardConfigurationContext(service, controllerServiceProvider, null, variableRegistry); - ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, service.getControllerServiceImplementation(), configurationContext); - } - - for (final Map.Entry entry : service.getProperties().entrySet()) { - final PropertyDescriptor descriptor = entry.getKey(); - if (descriptor.getControllerServiceDefinition() != null) { - final String value = entry.getValue() == null ? descriptor.getDefaultValue() : entry.getValue(); - if (value != null) { - final ControllerServiceNode referencedNode = getRootControllerService(value); - if (referencedNode != null) { - referencedNode.removeReference(service); - } - } - } - } - - rootControllerServices.remove(service.getIdentifier()); - getStateManagerProvider().onComponentRemoved(service.getIdentifier()); - - ExtensionManager.removeInstanceClassLoader(service.getIdentifier()); - - LOG.info("{} removed from Flow Controller", service, this); - } - @Override - public boolean isControllerServiceEnabled(final ControllerService service) { - return controllerServiceProvider.isControllerServiceEnabled(service); + public VariableRegistry getVariableRegistry() { + return variableRegistry; } - @Override - public boolean isControllerServiceEnabled(final String serviceIdentifier) { - return controllerServiceProvider.isControllerServiceEnabled(serviceIdentifier); + public ProvenanceAuthorizableFactory getProvenanceAuthorizableFactory() { + return provenanceAuthorizableFactory; } @Override - public boolean isControllerServiceEnabling(final String serviceIdentifier) { - return controllerServiceProvider.isControllerServiceEnabling(serviceIdentifier); + public void enableReportingTask(final ReportingTaskNode reportingTaskNode) { + reportingTaskNode.verifyCanEnable(); + reportingTaskNode.reloadAdditionalResourcesIfNecessary(); + processScheduler.enableReportingTask(reportingTaskNode); } @Override - public String getControllerServiceName(final String serviceIdentifier) { - return controllerServiceProvider.getControllerServiceName(serviceIdentifier); + public void disableReportingTask(final ReportingTaskNode reportingTaskNode) { + reportingTaskNode.verifyCanDisable(); + processScheduler.disableReportingTask(reportingTaskNode); } - @Override - public void removeControllerService(final ControllerServiceNode serviceNode) { - controllerServiceProvider.removeControllerService(serviceNode); - } - @Override - public Set getAllControllerServices() { - return controllerServiceProvider.getAllControllerServices(); - } // // Counters @@ -4229,10 +1966,6 @@ public int getActiveThreadCount() { return timerDrivenCount + eventDrivenCount; } - private RepositoryStatusReport getProcessorStats() { - return flowFileEventRepository.reportTransferEvents(System.currentTimeMillis()); - } - // // Clustering methods @@ -4471,7 +2204,7 @@ public void setClustered(final boolean clustered, final String clusterInstanceId setPrimary(false); } - final List remoteGroups = getGroup(getRootGroupId()).findAllRemoteProcessGroups(); + final List remoteGroups = flowManager.getRootGroup().findAllRemoteProcessGroups(); for (final RemoteProcessGroup remoteGroup : remoteGroups) { remoteGroup.reinitialize(clustered); } @@ -4483,7 +2216,7 @@ public void setClustered(final boolean clustered, final String clusterInstanceId } // update the heartbeat bean - this.heartbeatBeanRef.set(new HeartbeatBean(getRootGroup(), isPrimary())); + this.heartbeatBeanRef.set(new HeartbeatBean(flowManager.getRootGroup(), isPrimary())); } finally { writeLock.unlock("setClustered"); } @@ -4506,19 +2239,20 @@ public boolean isClusterCoordinator() { public void setPrimary(final boolean primary) { final PrimaryNodeState nodeState = primary ? PrimaryNodeState.ELECTED_PRIMARY_NODE : PrimaryNodeState.PRIMARY_NODE_REVOKED; - final ProcessGroup rootGroup = getGroup(getRootGroupId()); + final ProcessGroup rootGroup = flowManager.getRootGroup(); for (final ProcessorNode procNode : rootGroup.findAllProcessors()) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(procNode.getProcessor().getClass(), procNode.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, procNode.getProcessor().getClass(), procNode.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnPrimaryNodeStateChange.class, procNode.getProcessor(), nodeState); } } - for (final ControllerServiceNode serviceNode : getAllControllerServices()) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(serviceNode.getControllerServiceImplementation().getClass(), serviceNode.getIdentifier())) { + for (final ControllerServiceNode serviceNode : flowManager.getAllControllerServices()) { + final Class serviceImplClass = serviceNode.getControllerServiceImplementation().getClass(); + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, serviceImplClass, serviceNode.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnPrimaryNodeStateChange.class, serviceNode.getControllerServiceImplementation(), nodeState); } } for (final ReportingTaskNode reportingTaskNode : getAllReportingTasks()) { - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(reportingTaskNode.getReportingTask().getClass(), reportingTaskNode.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, reportingTaskNode.getReportingTask().getClass(), reportingTaskNode.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnPrimaryNodeStateChange.class, reportingTaskNode.getReportingTask(), nodeState); } } @@ -4666,8 +2400,8 @@ public InputStream getContent(final ProvenanceEventRecord provEvent, final Conte .setEventTime(System.currentTimeMillis()) .setFlowFileEntryDate(provEvent.getFlowFileEntryDate()) .setLineageStartDate(provEvent.getLineageStartDate()) - .setComponentType(getName()) - .setComponentId(getRootGroupId()) + .setComponentType(flowManager.getRootGroup().getName()) + .setComponentId(flowManager.getRootGroupId()) .setDetails("Download of " + (direction == ContentDirection.INPUT ? "Input" : "Output") + " Content requested by " + requestor + " for Provenance Event " + provEvent.getEventId()) .build(); @@ -4707,8 +2441,8 @@ public InputStream getContent(final FlowFileRecord flowFile, final String reques .setEventTime(System.currentTimeMillis()) .setFlowFileEntryDate(flowFile.getEntryDate()) .setLineageStartDate(flowFile.getLineageStartDate()) - .setComponentType(getName()) - .setComponentId(getRootGroupId()) + .setComponentType(flowManager.getRootGroup().getName()) + .setComponentId(flowManager.getRootGroupId()) .setDetails("Download of Content requested by " + requestor + " for " + flowFile); if (contentClaim != null) { @@ -4754,7 +2488,7 @@ private String getReplayFailureReason(final ProvenanceEventRecord event) { return "Cannot replay data from Provenance Event because the event does not specify the Source FlowFile Queue"; } - final List connections = getGroup(getRootGroupId()).findAllConnections(); + final List connections = flowManager.getRootGroup().findAllConnections(); FlowFileQueue queue = null; for (final Connection connection : connections) { if (event.getSourceQueueIdentifier().equals(connection.getIdentifier())) { @@ -4805,7 +2539,7 @@ public ProvenanceEventRecord replayFlowFile(final ProvenanceEventRecord event, f throw new IllegalArgumentException("Cannot replay data from Provenance Event because the event does not specify the Source FlowFile Queue"); } - final List connections = getGroup(getRootGroupId()).findAllConnections(); + final List connections = flowManager.getRootGroup().findAllConnections(); FlowFileQueue queue = null; for (final Connection connection : connections) { if (event.getSourceQueueIdentifier().equals(connection.getIdentifier())) { @@ -4904,39 +2638,6 @@ public ProvenanceEventRecord replayFlowFile(final ProvenanceEventRecord event, f return replayEvent; } - @Override - public List getComponentIdentifiers() { - final List componentIds = new ArrayList<>(); - getGroup(getRootGroupId()).findAllProcessors().stream() - .forEach(proc -> componentIds.add(proc.getIdentifier())); - getGroup(getRootGroupId()).getInputPorts().stream() - .forEach(port -> componentIds.add(port.getIdentifier())); - getGroup(getRootGroupId()).getOutputPorts().stream() - .forEach(port -> componentIds.add(port.getIdentifier())); - - return componentIds; - } - - @Override - @SuppressWarnings("rawtypes") - public List getComponentTypes() { - final Set procClasses = ExtensionManager.getExtensions(Processor.class); - final List componentTypes = new ArrayList<>(procClasses.size() + 2); - componentTypes.add(ProvenanceEventRecord.REMOTE_INPUT_PORT_TYPE); - componentTypes.add(ProvenanceEventRecord.REMOTE_OUTPUT_PORT_TYPE); - procClasses.stream() - .map(procClass -> procClass.getSimpleName()) - .forEach(componentType -> componentTypes.add(componentType)); - return componentTypes; - } - - @Override - public List getQueueIdentifiers() { - return getAllQueues().stream() - .map(q -> q.getIdentifier()) - .collect(Collectors.toList()); - } - public boolean isConnected() { rwLock.readLock().lock(); try { @@ -4952,7 +2653,7 @@ public void setConnectionStatus(final NodeConnectionStatus connectionStatus) { this.connectionStatus = connectionStatus; // update the heartbeat bean - this.heartbeatBeanRef.set(new HeartbeatBean(getRootGroup(), isPrimary())); + this.heartbeatBeanRef.set(new HeartbeatBean(flowManager.getRootGroup(), isPrimary())); } finally { rwLock.writeLock().unlock(); } @@ -5006,7 +2707,7 @@ HeartbeatMessage createHeartbeatMessage() { if (bean == null) { readLock.lock(); try { - bean = new HeartbeatBean(getGroup(getRootGroupId()), isPrimary()); + bean = new HeartbeatBean(flowManager.getRootGroup(), isPrimary()); } finally { readLock.unlock("createHeartbeatMessage"); } @@ -5043,7 +2744,7 @@ HeartbeatMessage createHeartbeatMessage() { } private void updateRemoteProcessGroups() { - final List remoteGroups = getGroup(getRootGroupId()).findAllRemoteProcessGroups(); + final List remoteGroups = flowManager.getRootGroup().findAllRemoteProcessGroups(); for (final RemoteProcessGroup remoteGroup : remoteGroups) { try { remoteGroup.refreshFlowContents(); @@ -5056,95 +2757,7 @@ private void updateRemoteProcessGroups() { } } - @Override - public List getProvenanceEvents(final long firstEventId, final int maxRecords) throws IOException { - return new ArrayList<>(provenanceRepository.getEvents(firstEventId, maxRecords)); - } - - @Override - public Authorizable createLocalDataAuthorizable(final String componentId) { - final String rootGroupId = getRootGroupId(); - - // Provenance Events are generated only by connectable components, with the exception of DOWNLOAD events, - // which have the root process group's identifier assigned as the component ID, and DROP events, which - // could have the connection identifier assigned as the component ID. So, we check if the component ID - // is set to the root group and otherwise assume that the ID is that of a connectable or connection. - final DataAuthorizable authorizable; - if (rootGroupId.equals(componentId)) { - authorizable = new DataAuthorizable(getRootGroup()); - } else { - // check if the component is a connectable, this should be the case most often - final Connectable connectable = findLocalConnectable(componentId); - if (connectable == null) { - // if the component id is not a connectable then consider a connection - final Connection connection = getRootGroup().findConnection(componentId); - - if (connection == null) { - throw new ResourceNotFoundException("The component that generated this event is no longer part of the data flow."); - } else { - // authorizable for connection data is associated with the source connectable - authorizable = new DataAuthorizable(connection.getSource()); - } - } else { - authorizable = new DataAuthorizable(connectable); - } - } - - return authorizable; - } - - @Override - public Authorizable createRemoteDataAuthorizable(String remoteGroupPortId) { - final DataAuthorizable authorizable; - - final RemoteGroupPort remoteGroupPort = getRootGroup().findRemoteGroupPort(remoteGroupPortId); - if (remoteGroupPort == null) { - throw new ResourceNotFoundException("The component that generated this event is no longer part of the data flow."); - } else { - // authorizable for remote group ports should be the remote process group - authorizable = new DataAuthorizable(remoteGroupPort.getRemoteProcessGroup()); - } - - return authorizable; - } - - @Override - public Authorizable createProvenanceDataAuthorizable(String componentId) { - final String rootGroupId = getRootGroupId(); - - // Provenance Events are generated only by connectable components, with the exception of DOWNLOAD events, - // which have the root process group's identifier assigned as the component ID, and DROP events, which - // could have the connection identifier assigned as the component ID. So, we check if the component ID - // is set to the root group and otherwise assume that the ID is that of a connectable or connection. - final ProvenanceDataAuthorizable authorizable; - if (rootGroupId.equals(componentId)) { - authorizable = new ProvenanceDataAuthorizable(getRootGroup()); - } else { - // check if the component is a connectable, this should be the case most often - final Connectable connectable = findLocalConnectable(componentId); - if (connectable == null) { - // if the component id is not a connectable then consider a connection - final Connection connection = getRootGroup().findConnection(componentId); - - if (connection == null) { - throw new ResourceNotFoundException("The component that generated this event is no longer part of the data flow."); - } else { - // authorizable for connection data is associated with the source connectable - authorizable = new ProvenanceDataAuthorizable(connection.getSource()); - } - } else { - authorizable = new ProvenanceDataAuthorizable(connectable); - } - } - - return authorizable; - } - @Override - public List getFlowChanges(final int firstActionId, final int maxActions) { - final History history = auditService.getActions(firstActionId, maxActions); - return new ArrayList<>(history.getActions()); - } public Integer getRemoteSiteListeningPort() { return remoteInputSocketPort; @@ -5158,16 +2771,14 @@ public Boolean isRemoteSiteCommsSecure() { return isSiteToSiteSecure; } - public ProcessScheduler getProcessScheduler() { + public StandardProcessScheduler getProcessScheduler() { return processScheduler; } - @Override - public Set getControllerServiceIdentifiers(final Class serviceType, final String groupId) { - return controllerServiceProvider.getControllerServiceIdentifiers(serviceType, groupId); + public AuditService getAuditService() { + return auditService; } - @Override public ProvenanceRepository getProvenanceRepository() { return provenanceRepository; } @@ -5204,17 +2815,6 @@ public StatusHistoryDTO getRemoteProcessGroupStatusHistory(final String remoteGr return StatusHistoryUtil.createStatusHistoryDTO(componentStatusRepository.getRemoteProcessGroupStatusHistory(remoteGroupId, startTime, endTime, preferredDataPoints)); } - @Override - public Collection getAllQueues() { - final Collection connections = getGroup(getRootGroupId()).findAllConnections(); - final List queues = new ArrayList<>(connections.size()); - for (final Connection connection : connections) { - queues.add(connection.getFlowFileQueue()); - } - - return queues; - } - private static class HeartbeatBean { private final ProcessGroup rootGroup; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowSnippet.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowSnippet.java new file mode 100644 index 000000000000..54c0b02b4923 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowSnippet.java @@ -0,0 +1,47 @@ +/* + * 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.controller; + +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.flow.FlowManager; +import org.apache.nifi.groups.ProcessGroup; + +public interface FlowSnippet { + /** + * Validates that the FlowSnippet can be added to the given ProcessGroup + * @param group the group to add the snippet to + */ + void validate(final ProcessGroup group); + + /** + * Verifies that the components referenced within the snippet are valid + * + * @throws IllegalStateException if any component within the snippet can is not a known extension + */ + void verifyComponentTypesInSnippet(); + + /** + * Instantiates this snippet, adding it to the given Process Group + * + * @param flowManager the FlowManager + * @param group the group to add the snippet to + * @throws ProcessorInstantiationException if unable to instantiate any of the Processors within the snippet + * @throws org.apache.nifi.controller.exception.ControllerServiceInstantiationException if unable to instantiate any of the Controller Services within the snippet + */ + void instantiate(FlowManager flowManager, ProcessGroup group) throws ProcessorInstantiationException; +} + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowService.java index 7a5c45e087df..83d845c55e68 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowService.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowService.java @@ -26,7 +26,9 @@ import org.apache.nifi.cluster.coordination.node.DisconnectionCode; import org.apache.nifi.cluster.coordination.node.NodeConnectionState; import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; +import org.apache.nifi.cluster.coordination.node.OffloadCode; import org.apache.nifi.cluster.exception.NoClusterCoordinatorException; +import org.apache.nifi.cluster.protocol.ComponentRevision; import org.apache.nifi.cluster.protocol.ConnectionRequest; import org.apache.nifi.cluster.protocol.ConnectionResponse; import org.apache.nifi.cluster.protocol.DataFlow; @@ -39,24 +41,30 @@ import org.apache.nifi.cluster.protocol.message.DisconnectMessage; import org.apache.nifi.cluster.protocol.message.FlowRequestMessage; import org.apache.nifi.cluster.protocol.message.FlowResponseMessage; +import org.apache.nifi.cluster.protocol.message.OffloadMessage; import org.apache.nifi.cluster.protocol.message.ProtocolMessage; import org.apache.nifi.cluster.protocol.message.ReconnectionRequestMessage; import org.apache.nifi.cluster.protocol.message.ReconnectionResponseMessage; import org.apache.nifi.components.state.Scope; import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.controller.flow.FlowManager; import org.apache.nifi.controller.serialization.FlowSerializationException; import org.apache.nifi.controller.serialization.FlowSynchronizationException; +import org.apache.nifi.controller.status.ProcessGroupStatus; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.lifecycle.LifeCycleStartException; import org.apache.nifi.logging.LogLevel; -import org.apache.nifi.nar.NarClassLoaders; +import org.apache.nifi.nar.NarClassLoadersHolder; import org.apache.nifi.persistence.FlowConfigurationDAO; import org.apache.nifi.persistence.StandardXMLFlowConfigurationDAO; import org.apache.nifi.persistence.TemplateDeserializer; import org.apache.nifi.reporting.Bulletin; +import org.apache.nifi.reporting.EventAccess; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; @@ -186,7 +194,7 @@ private StandardFlowService( gracefulShutdownSeconds = (int) FormatUtils.getTimeDuration(nifiProperties.getProperty(NiFiProperties.FLOW_CONTROLLER_GRACEFUL_SHUTDOWN_PERIOD), TimeUnit.SECONDS); autoResumeState = nifiProperties.getAutoResumeState(); - dao = new StandardXMLFlowConfigurationDAO(flowXml, encryptor, nifiProperties); + dao = new StandardXMLFlowConfigurationDAO(flowXml, encryptor, nifiProperties, controller.getExtensionManager()); this.clusterCoordinator = clusterCoordinator; if (clusterCoordinator != null) { clusterCoordinator.setFlowService(this); @@ -253,7 +261,7 @@ public void saveFlowChanges(final OutputStream outStream) throws IOException { public void overwriteFlow(final InputStream is) throws IOException { writeLock.lock(); try (final OutputStream output = Files.newOutputStream(flowXml, StandardOpenOption.WRITE, StandardOpenOption.CREATE); - final OutputStream gzipOut = new GZIPOutputStream(output);) { + final OutputStream gzipOut = new GZIPOutputStream(output)) { FileUtils.copy(is, gzipOut); } finally { writeLock.unlock(); @@ -330,12 +338,8 @@ public void stop(final boolean force) { running.set(false); if (clusterCoordinator != null) { - final Thread shutdownClusterCoordinator = new Thread(new Runnable() { - @Override - public void run() { - clusterCoordinator.shutdown(); - } - }); + final Thread shutdownClusterCoordinator = new Thread(clusterCoordinator::shutdown); + shutdownClusterCoordinator.setDaemon(true); shutdownClusterCoordinator.setName("Shutdown Cluster Coordinator"); shutdownClusterCoordinator.start(); @@ -381,6 +385,7 @@ public void run() { public boolean canHandle(final ProtocolMessage msg) { switch (msg.getType()) { case RECONNECTION_REQUEST: + case OFFLOAD_REQUEST: case DISCONNECTION_REQUEST: case FLOW_REQUEST: return true; @@ -415,6 +420,22 @@ public void run() { return new ReconnectionResponseMessage(); } + case OFFLOAD_REQUEST: { + final Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + handleOffloadRequest((OffloadMessage) request); + } catch (InterruptedException e) { + throw new ProtocolException("Could not complete offload request", e); + } + } + }, "Offload Flow Files from Node"); + t.setDaemon(true); + t.start(); + + return null; + } case DISCONNECTION_REQUEST: { final Thread t = new Thread(new Runnable() { @Override @@ -561,7 +582,7 @@ private void handleConnectionFailure(final Exception ex) { private FlowResponseMessage handleFlowRequest(final FlowRequestMessage request) throws ProtocolException { readLock.lock(); try { - logger.info("Received flow request message from manager."); + logger.info("Received flow request message from cluster coordinator."); // create the response final FlowResponseMessage response = new FlowResponseMessage(); @@ -611,10 +632,12 @@ public StandardDataFlow createDataFlowFromController() throws IOException { final byte[] flowBytes = baos.toByteArray(); baos.reset(); + final FlowManager flowManager = controller.getFlowManager(); + final Set missingComponents = new HashSet<>(); - controller.getRootGroup().findAllProcessors().stream().filter(p -> p.isExtensionMissing()).forEach(p -> missingComponents.add(p.getIdentifier())); - controller.getAllControllerServices().stream().filter(cs -> cs.isExtensionMissing()).forEach(cs -> missingComponents.add(cs.getIdentifier())); - controller.getAllReportingTasks().stream().filter(r -> r.isExtensionMissing()).forEach(r -> missingComponents.add(r.getIdentifier())); + flowManager.getRootGroup().findAllProcessors().stream().filter(AbstractComponentNode::isExtensionMissing).forEach(p -> missingComponents.add(p.getIdentifier())); + flowManager.getAllControllerServices().stream().filter(ComponentNode::isExtensionMissing).forEach(cs -> missingComponents.add(cs.getIdentifier())); + controller.getAllReportingTasks().stream().filter(ComponentNode::isExtensionMissing).forEach(r -> missingComponents.add(r.getIdentifier())); return new StandardDataFlow(flowBytes, snippetBytes, authorizerFingerprint, missingComponents); } @@ -631,7 +654,7 @@ private NodeIdentifier getNodeId() { private void handleReconnectionRequest(final ReconnectionRequestMessage request) { try { - logger.info("Processing reconnection request from manager."); + logger.info("Processing reconnection request from cluster coordinator."); // reconnect ConnectionResponse connectionResponse = new ConnectionResponse(getNodeId(), request.getDataFlow(), @@ -645,7 +668,7 @@ private void handleReconnectionRequest(final ReconnectionRequestMessage request) loadFromConnectionResponse(connectionResponse); clusterCoordinator.resetNodeStatuses(connectionResponse.getNodeConnectionStatuses().stream() - .collect(Collectors.toMap(status -> status.getNodeIdentifier(), status -> status))); + .collect(Collectors.toMap(NodeConnectionStatus::getNodeIdentifier, status -> status))); // reconnected, this node needs to explicitly write the inherited flow to disk, and resume heartbeats saveFlowChanges(); controller.resumeHeartbeats(); @@ -662,8 +685,70 @@ private void handleReconnectionRequest(final ReconnectionRequestMessage request) } } + private void handleOffloadRequest(final OffloadMessage request) throws InterruptedException { + logger.info("Received offload request message from cluster coordinator with explanation: " + request.getExplanation()); + offload(request.getExplanation()); + } + + private void offload(final String explanation) throws InterruptedException { + writeLock.lock(); + try { + logger.info("Offloading node due to " + explanation); + + // mark node as offloading + controller.setConnectionStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADING, OffloadCode.OFFLOADED, explanation)); + + final FlowManager flowManager = controller.getFlowManager(); + + // request to stop all processors on node + flowManager.getRootGroup().stopProcessing(); + + // terminate all processors + flowManager.getRootGroup().findAllProcessors() + // filter stream, only stopped processors can be terminated + .stream().filter(pn -> pn.getScheduledState() == ScheduledState.STOPPED) + .forEach(pn -> pn.getProcessGroup().terminateProcessor(pn)); + + // request to stop all remote process groups + flowManager.getRootGroup().findAllRemoteProcessGroups().forEach(RemoteProcessGroup::stopTransmitting); + + // offload all queues on node + final Set connections = flowManager.findAllConnections(); + for (final Connection connection : connections) { + connection.getFlowFileQueue().offloadQueue(); + } + + final EventAccess eventAccess = controller.getEventAccess(); + ProcessGroupStatus controllerStatus; + + // wait for rebalance of flowfiles on all queues + while (true) { + controllerStatus = eventAccess.getControllerStatus(); + if (controllerStatus.getQueuedCount() <= 0) { + break; + } + + logger.debug("Offloading queues on node {}, remaining queued count: {}", getNodeId(), controllerStatus.getQueuedCount()); + Thread.sleep(1000); + } + + // finish offload + for (final Connection connection : connections) { + connection.getFlowFileQueue().resetOffloadedQueue(); + } + + controller.setConnectionStatus(new NodeConnectionStatus(nodeId, NodeConnectionState.OFFLOADED, OffloadCode.OFFLOADED, explanation)); + clusterCoordinator.finishNodeOffload(getNodeId()); + + logger.info("Node offloaded due to " + explanation); + + } finally { + writeLock.unlock(); + } + } + private void handleDisconnectionRequest(final DisconnectMessage request) { - logger.info("Received disconnection request message from manager with explanation: " + request.getExplanation()); + logger.info("Received disconnection request message from cluster coordinator with explanation: " + request.getExplanation()); disconnect(request.getExplanation()); } @@ -724,7 +809,7 @@ private void loadFromBytes(final DataFlow proposedFlow, final boolean allowEmpty logger.debug("Loading proposed flow into FlowController"); dao.load(controller, actualProposedFlow); - final ProcessGroup rootGroup = controller.getGroup(controller.getRootGroupId()); + final ProcessGroup rootGroup = controller.getFlowManager().getRootGroup(); if (rootGroup.isEmpty() && !allowEmptyFlow) { throw new FlowSynchronizationException("Failed to load flow because unable to connect to cluster and local flow is empty"); } @@ -829,11 +914,11 @@ private ConnectionResponse connect(final boolean retryOnCommsFailure, final bool } } else if (response.getRejectionReason() != null) { logger.warn("Connection request was blocked by cluster coordinator with the explanation: " + response.getRejectionReason()); - // set response to null and treat a firewall blockage the same as getting no response from manager + // set response to null and treat a firewall blockage the same as getting no response from cluster coordinator response = null; break; } else { - // we received a successful connection response from manager + // we received a successful connection response from cluster coordinator break; } } catch (final NoClusterCoordinatorException ncce) { @@ -901,7 +986,7 @@ private void loadFromConnectionResponse(final ConnectionResponse response) throw try { if (response.getNodeConnectionStatuses() != null) { clusterCoordinator.resetNodeStatuses(response.getNodeConnectionStatuses().stream() - .collect(Collectors.toMap(status -> status.getNodeIdentifier(), status -> status))); + .collect(Collectors.toMap(NodeConnectionStatus::getNodeIdentifier, status -> status))); } // get the dataflow from the response @@ -919,7 +1004,7 @@ private void loadFromConnectionResponse(final ConnectionResponse response) throw controller.setNodeId(nodeId); clusterCoordinator.setLocalNodeIdentifier(nodeId); clusterCoordinator.setConnected(true); - revisionManager.reset(response.getComponentRevisions().stream().map(rev -> rev.toRevision()).collect(Collectors.toList())); + revisionManager.reset(response.getComponentRevisions().stream().map(ComponentRevision::toRevision).collect(Collectors.toList())); // mark the node as clustered controller.setClustered(true, response.getInstanceId()); @@ -977,7 +1062,7 @@ public void copyCurrentFlow(final OutputStream os) throws IOException { } } - public void loadSnippets(final byte[] bytes) throws IOException { + public void loadSnippets(final byte[] bytes) { if (bytes.length == 0) { return; } @@ -996,7 +1081,7 @@ private class SaveReportingTask implements Runnable { public void run() { ClassLoader currentCl = null; - final Bundle frameworkBundle = NarClassLoaders.getInstance().getFrameworkBundle(); + final Bundle frameworkBundle = NarClassLoadersHolder.getInstance().getFrameworkBundle(); if (frameworkBundle != null) { currentCl = Thread.currentThread().getContextClassLoader(); final ClassLoader cl = frameworkBundle.getClassLoader(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSnippet.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSnippet.java new file mode 100644 index 000000000000..4d684fef7ee1 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSnippet.java @@ -0,0 +1,619 @@ +/* + * 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.controller; + +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.ConnectableType; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.connectable.Funnel; +import org.apache.nifi.connectable.Port; +import org.apache.nifi.connectable.Position; +import org.apache.nifi.connectable.Size; +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.flow.FlowManager; +import org.apache.nifi.controller.label.Label; +import org.apache.nifi.controller.queue.FlowFileQueue; +import org.apache.nifi.controller.queue.LoadBalanceStrategy; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor; +import org.apache.nifi.logging.LogLevel; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.registry.flow.StandardVersionControlInformation; +import org.apache.nifi.registry.flow.VersionControlInformation; +import org.apache.nifi.remote.RootGroupPort; +import org.apache.nifi.remote.StandardRemoteProcessGroupPortDescriptor; +import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; +import org.apache.nifi.scheduling.ExecutionNode; +import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.util.BundleUtils; +import org.apache.nifi.util.SnippetUtils; +import org.apache.nifi.web.api.dto.BatchSettingsDTO; +import org.apache.nifi.web.api.dto.BundleDTO; +import org.apache.nifi.web.api.dto.ConnectableDTO; +import org.apache.nifi.web.api.dto.ConnectionDTO; +import org.apache.nifi.web.api.dto.ControllerServiceDTO; +import org.apache.nifi.web.api.dto.FlowSnippetDTO; +import org.apache.nifi.web.api.dto.FunnelDTO; +import org.apache.nifi.web.api.dto.LabelDTO; +import org.apache.nifi.web.api.dto.PortDTO; +import org.apache.nifi.web.api.dto.PositionDTO; +import org.apache.nifi.web.api.dto.ProcessGroupDTO; +import org.apache.nifi.web.api.dto.ProcessorConfigDTO; +import org.apache.nifi.web.api.dto.ProcessorDTO; +import org.apache.nifi.web.api.dto.RelationshipDTO; +import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO; +import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO; +import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class StandardFlowSnippet implements FlowSnippet { + + private final FlowSnippetDTO dto; + private final ExtensionManager extensionManager; + + public StandardFlowSnippet(final FlowSnippetDTO dto, final ExtensionManager extensionManager) { + this.dto = dto; + this.extensionManager = extensionManager; + } + + public void validate(final ProcessGroup group) { + // validate the names of Input Ports + for (final PortDTO port : dto.getInputPorts()) { + if (group.getInputPortByName(port.getName()) != null) { + throw new IllegalStateException("One or more of the proposed Port names is not available in the process group"); + } + } + + // validate the names of Output Ports + for (final PortDTO port : dto.getOutputPorts()) { + if (group.getOutputPortByName(port.getName()) != null) { + throw new IllegalStateException("One or more of the proposed Port names is not available in the process group"); + } + } + + verifyComponentTypesInSnippet(); + + SnippetUtils.verifyNoVersionControlConflicts(dto, group); + } + + public void verifyComponentTypesInSnippet() { + final Map> processorClasses = new HashMap<>(); + for (final Class c : extensionManager.getExtensions(Processor.class)) { + final String name = c.getName(); + processorClasses.put(name, extensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); + } + verifyProcessorsInSnippet(dto, processorClasses); + + final Map> controllerServiceClasses = new HashMap<>(); + for (final Class c : extensionManager.getExtensions(ControllerService.class)) { + final String name = c.getName(); + controllerServiceClasses.put(name, extensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet())); + } + verifyControllerServicesInSnippet(dto, controllerServiceClasses); + + final Set prioritizerClasses = new HashSet<>(); + for (final Class c : extensionManager.getExtensions(FlowFilePrioritizer.class)) { + prioritizerClasses.add(c.getName()); + } + + final Set allConns = new HashSet<>(); + allConns.addAll(dto.getConnections()); + for (final ProcessGroupDTO childGroup : dto.getProcessGroups()) { + allConns.addAll(findAllConnections(childGroup)); + } + + for (final ConnectionDTO conn : allConns) { + final List prioritizers = conn.getPrioritizers(); + if (prioritizers != null) { + for (final String prioritizer : prioritizers) { + if (!prioritizerClasses.contains(prioritizer)) { + throw new IllegalStateException("Invalid FlowFile Prioritizer Type: " + prioritizer); + } + } + } + } + } + + public void instantiate(final FlowManager flowManager, final ProcessGroup group) throws ProcessorInstantiationException { + instantiate(flowManager, group, true); + } + + + + /** + * Recursively finds all ConnectionDTO's + * + * @param group group + * @return connection dtos + */ + private Set findAllConnections(final ProcessGroupDTO group) { + final Set conns = new HashSet<>(); + conns.addAll(group.getContents().getConnections()); + + for (final ProcessGroupDTO childGroup : group.getContents().getProcessGroups()) { + conns.addAll(findAllConnections(childGroup)); + } + return conns; + } + + private void verifyControllerServicesInSnippet(final FlowSnippetDTO templateContents, final Map> supportedTypes) { + if (templateContents.getControllerServices() != null) { + templateContents.getControllerServices().forEach(controllerService -> { + if (supportedTypes.containsKey(controllerService.getType())) { + if (controllerService.getBundle() == null) { + throw new IllegalArgumentException("Controller Service bundle must be specified."); + } + + verifyBundleInSnippet(controllerService.getBundle(), supportedTypes.get(controllerService.getType())); + } else { + throw new IllegalStateException("Invalid Controller Service Type: " + controllerService.getType()); + } + }); + } + + if (templateContents.getProcessGroups() != null) { + templateContents.getProcessGroups().forEach(processGroup -> verifyControllerServicesInSnippet(processGroup.getContents(), supportedTypes)); + } + } + + private void verifyBundleInSnippet(final BundleDTO requiredBundle, final Set supportedBundles) { + final BundleCoordinate requiredCoordinate = new BundleCoordinate(requiredBundle.getGroup(), requiredBundle.getArtifact(), requiredBundle.getVersion()); + if (!supportedBundles.contains(requiredCoordinate)) { + throw new IllegalStateException("Unsupported bundle: " + requiredCoordinate); + } + } + + private void verifyProcessorsInSnippet(final FlowSnippetDTO templateContents, final Map> supportedTypes) { + if (templateContents.getProcessors() != null) { + templateContents.getProcessors().forEach(processor -> { + if (processor.getBundle() == null) { + throw new IllegalArgumentException("Processor bundle must be specified."); + } + + if (supportedTypes.containsKey(processor.getType())) { + verifyBundleInSnippet(processor.getBundle(), supportedTypes.get(processor.getType())); + } else { + throw new IllegalStateException("Invalid Processor Type: " + processor.getType()); + } + }); + } + + if (templateContents.getProcessGroups() != null) { + templateContents.getProcessGroups().forEach(processGroup -> verifyProcessorsInSnippet(processGroup.getContents(), supportedTypes)); + } + } + + + public void instantiate(final FlowManager flowManager, final ProcessGroup group, final boolean topLevel) { + // + // Instantiate Controller Services + // + final List serviceNodes = new ArrayList<>(); + try { + for (final ControllerServiceDTO controllerServiceDTO : dto.getControllerServices()) { + final BundleCoordinate bundleCoordinate = BundleUtils.getBundle(extensionManager, controllerServiceDTO.getType(), controllerServiceDTO.getBundle()); + final ControllerServiceNode serviceNode = flowManager.createControllerService(controllerServiceDTO.getType(), controllerServiceDTO.getId(), + bundleCoordinate, Collections.emptySet(), true,true); + + serviceNode.pauseValidationTrigger(); + serviceNodes.add(serviceNode); + + serviceNode.setAnnotationData(controllerServiceDTO.getAnnotationData()); + serviceNode.setComments(controllerServiceDTO.getComments()); + serviceNode.setName(controllerServiceDTO.getName()); + if (!topLevel) { + serviceNode.setVersionedComponentId(controllerServiceDTO.getVersionedComponentId()); + } + + group.addControllerService(serviceNode); + } + + // configure controller services. We do this after creating all of them in case 1 service + // references another service. + for (final ControllerServiceDTO controllerServiceDTO : dto.getControllerServices()) { + final String serviceId = controllerServiceDTO.getId(); + final ControllerServiceNode serviceNode = flowManager.getControllerServiceNode(serviceId); + serviceNode.setProperties(controllerServiceDTO.getProperties()); + } + } finally { + serviceNodes.forEach(ControllerServiceNode::resumeValidationTrigger); + } + + // + // Instantiate the labels + // + for (final LabelDTO labelDTO : dto.getLabels()) { + final Label label = flowManager.createLabel(labelDTO.getId(), labelDTO.getLabel()); + label.setPosition(toPosition(labelDTO.getPosition())); + if (labelDTO.getWidth() != null && labelDTO.getHeight() != null) { + label.setSize(new Size(labelDTO.getWidth(), labelDTO.getHeight())); + } + + label.setStyle(labelDTO.getStyle()); + if (!topLevel) { + label.setVersionedComponentId(labelDTO.getVersionedComponentId()); + } + + group.addLabel(label); + } + + // Instantiate the funnels + for (final FunnelDTO funnelDTO : dto.getFunnels()) { + final Funnel funnel = flowManager.createFunnel(funnelDTO.getId()); + funnel.setPosition(toPosition(funnelDTO.getPosition())); + if (!topLevel) { + funnel.setVersionedComponentId(funnelDTO.getVersionedComponentId()); + } + + group.addFunnel(funnel); + } + + // + // Instantiate Input Ports & Output Ports + // + for (final PortDTO portDTO : dto.getInputPorts()) { + final Port inputPort; + if (group.isRootGroup()) { + inputPort = flowManager.createRemoteInputPort(portDTO.getId(), portDTO.getName()); + inputPort.setMaxConcurrentTasks(portDTO.getConcurrentlySchedulableTaskCount()); + if (portDTO.getGroupAccessControl() != null) { + ((RootGroupPort) inputPort).setGroupAccessControl(portDTO.getGroupAccessControl()); + } + if (portDTO.getUserAccessControl() != null) { + ((RootGroupPort) inputPort).setUserAccessControl(portDTO.getUserAccessControl()); + } + } else { + inputPort = flowManager.createLocalInputPort(portDTO.getId(), portDTO.getName()); + } + + if (!topLevel) { + inputPort.setVersionedComponentId(portDTO.getVersionedComponentId()); + } + inputPort.setPosition(toPosition(portDTO.getPosition())); + inputPort.setProcessGroup(group); + inputPort.setComments(portDTO.getComments()); + group.addInputPort(inputPort); + } + + for (final PortDTO portDTO : dto.getOutputPorts()) { + final Port outputPort; + if (group.isRootGroup()) { + outputPort = flowManager.createRemoteOutputPort(portDTO.getId(), portDTO.getName()); + outputPort.setMaxConcurrentTasks(portDTO.getConcurrentlySchedulableTaskCount()); + if (portDTO.getGroupAccessControl() != null) { + ((RootGroupPort) outputPort).setGroupAccessControl(portDTO.getGroupAccessControl()); + } + if (portDTO.getUserAccessControl() != null) { + ((RootGroupPort) outputPort).setUserAccessControl(portDTO.getUserAccessControl()); + } + } else { + outputPort = flowManager.createLocalOutputPort(portDTO.getId(), portDTO.getName()); + } + + if (!topLevel) { + outputPort.setVersionedComponentId(portDTO.getVersionedComponentId()); + } + outputPort.setPosition(toPosition(portDTO.getPosition())); + outputPort.setProcessGroup(group); + outputPort.setComments(portDTO.getComments()); + group.addOutputPort(outputPort); + } + + // + // Instantiate the processors + // + for (final ProcessorDTO processorDTO : dto.getProcessors()) { + final BundleCoordinate bundleCoordinate = BundleUtils.getBundle(extensionManager, processorDTO.getType(), processorDTO.getBundle()); + final ProcessorNode procNode = flowManager.createProcessor(processorDTO.getType(), processorDTO.getId(), bundleCoordinate); + procNode.pauseValidationTrigger(); + + try { + procNode.setPosition(toPosition(processorDTO.getPosition())); + procNode.setProcessGroup(group); + if (!topLevel) { + procNode.setVersionedComponentId(processorDTO.getVersionedComponentId()); + } + + final ProcessorConfigDTO config = processorDTO.getConfig(); + procNode.setComments(config.getComments()); + if (config.isLossTolerant() != null) { + procNode.setLossTolerant(config.isLossTolerant()); + } + procNode.setName(processorDTO.getName()); + + procNode.setYieldPeriod(config.getYieldDuration()); + procNode.setPenalizationPeriod(config.getPenaltyDuration()); + procNode.setBulletinLevel(LogLevel.valueOf(config.getBulletinLevel())); + procNode.setAnnotationData(config.getAnnotationData()); + procNode.setStyle(processorDTO.getStyle()); + + if (config.getRunDurationMillis() != null) { + procNode.setRunDuration(config.getRunDurationMillis(), TimeUnit.MILLISECONDS); + } + + if (config.getSchedulingStrategy() != null) { + procNode.setSchedulingStrategy(SchedulingStrategy.valueOf(config.getSchedulingStrategy())); + } + + if (config.getExecutionNode() != null) { + procNode.setExecutionNode(ExecutionNode.valueOf(config.getExecutionNode())); + } + + if (processorDTO.getState().equals(ScheduledState.DISABLED.toString())) { + procNode.disable(); + } + + // ensure that the scheduling strategy is set prior to these values + procNode.setMaxConcurrentTasks(config.getConcurrentlySchedulableTaskCount()); + procNode.setScheduldingPeriod(config.getSchedulingPeriod()); + + final Set relationships = new HashSet<>(); + if (processorDTO.getRelationships() != null) { + for (final RelationshipDTO rel : processorDTO.getRelationships()) { + if (rel.isAutoTerminate()) { + relationships.add(procNode.getRelationship(rel.getName())); + } + } + procNode.setAutoTerminatedRelationships(relationships); + } + + if (config.getProperties() != null) { + procNode.setProperties(config.getProperties()); + } + + group.addProcessor(procNode); + } finally { + procNode.resumeValidationTrigger(); + } + } + + // + // Instantiate Remote Process Groups + // + for (final RemoteProcessGroupDTO remoteGroupDTO : dto.getRemoteProcessGroups()) { + final RemoteProcessGroup remoteGroup = flowManager.createRemoteProcessGroup(remoteGroupDTO.getId(), remoteGroupDTO.getTargetUris()); + remoteGroup.setComments(remoteGroupDTO.getComments()); + remoteGroup.setPosition(toPosition(remoteGroupDTO.getPosition())); + remoteGroup.setCommunicationsTimeout(remoteGroupDTO.getCommunicationsTimeout()); + remoteGroup.setYieldDuration(remoteGroupDTO.getYieldDuration()); + if (!topLevel) { + remoteGroup.setVersionedComponentId(remoteGroupDTO.getVersionedComponentId()); + } + + if (remoteGroupDTO.getTransportProtocol() == null) { + remoteGroup.setTransportProtocol(SiteToSiteTransportProtocol.RAW); + } else { + remoteGroup.setTransportProtocol(SiteToSiteTransportProtocol.valueOf(remoteGroupDTO.getTransportProtocol())); + } + + remoteGroup.setProxyHost(remoteGroupDTO.getProxyHost()); + remoteGroup.setProxyPort(remoteGroupDTO.getProxyPort()); + remoteGroup.setProxyUser(remoteGroupDTO.getProxyUser()); + remoteGroup.setProxyPassword(remoteGroupDTO.getProxyPassword()); + remoteGroup.setProcessGroup(group); + + // set the input/output ports + if (remoteGroupDTO.getContents() != null) { + final RemoteProcessGroupContentsDTO contents = remoteGroupDTO.getContents(); + + // ensure there are input ports + if (contents.getInputPorts() != null) { + remoteGroup.setInputPorts(convertRemotePort(contents.getInputPorts()), false); + } + + // ensure there are output ports + if (contents.getOutputPorts() != null) { + remoteGroup.setOutputPorts(convertRemotePort(contents.getOutputPorts()), false); + } + } + + group.addRemoteProcessGroup(remoteGroup); + } + + // + // Instantiate ProcessGroups + // + for (final ProcessGroupDTO groupDTO : dto.getProcessGroups()) { + final ProcessGroup childGroup = flowManager.createProcessGroup(groupDTO.getId()); + childGroup.setParent(group); + childGroup.setPosition(toPosition(groupDTO.getPosition())); + childGroup.setComments(groupDTO.getComments()); + childGroup.setName(groupDTO.getName()); + if (groupDTO.getVariables() != null) { + childGroup.setVariables(groupDTO.getVariables()); + } + + // If this Process Group is 'top level' then we do not set versioned component ID's. + // We do this only if this component is the child of a Versioned Component. + if (!topLevel) { + childGroup.setVersionedComponentId(groupDTO.getVersionedComponentId()); + } + + group.addProcessGroup(childGroup); + + final FlowSnippetDTO contents = groupDTO.getContents(); + + // we want this to be recursive, so we will create a new template that contains only + // the contents of this child group and recursively call ourselves. + final FlowSnippetDTO childTemplateDTO = new FlowSnippetDTO(); + childTemplateDTO.setConnections(contents.getConnections()); + childTemplateDTO.setInputPorts(contents.getInputPorts()); + childTemplateDTO.setLabels(contents.getLabels()); + childTemplateDTO.setOutputPorts(contents.getOutputPorts()); + childTemplateDTO.setProcessGroups(contents.getProcessGroups()); + childTemplateDTO.setProcessors(contents.getProcessors()); + childTemplateDTO.setFunnels(contents.getFunnels()); + childTemplateDTO.setRemoteProcessGroups(contents.getRemoteProcessGroups()); + childTemplateDTO.setControllerServices(contents.getControllerServices()); + + final StandardFlowSnippet childSnippet = new StandardFlowSnippet(childTemplateDTO, extensionManager); + childSnippet.instantiate(flowManager, childGroup, false); + + if (groupDTO.getVersionControlInformation() != null) { + final VersionControlInformation vci = StandardVersionControlInformation.Builder + .fromDto(groupDTO.getVersionControlInformation()) + .build(); + childGroup.setVersionControlInformation(vci, Collections.emptyMap()); + } + } + + // + // Instantiate Connections + // + for (final ConnectionDTO connectionDTO : dto.getConnections()) { + final ConnectableDTO sourceDTO = connectionDTO.getSource(); + final ConnectableDTO destinationDTO = connectionDTO.getDestination(); + final Connectable source; + final Connectable destination; + + // Locate the source and destination connectable. If this is a remote port we need to locate the remote process groups. Otherwise, we need to + // find the connectable given its parent group. + // + // NOTE: (getConnectable returns ANY connectable, when the parent is not this group only input ports or output ports should be returned. If something + // other than a port is returned, an exception will be thrown when adding the connection below.) + + // See if the source connectable is a remote port + if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceDTO.getType())) { + final RemoteProcessGroup remoteGroup = group.getRemoteProcessGroup(sourceDTO.getGroupId()); + source = remoteGroup.getOutputPort(sourceDTO.getId()); + } else { + final ProcessGroup sourceGroup = getConnectableParent(group, sourceDTO.getGroupId(), flowManager); + source = sourceGroup.getConnectable(sourceDTO.getId()); + } + + // see if the destination connectable is a remote port + if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationDTO.getType())) { + final RemoteProcessGroup remoteGroup = group.getRemoteProcessGroup(destinationDTO.getGroupId()); + destination = remoteGroup.getInputPort(destinationDTO.getId()); + } else { + final ProcessGroup destinationGroup = getConnectableParent(group, destinationDTO.getGroupId(), flowManager); + destination = destinationGroup.getConnectable(destinationDTO.getId()); + } + + // determine the selection relationships for this connection + final Set relationships = new HashSet<>(); + if (connectionDTO.getSelectedRelationships() != null) { + relationships.addAll(connectionDTO.getSelectedRelationships()); + } + + final Connection connection = flowManager.createConnection(connectionDTO.getId(), connectionDTO.getName(), source, destination, relationships); + if (!topLevel) { + connection.setVersionedComponentId(connectionDTO.getVersionedComponentId()); + } + + if (connectionDTO.getBends() != null) { + final List bendPoints = new ArrayList<>(); + for (final PositionDTO bend : connectionDTO.getBends()) { + bendPoints.add(new Position(bend.getX(), bend.getY())); + } + connection.setBendPoints(bendPoints); + } + + final FlowFileQueue queue = connection.getFlowFileQueue(); + queue.setBackPressureDataSizeThreshold(connectionDTO.getBackPressureDataSizeThreshold()); + queue.setBackPressureObjectThreshold(connectionDTO.getBackPressureObjectThreshold()); + queue.setFlowFileExpiration(connectionDTO.getFlowFileExpiration()); + + final List prioritizers = connectionDTO.getPrioritizers(); + if (prioritizers != null) { + final List newPrioritizersClasses = new ArrayList<>(prioritizers); + final List newPrioritizers = new ArrayList<>(); + for (final String className : newPrioritizersClasses) { + try { + newPrioritizers.add(flowManager.createPrioritizer(className)); + } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e); + } + } + queue.setPriorities(newPrioritizers); + } + + final String loadBalanceStrategyName = connectionDTO.getLoadBalanceStrategy(); + if (loadBalanceStrategyName != null) { + final LoadBalanceStrategy loadBalanceStrategy = LoadBalanceStrategy.valueOf(loadBalanceStrategyName); + final String partitioningAttribute = connectionDTO.getLoadBalancePartitionAttribute(); + queue.setLoadBalanceStrategy(loadBalanceStrategy, partitioningAttribute); + } + + connection.setProcessGroup(group); + group.addConnection(connection); + } + } + + private ProcessGroup getConnectableParent(final ProcessGroup group, final String parentGroupId, final FlowManager flowManager) { + if (flowManager.areGroupsSame(group.getIdentifier(), parentGroupId)) { + return group; + } else { + return group.getProcessGroup(parentGroupId); + } + } + + + private Position toPosition(final PositionDTO dto) { + return new Position(dto.getX(), dto.getY()); + } + + /** + * Converts a set of ports into a set of remote process group ports. + * + * @param ports ports + * @return group descriptors + */ + private Set convertRemotePort(final Set ports) { + Set remotePorts = null; + if (ports != null) { + remotePorts = new LinkedHashSet<>(ports.size()); + for (final RemoteProcessGroupPortDTO port : ports) { + final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor(); + descriptor.setId(port.getId()); + descriptor.setVersionedComponentId(port.getVersionedComponentId()); + descriptor.setTargetId(port.getTargetId()); + descriptor.setName(port.getName()); + descriptor.setComments(port.getComments()); + descriptor.setTargetRunning(port.isTargetRunning()); + descriptor.setConnected(port.isConnected()); + descriptor.setConcurrentlySchedulableTaskCount(port.getConcurrentlySchedulableTaskCount()); + descriptor.setTransmitting(port.isTransmitting()); + descriptor.setUseCompression(port.getUseCompression()); + final BatchSettingsDTO batchSettings = port.getBatchSettings(); + if (batchSettings != null) { + descriptor.setBatchCount(batchSettings.getCount()); + descriptor.setBatchSize(batchSettings.getSize()); + descriptor.setBatchDuration(batchSettings.getDuration()); + } + remotePorts.add(descriptor); + } + } + return remotePorts; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java index d47e198a289f..a7ced2c01bab 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java @@ -30,15 +30,14 @@ import org.apache.nifi.connectable.ConnectableType; import org.apache.nifi.connectable.Connection; import org.apache.nifi.connectable.Funnel; -import org.apache.nifi.controller.queue.LoadBalanceCompression; -import org.apache.nifi.controller.queue.LoadBalanceStrategy; import org.apache.nifi.connectable.Port; import org.apache.nifi.connectable.Position; import org.apache.nifi.connectable.Size; -import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.flow.FlowManager; import org.apache.nifi.controller.label.Label; +import org.apache.nifi.controller.queue.LoadBalanceCompression; +import org.apache.nifi.controller.queue.LoadBalanceStrategy; import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException; -import org.apache.nifi.controller.reporting.StandardReportingInitializationContext; import org.apache.nifi.controller.serialization.FlowEncodingVersion; import org.apache.nifi.controller.serialization.FlowFromDOMFactory; import org.apache.nifi.controller.serialization.FlowSerializationException; @@ -47,6 +46,7 @@ import org.apache.nifi.controller.serialization.StandardFlowSerializer; import org.apache.nifi.controller.service.ControllerServiceLoader; import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.ControllerServiceState; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.events.BulletinFactory; @@ -56,10 +56,9 @@ import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor; -import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.logging.LogLevel; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.processor.Relationship; -import org.apache.nifi.processor.SimpleProcessLogger; import org.apache.nifi.registry.flow.FlowRegistry; import org.apache.nifi.registry.flow.FlowRegistryClient; import org.apache.nifi.registry.flow.StandardVersionControlInformation; @@ -67,8 +66,6 @@ import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.remote.RootGroupPort; import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; -import org.apache.nifi.reporting.InitializationException; -import org.apache.nifi.reporting.ReportingInitializationContext; import org.apache.nifi.reporting.Severity; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.scheduling.SchedulingStrategy; @@ -137,11 +134,13 @@ public class StandardFlowSynchronizer implements FlowSynchronizer { private final StringEncryptor encryptor; private final boolean autoResumeState; private final NiFiProperties nifiProperties; + private final ExtensionManager extensionManager; - public StandardFlowSynchronizer(final StringEncryptor encryptor, final NiFiProperties nifiProperties) { + public StandardFlowSynchronizer(final StringEncryptor encryptor, final NiFiProperties nifiProperties, final ExtensionManager extensionManager) { this.encryptor = encryptor; - autoResumeState = nifiProperties.getAutoResumeState(); + this.autoResumeState = nifiProperties.getAutoResumeState(); this.nifiProperties = nifiProperties; + this.extensionManager = extensionManager; } public static boolean isEmpty(final DataFlow dataFlow) { @@ -169,9 +168,12 @@ public static boolean isEmpty(final DataFlow dataFlow) { public void sync(final FlowController controller, final DataFlow proposedFlow, final StringEncryptor encryptor) throws FlowSerializationException, UninheritableFlowException, FlowSynchronizationException, MissingBundleException { + final FlowManager flowManager = controller.getFlowManager(); + final ProcessGroup root = flowManager.getRootGroup(); + // handle corner cases involving no proposed flow if (proposedFlow == null) { - if (controller.getGroup(controller.getRootGroupId()).isEmpty()) { + if (root.isEmpty()) { return; // no sync to perform } else { throw new UninheritableFlowException("Proposed configuration is empty, but the controller contains a data flow."); @@ -188,9 +190,9 @@ public void sync(final FlowController controller, final DataFlow proposedFlow, f try { if (flowAlreadySynchronized) { existingFlow = toBytes(controller); - existingFlowEmpty = controller.getGroup(controller.getRootGroupId()).isEmpty() - && controller.getAllReportingTasks().isEmpty() - && controller.getAllControllerServices().isEmpty() + existingFlowEmpty = root.isEmpty() + && flowManager.getAllReportingTasks().isEmpty() + && flowManager.getAllControllerServices().isEmpty() && controller.getFlowRegistryClient().getRegistryIdentifiers().isEmpty(); } else { existingFlow = readFlowFromDisk(); @@ -268,9 +270,9 @@ && isEmpty(rootGroupDto) } final Set missingComponents = new HashSet<>(); - controller.getAllControllerServices().stream().filter(cs -> cs.isExtensionMissing()).forEach(cs -> missingComponents.add(cs.getIdentifier())); - controller.getAllReportingTasks().stream().filter(r -> r.isExtensionMissing()).forEach(r -> missingComponents.add(r.getIdentifier())); - controller.getRootGroup().findAllProcessors().stream().filter(p -> p.isExtensionMissing()).forEach(p -> missingComponents.add(p.getIdentifier())); + flowManager.getAllControllerServices().stream().filter(ComponentNode::isExtensionMissing).forEach(cs -> missingComponents.add(cs.getIdentifier())); + flowManager.getAllReportingTasks().stream().filter(ComponentNode::isExtensionMissing).forEach(r -> missingComponents.add(r.getIdentifier())); + root.findAllProcessors().stream().filter(AbstractComponentNode::isExtensionMissing).forEach(p -> missingComponents.add(p.getIdentifier())); final DataFlow existingDataFlow = new StandardDataFlow(existingFlow, existingSnippets, existingAuthFingerprint, missingComponents); @@ -413,7 +415,7 @@ && isEmpty(rootGroupDto) final Set controllerServicesInReportingTasks = reportingTaskNodesToDTOs.keySet().stream() .flatMap(r -> r.getProperties().entrySet().stream()) .filter(e -> e.getKey().getControllerServiceDefinition() != null) - .map(e -> e.getValue()) + .map(Map.Entry::getValue) .collect(Collectors.toSet()); // find the controller service nodes for each id referenced by a reporting task @@ -425,7 +427,7 @@ && isEmpty(rootGroupDto) final Map controllerServiceMapping = new HashMap<>(); for (ControllerServiceNode controllerService : controllerServicesToClone) { final ControllerServiceNode clone = ControllerServiceLoader.cloneControllerService(controller, controllerService); - controller.addRootControllerService(clone); + flowManager.addRootControllerService(clone); controllerServiceMapping.put(controllerService.getIdentifier(), clone); } @@ -490,7 +492,7 @@ private void checkBundleCompatibility(final Document configuration) { if (!withinTemplate(componentElement)) { final String componentType = DomUtils.getChildText(componentElement, "class"); try { - BundleUtils.getBundle(componentType, FlowFromDOMFactory.getBundle(bundleElement)); + BundleUtils.getBundle(extensionManager, componentType, FlowFromDOMFactory.getBundle(bundleElement)); } catch (IllegalStateException e) { throw new MissingBundleException(e.getMessage(), e); } @@ -644,7 +646,7 @@ private ReportingTaskNode getOrCreateReportingTask(final FlowController controll if (!controllerInitialized || existingFlowEmpty) { BundleCoordinate coordinate; try { - coordinate = BundleUtils.getCompatibleBundle(dto.getType(), dto.getBundle()); + coordinate = BundleUtils.getCompatibleBundle(extensionManager, dto.getType(), dto.getBundle()); } catch (final IllegalStateException e) { final BundleDTO bundleDTO = dto.getBundle(); if (bundleDTO == null) { @@ -662,17 +664,6 @@ private ReportingTaskNode getOrCreateReportingTask(final FlowController controll reportingTask.setAnnotationData(dto.getAnnotationData()); reportingTask.setProperties(dto.getProperties()); - - final ComponentLog componentLog = new SimpleProcessLogger(dto.getId(), reportingTask.getReportingTask()); - final ReportingInitializationContext config = new StandardReportingInitializationContext(dto.getId(), dto.getName(), - SchedulingStrategy.valueOf(dto.getSchedulingStrategy()), dto.getSchedulingPeriod(), componentLog, controller, nifiProperties, controller); - - try { - reportingTask.getReportingTask().initialize(config); - } catch (final InitializationException ie) { - throw new ReportingTaskInstantiationException("Failed to initialize reporting task of type " + dto.getType(), ie); - } - return reportingTask; } else { // otherwise return the existing reporting task node @@ -770,13 +761,14 @@ private ControllerServiceState getFinalTransitionState(final ControllerServiceSt } private ProcessGroup updateProcessGroup(final FlowController controller, final ProcessGroup parentGroup, final Element processGroupElement, - final StringEncryptor encryptor, final FlowEncodingVersion encodingVersion) throws ProcessorInstantiationException { + final StringEncryptor encryptor, final FlowEncodingVersion encodingVersion) { // get the parent group ID final String parentId = (parentGroup == null) ? null : parentGroup.getIdentifier(); // get the process group final ProcessGroupDTO processGroupDto = FlowFromDOMFactory.getProcessGroup(parentId, processGroupElement, encryptor, encodingVersion); + final FlowManager flowManager = controller.getFlowManager(); // update the process group if (parentId == null) { @@ -787,17 +779,22 @@ private ProcessGroup updateProcessGroup(final FlowController controller, final P * Therefore, we first remove all labels, and then let the updating * process add labels defined in the new flow. */ - final ProcessGroup root = controller.getGroup(controller.getRootGroupId()); + final ProcessGroup root = flowManager.getRootGroup(); for (final Label label : root.findAllLabels()) { label.getProcessGroup().removeLabel(label); } } // update the process group - controller.updateProcessGroup(processGroupDto); + final ProcessGroup group = flowManager.getGroup(processGroupDto.getId()); + if (group == null) { + throw new IllegalStateException("No Group with ID " + processGroupDto.getId() + " exists"); + } + + updateProcessGroup(group, processGroupDto); // get the real process group and ID - final ProcessGroup processGroup = controller.getGroup(processGroupDto.getId()); + final ProcessGroup processGroup = flowManager.getGroup(processGroupDto.getId()); // determine the scheduled state of all of the Controller Service final List controllerServiceNodeList = getChildrenByTagName(processGroupElement, "controllerService"); @@ -827,10 +824,11 @@ private ProcessGroup updateProcessGroup(final FlowController controller, final P } // Ensure that all services have been validated, so that we don't attempt to enable a service that is still in a 'validating' state - toEnable.stream().forEach(ControllerServiceNode::performValidation); + toEnable.forEach(ControllerServiceNode::performValidation); - controller.disableControllerServicesAsync(toDisable); - controller.enableControllerServices(toEnable); + final ControllerServiceProvider serviceProvider = controller.getControllerServiceProvider(); + serviceProvider.disableControllerServicesAsync(toDisable); + serviceProvider.enableControllerServices(toEnable); // processors & ports cannot be updated - they must be the same. Except for the scheduled state. final List processorNodeList = getChildrenByTagName(processGroupElement, "processor"); @@ -994,7 +992,7 @@ private ProcessGroup updateProcessGroup(final FlowController controller, final P final List labelNodeList = getChildrenByTagName(processGroupElement, "label"); for (final Element labelElement : labelNodeList) { final LabelDTO labelDTO = FlowFromDOMFactory.getLabel(labelElement); - final Label label = controller.createLabel(labelDTO.getId(), labelDTO.getLabel()); + final Label label = flowManager.createLabel(labelDTO.getId(), labelDTO.getLabel()); label.setStyle(labelDTO.getStyle()); label.setPosition(new Position(labelDTO.getPosition().getX(), labelDTO.getPosition().getY())); label.setVersionedComponentId(labelDTO.getVersionedComponentId()); @@ -1040,7 +1038,7 @@ private ProcessGroup updateProcessGroup(final FlowController controller, final P newPrioritizers = new ArrayList<>(); for (final String className : newPrioritizersClasses) { try { - newPrioritizers.add(controller.createPrioritizer(className)); + newPrioritizers.add(flowManager.createPrioritizer(className)); } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e); } @@ -1083,6 +1081,34 @@ private ProcessGroup updateProcessGroup(final FlowController controller, final P return processGroup; } + /** + * Updates the process group corresponding to the specified DTO. Any field + * in DTO that is null (with the exception of the required ID) + * will be ignored. + * + * @throws IllegalStateException if no process group can be found with the + * ID of DTO or with the ID of the DTO's parentGroupId, if the template ID + * specified is invalid, or if the DTO's Parent Group ID changes but the + * parent group has incoming or outgoing connections + * + * @throws NullPointerException if the DTO or its ID is null + */ + private void updateProcessGroup(final ProcessGroup group, final ProcessGroupDTO dto) { + final String name = dto.getName(); + final PositionDTO position = dto.getPosition(); + final String comments = dto.getComments(); + + if (name != null) { + group.setName(name); + } + if (position != null) { + group.setPosition(toPosition(position)); + } + if (comments != null) { + group.setComments(comments); + } + } + private ScheduledState getScheduledState(final T component, final FlowController flowController) { final ScheduledState componentState = component.getScheduledState(); if (componentState == ScheduledState.STOPPED) { @@ -1098,8 +1124,7 @@ private Position toPosition(final PositionDTO dto) { return new Position(dto.getX(), dto.getY()); } - private void updateProcessor(final ProcessorNode procNode, final ProcessorDTO processorDTO, final ProcessGroup processGroup, final FlowController controller) - throws ProcessorInstantiationException { + private void updateProcessor(final ProcessorNode procNode, final ProcessorDTO processorDTO, final ProcessGroup processGroup, final FlowController controller) { procNode.pauseValidationTrigger(); try { @@ -1160,13 +1185,15 @@ private void updateNonFingerprintedProcessorSettings(final ProcessorNode procNod } private ProcessGroup addProcessGroup(final FlowController controller, final ProcessGroup parentGroup, final Element processGroupElement, - final StringEncryptor encryptor, final FlowEncodingVersion encodingVersion) throws ProcessorInstantiationException { + final StringEncryptor encryptor, final FlowEncodingVersion encodingVersion) { + // get the parent group ID final String parentId = (parentGroup == null) ? null : parentGroup.getIdentifier(); + final FlowManager flowManager = controller.getFlowManager(); // add the process group final ProcessGroupDTO processGroupDTO = FlowFromDOMFactory.getProcessGroup(parentId, processGroupElement, encryptor, encodingVersion); - final ProcessGroup processGroup = controller.createProcessGroup(processGroupDTO.getId()); + final ProcessGroup processGroup = flowManager.createProcessGroup(processGroupDTO.getId()); processGroup.setComments(processGroupDTO.getComments()); processGroup.setVersionedComponentId(processGroupDTO.getVersionedComponentId()); processGroup.setPosition(toPosition(processGroupDTO.getPosition())); @@ -1222,7 +1249,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc BundleCoordinate coordinate; try { - coordinate = BundleUtils.getCompatibleBundle(processorDTO.getType(), processorDTO.getBundle()); + coordinate = BundleUtils.getCompatibleBundle(extensionManager, processorDTO.getType(), processorDTO.getBundle()); } catch (final IllegalStateException e) { final BundleDTO bundleDTO = processorDTO.getBundle(); if (bundleDTO == null) { @@ -1232,7 +1259,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc } } - final ProcessorNode procNode = controller.createProcessor(processorDTO.getType(), processorDTO.getId(), coordinate, false); + final ProcessorNode procNode = flowManager.createProcessor(processorDTO.getType(), processorDTO.getId(), coordinate, false); procNode.setVersionedComponentId(processorDTO.getVersionedComponentId()); processGroup.addProcessor(procNode); updateProcessor(procNode, processorDTO, processGroup, controller); @@ -1245,9 +1272,9 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final Port port; if (processGroup.isRootGroup()) { - port = controller.createRemoteInputPort(portDTO.getId(), portDTO.getName()); + port = flowManager.createRemoteInputPort(portDTO.getId(), portDTO.getName()); } else { - port = controller.createLocalInputPort(portDTO.getId(), portDTO.getName()); + port = flowManager.createLocalInputPort(portDTO.getId(), portDTO.getName()); } port.setVersionedComponentId(portDTO.getVersionedComponentId()); @@ -1290,9 +1317,9 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final Port port; if (processGroup.isRootGroup()) { - port = controller.createRemoteOutputPort(portDTO.getId(), portDTO.getName()); + port = flowManager.createRemoteOutputPort(portDTO.getId(), portDTO.getName()); } else { - port = controller.createLocalOutputPort(portDTO.getId(), portDTO.getName()); + port = flowManager.createLocalOutputPort(portDTO.getId(), portDTO.getName()); } port.setVersionedComponentId(portDTO.getVersionedComponentId()); @@ -1332,7 +1359,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final List funnelNodeList = getChildrenByTagName(processGroupElement, "funnel"); for (final Element funnelElement : funnelNodeList) { final FunnelDTO funnelDTO = FlowFromDOMFactory.getFunnel(funnelElement); - final Funnel funnel = controller.createFunnel(funnelDTO.getId()); + final Funnel funnel = flowManager.createFunnel(funnelDTO.getId()); funnel.setVersionedComponentId(funnelDTO.getVersionedComponentId()); funnel.setPosition(toPosition(funnelDTO.getPosition())); @@ -1347,7 +1374,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final List labelNodeList = getChildrenByTagName(processGroupElement, "label"); for (final Element labelElement : labelNodeList) { final LabelDTO labelDTO = FlowFromDOMFactory.getLabel(labelElement); - final Label label = controller.createLabel(labelDTO.getId(), labelDTO.getLabel()); + final Label label = flowManager.createLabel(labelDTO.getId(), labelDTO.getLabel()); label.setVersionedComponentId(labelDTO.getVersionedComponentId()); label.setStyle(labelDTO.getStyle()); @@ -1366,7 +1393,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final List remoteProcessGroupNodeList = getChildrenByTagName(processGroupElement, "remoteProcessGroup"); for (final Element remoteProcessGroupElement : remoteProcessGroupNodeList) { final RemoteProcessGroupDTO remoteGroupDto = FlowFromDOMFactory.getRemoteProcessGroup(remoteProcessGroupElement, encryptor); - final RemoteProcessGroup remoteGroup = controller.createRemoteProcessGroup(remoteGroupDto.getId(), remoteGroupDto.getTargetUris()); + final RemoteProcessGroup remoteGroup = flowManager.createRemoteProcessGroup(remoteGroupDto.getId(), remoteGroupDto.getTargetUris()); remoteGroup.setVersionedComponentId(remoteGroupDto.getVersionedComponentId()); remoteGroup.setComments(remoteGroupDto.getComments()); remoteGroup.setPosition(toPosition(remoteGroupDto.getPosition())); @@ -1446,7 +1473,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final RemoteProcessGroup remoteGroup = processGroup.getRemoteProcessGroup(sourceDto.getGroupId()); source = remoteGroup.getOutputPort(sourceDto.getId()); } else { - final ProcessGroup sourceGroup = controller.getGroup(sourceDto.getGroupId()); + final ProcessGroup sourceGroup = flowManager.getGroup(sourceDto.getGroupId()); if (sourceGroup == null) { throw new RuntimeException("Found Invalid ProcessGroup ID for Source: " + dto.getSource().getGroupId()); } @@ -1463,7 +1490,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc final RemoteProcessGroup remoteGroup = processGroup.getRemoteProcessGroup(destinationDto.getGroupId()); destination = remoteGroup.getInputPort(destinationDto.getId()); } else { - final ProcessGroup destinationGroup = controller.getGroup(destinationDto.getGroupId()); + final ProcessGroup destinationGroup = flowManager.getGroup(destinationDto.getGroupId()); if (destinationGroup == null) { throw new RuntimeException("Found Invalid ProcessGroup ID for Destination: " + dto.getDestination().getGroupId()); } @@ -1474,7 +1501,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc throw new RuntimeException("Found Invalid Connectable ID for Destination: " + dto.getDestination().getId()); } - final Connection connection = controller.createConnection(dto.getId(), dto.getName(), source, destination, dto.getSelectedRelationships()); + final Connection connection = flowManager.createConnection(dto.getId(), dto.getName(), source, destination, dto.getSelectedRelationships()); connection.setVersionedComponentId(dto.getVersionedComponentId()); connection.setProcessGroup(processGroup); @@ -1500,7 +1527,7 @@ private ProcessGroup addProcessGroup(final FlowController controller, final Proc newPrioritizers = new ArrayList<>(); for (final String className : newPrioritizersClasses) { try { - newPrioritizers.add(controller.createPrioritizer(className)); + newPrioritizers.add(flowManager.createPrioritizer(className)); } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e); } @@ -1651,7 +1678,7 @@ private String checkFlowInheritability(final byte[] existingFlow, final byte[] p } // check if the Flow is inheritable - final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor); + final FingerprintFactory fingerprintFactory = new FingerprintFactory(encryptor, extensionManager); final String existingFlowFingerprintBeforeHash = fingerprintFactory.createFingerprint(existingFlow, controller); if (existingFlowFingerprintBeforeHash.trim().isEmpty()) { return null; // no existing flow, so equivalent to proposed flow diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java index 2cee3d418514..a68eaab58147 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java @@ -16,35 +16,6 @@ */ package org.apache.nifi.controller; -import static java.util.Objects.requireNonNull; - -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; -import java.lang.management.ThreadMXBean; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; @@ -67,6 +38,7 @@ import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.validation.ValidationState; +import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.components.validation.ValidationTrigger; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.ConnectableType; @@ -82,6 +54,7 @@ import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.logging.LogLevel; import org.apache.nifi.logging.LogRepositoryFactory; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSessionFactory; @@ -93,7 +66,6 @@ import org.apache.nifi.scheduling.SchedulingStrategy; import org.apache.nifi.util.CharacterFilterUtils; import org.apache.nifi.util.FormatUtils; -import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.ReflectionUtils; import org.apache.nifi.util.ThreadUtils; import org.apache.nifi.util.file.classloader.ClassLoaderUtils; @@ -102,6 +74,35 @@ import org.slf4j.LoggerFactory; import org.springframework.util.Assert; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; + /** * ProcessorNode provides thread-safe access to a FlowFileProcessor as it exists * within a controlled flow. This node keeps track of the processor, its @@ -139,33 +140,34 @@ public class StandardProcessorNode extends ProcessorNode implements Connectable private final ProcessScheduler processScheduler; private long runNanos = 0L; private volatile long yieldNanos; - private volatile ScheduledState desiredState; + private volatile ScheduledState desiredState = ScheduledState.STOPPED; + private volatile LogLevel bulletinLevel = LogLevel.WARN; private SchedulingStrategy schedulingStrategy; // guarded by read/write lock // ??????? NOT any more private ExecutionNode executionNode; - private final long onScheduleTimeoutMillis; private final Map activeThreads = new HashMap<>(48); private final int hashCode; private volatile boolean hasActiveThreads = false; public StandardProcessorNode(final LoggableComponent processor, final String uuid, final ValidationContextFactory validationContextFactory, final ProcessScheduler scheduler, - final ControllerServiceProvider controllerServiceProvider, final NiFiProperties nifiProperties, - final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger) { + final ControllerServiceProvider controllerServiceProvider, final ComponentVariableRegistry variableRegistry, + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger) { this(processor, uuid, validationContextFactory, scheduler, controllerServiceProvider, processor.getComponent().getClass().getSimpleName(), - processor.getComponent().getClass().getCanonicalName(), nifiProperties, variableRegistry, reloadComponent, validationTrigger, false); + processor.getComponent().getClass().getCanonicalName(), variableRegistry, reloadComponent, extensionManager, validationTrigger, false); } public StandardProcessorNode(final LoggableComponent processor, final String uuid, final ValidationContextFactory validationContextFactory, final ProcessScheduler scheduler, final ControllerServiceProvider controllerServiceProvider, - final String componentType, final String componentCanonicalClass, final NiFiProperties nifiProperties, - final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, + final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry, + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { - super(uuid, validationContextFactory, controllerServiceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, validationTrigger, isExtensionMissing); + super(uuid, validationContextFactory, controllerServiceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, + extensionManager, validationTrigger, isExtensionMissing); final ProcessorDetails processorDetails = new ProcessorDetails(processor); this.processorRef = new AtomicReference<>(processorDetails); @@ -189,9 +191,6 @@ public StandardProcessorNode(final LoggableComponent processor, final processScheduler = scheduler; penalizationPeriod = new AtomicReference<>(DEFAULT_PENALIZATION_PERIOD); - final String timeoutString = nifiProperties.getProperty(NiFiProperties.PROCESSOR_SCHEDULING_TIMEOUT); - onScheduleTimeoutMillis = timeoutString == null ? 60000 : FormatUtils.getTimeDuration(timeoutString.trim(), TimeUnit.MILLISECONDS); - schedulingStrategy = SchedulingStrategy.TIMER_DRIVEN; executionNode = isExecutionNodeRestricted() ? ExecutionNode.PRIMARY : ExecutionNode.ALL; this.hashCode = new HashCodeBuilder(7, 67).append(identifier).toHashCode(); @@ -448,12 +447,8 @@ public synchronized void setName(final String name) { } /** - * @param timeUnit - * determines the unit of time to represent the scheduling - * period. If null will be reported in units of - * {@link #DEFAULT_SCHEDULING_TIME_UNIT} - * @return the schedule period that should elapse before subsequent cycles - * of this processor's tasks + * @param timeUnit determines the unit of time to represent the scheduling period. + * @return the schedule period that should elapse before subsequent cycles of this processor's tasks */ @Override public long getSchedulingPeriod(final TimeUnit timeUnit) { @@ -594,7 +589,7 @@ public synchronized void setYieldPeriod(final String yieldPeriod) { * Causes the processor not to be scheduled for some period of time. This * duration can be obtained and set via the * {@link #getYieldPeriod(TimeUnit)} and - * {@link #setYieldPeriod(long, TimeUnit)} methods. + * {@link #setYieldPeriod(String)}. */ @Override public void yield() { @@ -690,12 +685,13 @@ public int getMaxConcurrentTasks() { @Override public LogLevel getBulletinLevel() { - return LogRepositoryFactory.getRepository(getIdentifier()).getObservationLevel(BULLETIN_OBSERVER_ID); + return bulletinLevel; } @Override public synchronized void setBulletinLevel(final LogLevel level) { LogRepositoryFactory.getRepository(getIdentifier()).setObservationLevel(BULLETIN_OBSERVER_ID, level); + this.bulletinLevel = level; } @Override @@ -916,7 +912,7 @@ public Relationship getRelationship(final String relationshipName) { final Set relationships; final Processor processor = processorRef.get().getProcessor(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { relationships = processor.getRelationships(); } @@ -983,7 +979,7 @@ public Set getUndefinedRelationships() { final Set undefined = new HashSet<>(); final Set relationships; final Processor processor = processorRef.get().getProcessor(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { relationships = processor.getRelationships(); } @@ -1131,7 +1127,7 @@ public int hashCode() { @Override public Collection getRelationships() { final Processor processor = processorRef.get().getProcessor(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { return getProcessor().getRelationships(); } } @@ -1139,7 +1135,7 @@ public Collection getRelationships() { @Override public String toString() { final Processor processor = processorRef.get().getProcessor(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { return getProcessor().toString(); } } @@ -1161,7 +1157,7 @@ public void onTrigger(final ProcessContext context, final ProcessSessionFactory final Processor processor = processorRef.get().getProcessor(); activateThread(); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { processor.onTrigger(context, sessionFactory); } finally { deactivateThread(); @@ -1345,16 +1341,9 @@ public void disable() { *

*/ @Override - public void start(final ScheduledExecutorService taskScheduler, final long administrativeYieldMillis, final ProcessContext processContext, + public void start(final ScheduledExecutorService taskScheduler, final long administrativeYieldMillis, final long timeoutMillis, final ProcessContext processContext, final SchedulingAgentCallback schedulingAgentCallback, final boolean failIfStopping) { - switch (getValidationStatus()) { - case INVALID: - throw new IllegalStateException("Processor " + this.getName() + " is not in a valid state due to " + this.getValidationErrors()); - case VALIDATING: - throw new IllegalStateException("Processor " + this.getName() + " cannot be started because its validation is still being performed"); - } - final Processor processor = processorRef.get().getProcessor(); final ComponentLog procLog = new SimpleProcessLogger(StandardProcessorNode.this.getIdentifier(), processor); @@ -1378,7 +1367,7 @@ public void start(final ScheduledExecutorService taskScheduler, final long admin if (starting) { // will ensure that the Processor represented by this node can only be started once hasActiveThreads = true; - initiateStart(taskScheduler, administrativeYieldMillis, processContext, schedulingAgentCallback); + initiateStart(taskScheduler, administrativeYieldMillis, timeoutMillis, processContext, schedulingAgentCallback); } else { final String procName = processorRef.get().toString(); LOG.warn("Cannot start {} because it is not currently stopped. Current state is {}", procName, currentState); @@ -1481,7 +1470,7 @@ public void verifyCanTerminate() { } - private void initiateStart(final ScheduledExecutorService taskScheduler, final long administrativeYieldMillis, + private void initiateStart(final ScheduledExecutorService taskScheduler, final long administrativeYieldMillis, final long timeoutMilis, final ProcessContext processContext, final SchedulingAgentCallback schedulingAgentCallback) { final Processor processor = getProcessor(); @@ -1492,12 +1481,32 @@ private void initiateStart(final ScheduledExecutorService taskScheduler, final l // Create a task to invoke the @OnScheduled annotation of the processor final Callable startupTask = () -> { + final ScheduledState currentScheduleState = scheduledState.get(); + if (currentScheduleState == ScheduledState.STOPPING || currentScheduleState == ScheduledState.STOPPED) { + LOG.debug("{} is stopped. Will not call @OnScheduled lifecycle methods or begin trigger onTrigger() method", StandardProcessorNode.this); + schedulingAgentCallback.onTaskComplete(); + scheduledState.set(ScheduledState.STOPPED); + return null; + } + + final ValidationStatus validationStatus = getValidationStatus(); + if (validationStatus != ValidationStatus.VALID) { + LOG.debug("Cannot start {} because Processor is currently not valid; will try again after 5 seconds", StandardProcessorNode.this); + + // re-initiate the entire process + final Runnable initiateStartTask = () -> initiateStart(taskScheduler, administrativeYieldMillis, timeoutMilis, processContext, schedulingAgentCallback); + taskScheduler.schedule(initiateStartTask, 5, TimeUnit.SECONDS); + + schedulingAgentCallback.onTaskComplete(); + return null; + } + LOG.debug("Invoking @OnScheduled methods of {}", processor); // Now that the task has been scheduled, set the timeout - completionTimestampRef.set(System.currentTimeMillis() + onScheduleTimeoutMillis); + completionTimestampRef.set(System.currentTimeMillis() + timeoutMilis); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { try { activateThread(); try { @@ -1540,7 +1549,7 @@ private void initiateStart(final ScheduledExecutorService taskScheduler, final l + "initialize and run the Processor again after the 'Administrative Yield Duration' has elapsed. Failure is due to " + e, e); // If processor's task completed Exceptionally, then we want to retry initiating the start (if Processor is still scheduled to run). - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { activateThread(); try { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnUnscheduled.class, processor, processContext); @@ -1554,7 +1563,7 @@ private void initiateStart(final ScheduledExecutorService taskScheduler, final l // make sure we only continue retry loop if STOP action wasn't initiated if (scheduledState.get() != ScheduledState.STOPPING) { // re-initiate the entire process - final Runnable initiateStartTask = () -> initiateStart(taskScheduler, administrativeYieldMillis, processContext, schedulingAgentCallback); + final Runnable initiateStartTask = () -> initiateStart(taskScheduler, administrativeYieldMillis, timeoutMilis, processContext, schedulingAgentCallback); taskScheduler.schedule(initiateStartTask, administrativeYieldMillis, TimeUnit.MILLISECONDS); } else { scheduledState.set(ScheduledState.STOPPED); @@ -1609,7 +1618,7 @@ public void run() { * STOPPING (e.g., the processor didn't finish @OnScheduled operation when * stop was called), the attempt will be made to transition processor's * scheduled state from STARTING to STOPPING which will allow - * {@link #start(ScheduledExecutorService, long, ProcessContext, Runnable)} + * {@link #start(ScheduledExecutorService, long, long, ProcessContext, SchedulingAgentCallback, boolean)} * method to initiate processor's shutdown upon exiting @OnScheduled * operation, otherwise the processor's scheduled state will remain * unchanged ensuring that multiple calls to this method are idempotent. @@ -1637,7 +1646,7 @@ public void run() { schedulingAgent.unschedule(StandardProcessorNode.this, scheduleState); activateThread(); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnUnscheduled.class, processor, processContext); } finally { deactivateThread(); @@ -1649,7 +1658,7 @@ public void run() { final boolean allThreadsComplete = scheduleState.getActiveThreadCount() == 1; if (allThreadsComplete) { activateThread(); - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), processor.getClass(), processor.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, processor, processContext); } finally { deactivateThread(); @@ -1701,6 +1710,10 @@ public void run() { return future; } + @Override + public ScheduledState getDesiredState() { + return desiredState; + } private void monitorAsyncTask(final Future taskFuture, final Future monitoringFuture, final long completionTimestamp) { if (taskFuture.isDone()) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java new file mode 100644 index 000000000000..6a579e9f80c8 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java @@ -0,0 +1,209 @@ +/* + * 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.controller; + +import org.apache.nifi.annotation.lifecycle.OnRemoved; +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.controller.exception.ControllerServiceInstantiationException; +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.reporting.ReportingTaskInstantiationException; +import org.apache.nifi.controller.service.ControllerServiceInvocationHandler; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.StandardConfigurationContext; +import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.logging.LogRepositoryFactory; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.SimpleProcessLogger; +import org.apache.nifi.processor.StandardProcessContext; +import org.apache.nifi.reporting.ReportingTask; +import org.apache.nifi.util.ReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.Set; + +public class StandardReloadComponent implements ReloadComponent { + private static final Logger logger = LoggerFactory.getLogger(StandardReloadComponent.class); + + private final FlowController flowController; + + public StandardReloadComponent(final FlowController flowController) { + this.flowController = flowController; + } + + + @Override + public void reload(final ProcessorNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) + throws ProcessorInstantiationException { + if (existingNode == null) { + throw new IllegalStateException("Existing ProcessorNode cannot be null"); + } + + final String id = existingNode.getProcessor().getIdentifier(); + + // ghost components will have a null logger + if (existingNode.getLogger() != null) { + existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); + } + + final ExtensionManager extensionManager = flowController.getExtensionManager(); + + // createProcessor will create a new instance class loader for the same id so + // save the instance class loader to use it for calling OnRemoved on the existing processor + final ClassLoader existingInstanceClassLoader = extensionManager.getInstanceClassLoader(id); + + // create a new node with firstTimeAdded as true so lifecycle methods get fired + // attempt the creation to make sure it works before firing the OnRemoved methods below + final ProcessorNode newNode = flowController.getFlowManager().createProcessor(newType, id, bundleCoordinate, additionalUrls, true, false); + + // call OnRemoved for the existing processor using the previous instance class loader + try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { + final StateManager stateManager = flowController.getStateManagerProvider().getStateManager(id); + final StandardProcessContext processContext = new StandardProcessContext(existingNode, flowController.getControllerServiceProvider(), + flowController.getEncryptor(), stateManager, () -> false); + + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getProcessor(), processContext); + } finally { + extensionManager.closeURLClassLoader(id, existingInstanceClassLoader); + } + + // set the new processor in the existing node + final ComponentLog componentLogger = new SimpleProcessLogger(id, newNode.getProcessor()); + final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); + LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); + + final LoggableComponent newProcessor = new LoggableComponent<>(newNode.getProcessor(), newNode.getBundleCoordinate(), terminationAwareLogger); + existingNode.setProcessor(newProcessor); + existingNode.setExtensionMissing(newNode.isExtensionMissing()); + + // need to refresh the properties in case we are changing from ghost component to real component + existingNode.refreshProperties(); + + logger.debug("Triggering async validation of {} due to processor reload", existingNode); + flowController.getValidationTrigger().trigger(existingNode); + } + + @Override + public void reload(final ControllerServiceNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) + throws ControllerServiceInstantiationException { + if (existingNode == null) { + throw new IllegalStateException("Existing ControllerServiceNode cannot be null"); + } + + final String id = existingNode.getIdentifier(); + + // ghost components will have a null logger + if (existingNode.getLogger() != null) { + existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); + } + + final ExtensionManager extensionManager = flowController.getExtensionManager(); + + // createControllerService will create a new instance class loader for the same id so + // save the instance class loader to use it for calling OnRemoved on the existing service + final ClassLoader existingInstanceClassLoader = extensionManager.getInstanceClassLoader(id); + + // create a new node with firstTimeAdded as true so lifecycle methods get called + // attempt the creation to make sure it works before firing the OnRemoved methods below + final ControllerServiceNode newNode = flowController.getFlowManager().createControllerService(newType, id, bundleCoordinate, additionalUrls, true, false); + + // call OnRemoved for the existing service using the previous instance class loader + try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { + final ConfigurationContext configurationContext = new StandardConfigurationContext(existingNode, flowController.getControllerServiceProvider(), + null, flowController.getVariableRegistry()); + + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getControllerServiceImplementation(), configurationContext); + } finally { + extensionManager.closeURLClassLoader(id, existingInstanceClassLoader); + } + + // take the invocation handler that was created for new proxy and is set to look at the new node, + // and set it to look at the existing node + final ControllerServiceInvocationHandler invocationHandler = newNode.getInvocationHandler(); + invocationHandler.setServiceNode(existingNode); + + // create LoggableComponents for the proxy and implementation + final ComponentLog componentLogger = new SimpleProcessLogger(id, newNode.getControllerServiceImplementation()); + final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); + LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); + + final LoggableComponent loggableProxy = new LoggableComponent<>(newNode.getProxiedControllerService(), bundleCoordinate, terminationAwareLogger); + final LoggableComponent loggableImplementation = new LoggableComponent<>(newNode.getControllerServiceImplementation(), bundleCoordinate, terminationAwareLogger); + + // set the new impl, proxy, and invocation handler into the existing node + existingNode.setControllerServiceAndProxy(loggableImplementation, loggableProxy, invocationHandler); + existingNode.setExtensionMissing(newNode.isExtensionMissing()); + + // need to refresh the properties in case we are changing from ghost component to real component + existingNode.refreshProperties(); + + logger.debug("Triggering async validation of {} due to controller service reload", existingNode); + flowController.getValidationTrigger().triggerAsync(existingNode); + } + + @Override + public void reload(final ReportingTaskNode existingNode, final String newType, final BundleCoordinate bundleCoordinate, final Set additionalUrls) + throws ReportingTaskInstantiationException { + if (existingNode == null) { + throw new IllegalStateException("Existing ReportingTaskNode cannot be null"); + } + + final String id = existingNode.getReportingTask().getIdentifier(); + + // ghost components will have a null logger + if (existingNode.getLogger() != null) { + existingNode.getLogger().debug("Reloading component {} to type {} from bundle {}", new Object[]{id, newType, bundleCoordinate}); + } + + final ExtensionManager extensionManager = flowController.getExtensionManager(); + + // createReportingTask will create a new instance class loader for the same id so + // save the instance class loader to use it for calling OnRemoved on the existing processor + final ClassLoader existingInstanceClassLoader = extensionManager.getInstanceClassLoader(id); + + // set firstTimeAdded to true so lifecycle annotations get fired, but don't register this node + // attempt the creation to make sure it works before firing the OnRemoved methods below + final ReportingTaskNode newNode = flowController.getFlowManager().createReportingTask(newType, id, bundleCoordinate, additionalUrls, true, false); + + // call OnRemoved for the existing reporting task using the previous instance class loader + try (final NarCloseable x = NarCloseable.withComponentNarLoader(existingInstanceClassLoader)) { + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, existingNode.getReportingTask(), existingNode.getConfigurationContext()); + } finally { + extensionManager.closeURLClassLoader(id, existingInstanceClassLoader); + } + + // set the new reporting task into the existing node + final ComponentLog componentLogger = new SimpleProcessLogger(id, existingNode.getReportingTask()); + final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(componentLogger); + LogRepositoryFactory.getRepository(id).setLogger(terminationAwareLogger); + + final LoggableComponent newReportingTask = new LoggableComponent<>(newNode.getReportingTask(), newNode.getBundleCoordinate(), terminationAwareLogger); + existingNode.setReportingTask(newReportingTask); + existingNode.setExtensionMissing(newNode.isExtensionMissing()); + + // need to refresh the properties in case we are changing from ghost component to real component + existingNode.refreshProperties(); + + logger.debug("Triggering async validation of {} due to reporting task reload", existingNode); + flowController.getValidationTrigger().triggerAsync(existingNode); + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java new file mode 100644 index 000000000000..f100092ddd5f --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java @@ -0,0 +1,656 @@ +/* + * 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.controller.flow; + +import org.apache.nifi.annotation.lifecycle.OnAdded; +import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored; +import org.apache.nifi.annotation.lifecycle.OnRemoved; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.bundle.Bundle; +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.ConnectableType; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.connectable.Funnel; +import org.apache.nifi.connectable.LocalPort; +import org.apache.nifi.connectable.Port; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.controller.ExtensionBuilder; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.FlowSnippet; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.StandardFlowSnippet; +import org.apache.nifi.controller.StandardFunnel; +import org.apache.nifi.controller.StandardProcessorNode; +import org.apache.nifi.controller.exception.ComponentLifeCycleException; +import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.label.Label; +import org.apache.nifi.controller.label.StandardLabel; +import org.apache.nifi.controller.repository.FlowFileEventRepository; +import org.apache.nifi.controller.scheduling.StandardProcessScheduler; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceProvider; +import org.apache.nifi.controller.service.StandardConfigurationContext; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.groups.StandardProcessGroup; +import org.apache.nifi.logging.ControllerServiceLogObserver; +import org.apache.nifi.logging.LogLevel; +import org.apache.nifi.logging.LogRepository; +import org.apache.nifi.logging.LogRepositoryFactory; +import org.apache.nifi.logging.ProcessorLogObserver; +import org.apache.nifi.logging.ReportingTaskLogObserver; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.registry.VariableRegistry; +import org.apache.nifi.registry.variable.MutableVariableRegistry; +import org.apache.nifi.remote.RemoteGroupPort; +import org.apache.nifi.remote.StandardRemoteProcessGroup; +import org.apache.nifi.remote.StandardRootGroupPort; +import org.apache.nifi.remote.TransferDirection; +import org.apache.nifi.reporting.BulletinRepository; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.ReflectionUtils; +import org.apache.nifi.web.api.dto.FlowSnippetDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static java.util.Objects.requireNonNull; + +public class StandardFlowManager implements FlowManager { + private static final Logger logger = LoggerFactory.getLogger(StandardFlowManager.class); + + private final NiFiProperties nifiProperties; + private final BulletinRepository bulletinRepository; + private final StandardProcessScheduler processScheduler; + private final Authorizer authorizer; + private final SSLContext sslContext; + private final FlowController flowController; + private final FlowFileEventRepository flowFileEventRepository; + + private final boolean isSiteToSiteSecure; + + private volatile ProcessGroup rootGroup; + private final ConcurrentMap allProcessGroups = new ConcurrentHashMap<>(); + private final ConcurrentMap allProcessors = new ConcurrentHashMap<>(); + private final ConcurrentMap allReportingTasks = new ConcurrentHashMap<>(); + private final ConcurrentMap rootControllerServices = new ConcurrentHashMap<>(); + private final ConcurrentMap allConnections = new ConcurrentHashMap<>(); + private final ConcurrentMap allInputPorts = new ConcurrentHashMap<>(); + private final ConcurrentMap allOutputPorts = new ConcurrentHashMap<>(); + private final ConcurrentMap allFunnels = new ConcurrentHashMap<>(); + + public StandardFlowManager(final NiFiProperties nifiProperties, final SSLContext sslContext, final FlowController flowController, final FlowFileEventRepository flowFileEventRepository) { + this.nifiProperties = nifiProperties; + this.flowController = flowController; + this.bulletinRepository = flowController.getBulletinRepository(); + this.processScheduler = flowController.getProcessScheduler(); + this.authorizer = flowController.getAuthorizer(); + this.sslContext = sslContext; + this.flowFileEventRepository = flowFileEventRepository; + + this.isSiteToSiteSecure = Boolean.TRUE.equals(nifiProperties.isSiteToSiteSecure()); + } + + public Port createRemoteInputPort(String id, String name) { + id = requireNonNull(id).intern(); + name = requireNonNull(name).intern(); + verifyPortIdDoesNotExist(id); + return new StandardRootGroupPort(id, name, null, TransferDirection.RECEIVE, ConnectableType.INPUT_PORT, + authorizer, bulletinRepository, processScheduler, isSiteToSiteSecure, nifiProperties); + } + + public Port createRemoteOutputPort(String id, String name) { + id = requireNonNull(id).intern(); + name = requireNonNull(name).intern(); + verifyPortIdDoesNotExist(id); + return new StandardRootGroupPort(id, name, null, TransferDirection.SEND, ConnectableType.OUTPUT_PORT, + authorizer, bulletinRepository, processScheduler, isSiteToSiteSecure, nifiProperties); + } + + public RemoteProcessGroup createRemoteProcessGroup(final String id, final String uris) { + return new StandardRemoteProcessGroup(requireNonNull(id), uris, null, processScheduler, bulletinRepository, sslContext, nifiProperties); + } + + public void setRootGroup(final ProcessGroup rootGroup) { + this.rootGroup = rootGroup; + allProcessGroups.put(ROOT_GROUP_ID_ALIAS, rootGroup); + allProcessGroups.put(rootGroup.getIdentifier(), rootGroup); + } + + public ProcessGroup getRootGroup() { + return rootGroup; + } + + @Override + public String getRootGroupId() { + return rootGroup.getIdentifier(); + } + + public boolean areGroupsSame(final String id1, final String id2) { + if (id1 == null || id2 == null) { + return false; + } else if (id1.equals(id2)) { + return true; + } else { + final String comparable1 = id1.equals(ROOT_GROUP_ID_ALIAS) ? getRootGroupId() : id1; + final String comparable2 = id2.equals(ROOT_GROUP_ID_ALIAS) ? getRootGroupId() : id2; + return comparable1.equals(comparable2); + } + } + + private void verifyPortIdDoesNotExist(final String id) { + final ProcessGroup rootGroup = getRootGroup(); + Port port = rootGroup.findOutputPort(id); + if (port != null) { + throw new IllegalStateException("An Input Port already exists with ID " + id); + } + port = rootGroup.findInputPort(id); + if (port != null) { + throw new IllegalStateException("An Input Port already exists with ID " + id); + } + } + + public Label createLabel(final String id, final String text) { + return new StandardLabel(requireNonNull(id).intern(), text); + } + + public Funnel createFunnel(final String id) { + return new StandardFunnel(id.intern(), null, processScheduler); + } + + public Port createLocalInputPort(String id, String name) { + id = requireNonNull(id).intern(); + name = requireNonNull(name).intern(); + verifyPortIdDoesNotExist(id); + return new LocalPort(id, name, null, ConnectableType.INPUT_PORT, processScheduler); + } + + public Port createLocalOutputPort(String id, String name) { + id = requireNonNull(id).intern(); + name = requireNonNull(name).intern(); + verifyPortIdDoesNotExist(id); + return new LocalPort(id, name, null, ConnectableType.OUTPUT_PORT, processScheduler); + } + + public ProcessGroup createProcessGroup(final String id) { + final MutableVariableRegistry mutableVariableRegistry = new MutableVariableRegistry(flowController.getVariableRegistry()); + + final ProcessGroup group = new StandardProcessGroup(requireNonNull(id), flowController.getControllerServiceProvider(), processScheduler, nifiProperties, flowController.getEncryptor(), + flowController, mutableVariableRegistry); + allProcessGroups.put(group.getIdentifier(), group); + + return group; + } + + public void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto) throws ProcessorInstantiationException { + requireNonNull(group); + requireNonNull(dto); + + final FlowSnippet snippet = new StandardFlowSnippet(dto, flowController.getExtensionManager()); + snippet.validate(group); + snippet.instantiate(this, group); + + group.findAllRemoteProcessGroups().forEach(RemoteProcessGroup::initialize); + } + + public FlowFilePrioritizer createPrioritizer(final String type) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + FlowFilePrioritizer prioritizer; + + final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); + try { + final List prioritizerBundles = flowController.getExtensionManager().getBundles(type); + if (prioritizerBundles.size() == 0) { + throw new IllegalStateException(String.format("The specified class '%s' is not known to this nifi.", type)); + } + if (prioritizerBundles.size() > 1) { + throw new IllegalStateException(String.format("Multiple bundles found for the specified class '%s', only one is allowed.", type)); + } + + final Bundle bundle = prioritizerBundles.get(0); + final ClassLoader detectedClassLoaderForType = bundle.getClassLoader(); + final Class rawClass = Class.forName(type, true, detectedClassLoaderForType); + + Thread.currentThread().setContextClassLoader(detectedClassLoaderForType); + final Class prioritizerClass = rawClass.asSubclass(FlowFilePrioritizer.class); + final Object processorObj = prioritizerClass.newInstance(); + prioritizer = prioritizerClass.cast(processorObj); + + return prioritizer; + } finally { + if (ctxClassLoader != null) { + Thread.currentThread().setContextClassLoader(ctxClassLoader); + } + } + } + + public ProcessGroup getGroup(final String id) { + return allProcessGroups.get(requireNonNull(id)); + } + + public void onProcessGroupAdded(final ProcessGroup group) { + allProcessGroups.put(group.getIdentifier(), group); + } + + public void onProcessGroupRemoved(final ProcessGroup group) { + allProcessGroups.remove(group.getIdentifier()); + } + + public ProcessorNode createProcessor(final String type, final String id, final BundleCoordinate coordinate) { + return createProcessor(type, id, coordinate, true); + } + + public ProcessorNode createProcessor(final String type, String id, final BundleCoordinate coordinate, final boolean firstTimeAdded) { + return createProcessor(type, id, coordinate, Collections.emptySet(), firstTimeAdded, true); + } + + public ProcessorNode createProcessor(final String type, String id, final BundleCoordinate coordinate, final Set additionalUrls, + final boolean firstTimeAdded, final boolean registerLogObserver) { + + // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader + final LogRepository logRepository = LogRepositoryFactory.getRepository(id); + final ExtensionManager extensionManager = flowController.getExtensionManager(); + + final ProcessorNode procNode = new ExtensionBuilder() + .identifier(id) + .type(type) + .bundleCoordinate(coordinate) + .extensionManager(extensionManager) + .controllerServiceProvider(flowController.getControllerServiceProvider()) + .processScheduler(processScheduler) + .nodeTypeProvider(flowController) + .validationTrigger(flowController.getValidationTrigger()) + .reloadComponent(flowController.getReloadComponent()) + .variableRegistry(flowController.getVariableRegistry()) + .addClasspathUrls(additionalUrls) + .kerberosConfig(flowController.createKerberosConfig(nifiProperties)) + .extensionManager(extensionManager) + .buildProcessor(); + + LogRepositoryFactory.getRepository(procNode.getIdentifier()).setLogger(procNode.getLogger()); + if (registerLogObserver) { + logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, procNode.getBulletinLevel(), new ProcessorLogObserver(bulletinRepository, procNode)); + } + + if (firstTimeAdded) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, procNode.getProcessor().getClass(), procNode.getProcessor().getIdentifier())) { + ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, procNode.getProcessor()); + } catch (final Exception e) { + if (registerLogObserver) { + logRepository.removeObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID); + } + throw new ComponentLifeCycleException("Failed to invoke @OnAdded methods of " + procNode.getProcessor(), e); + } + + if (firstTimeAdded) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, procNode.getProcessor().getClass(), procNode.getProcessor().getIdentifier())) { + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, procNode.getProcessor()); + } + } + } + + return procNode; + } + + public void onProcessorAdded(final ProcessorNode procNode) { + allProcessors.put(procNode.getIdentifier(), procNode); + } + + public void onProcessorRemoved(final ProcessorNode procNode) { + String identifier = procNode.getIdentifier(); + flowFileEventRepository.purgeTransferEvents(identifier); + allProcessors.remove(identifier); + } + + public Connectable findConnectable(final String id) { + final ProcessorNode procNode = getProcessorNode(id); + if (procNode != null) { + return procNode; + } + + final Port inPort = getInputPort(id); + if (inPort != null) { + return inPort; + } + + final Port outPort = getOutputPort(id); + if (outPort != null) { + return outPort; + } + + final Funnel funnel = getFunnel(id); + if (funnel != null) { + return funnel; + } + + final RemoteGroupPort remoteGroupPort = getRootGroup().findRemoteGroupPort(id); + if (remoteGroupPort != null) { + return remoteGroupPort; + } + + return null; + } + + public ProcessorNode getProcessorNode(final String id) { + return allProcessors.get(id); + } + + public void onConnectionAdded(final Connection connection) { + allConnections.put(connection.getIdentifier(), connection); + + if (flowController.isInitialized()) { + connection.getFlowFileQueue().startLoadBalancing(); + } + } + + public void onConnectionRemoved(final Connection connection) { + String identifier = connection.getIdentifier(); + flowFileEventRepository.purgeTransferEvents(identifier); + allConnections.remove(identifier); + } + + public Connection getConnection(final String id) { + return allConnections.get(id); + } + + public Connection createConnection(final String id, final String name, final Connectable source, final Connectable destination, final Collection relationshipNames) { + return flowController.createConnection(id, name, source, destination, relationshipNames); + } + + public Set findAllConnections() { + return new HashSet<>(allConnections.values()); + } + + public void onInputPortAdded(final Port inputPort) { + allInputPorts.put(inputPort.getIdentifier(), inputPort); + } + + public void onInputPortRemoved(final Port inputPort) { + String identifier = inputPort.getIdentifier(); + flowFileEventRepository.purgeTransferEvents(identifier); + allInputPorts.remove(identifier); + } + + public Port getInputPort(final String id) { + return allInputPorts.get(id); + } + + public void onOutputPortAdded(final Port outputPort) { + allOutputPorts.put(outputPort.getIdentifier(), outputPort); + } + + public void onOutputPortRemoved(final Port outputPort) { + String identifier = outputPort.getIdentifier(); + flowFileEventRepository.purgeTransferEvents(identifier); + allOutputPorts.remove(identifier); + } + + public Port getOutputPort(final String id) { + return allOutputPorts.get(id); + } + + public void onFunnelAdded(final Funnel funnel) { + allFunnels.put(funnel.getIdentifier(), funnel); + } + + public void onFunnelRemoved(final Funnel funnel) { + String identifier = funnel.getIdentifier(); + flowFileEventRepository.purgeTransferEvents(identifier); + allFunnels.remove(identifier); + } + + public Funnel getFunnel(final String id) { + return allFunnels.get(id); + } + + public ReportingTaskNode createReportingTask(final String type, final BundleCoordinate bundleCoordinate) { + return createReportingTask(type, bundleCoordinate, true); + } + + public ReportingTaskNode createReportingTask(final String type, final BundleCoordinate bundleCoordinate, final boolean firstTimeAdded) { + return createReportingTask(type, UUID.randomUUID().toString(), bundleCoordinate, firstTimeAdded); + } + + @Override + public ReportingTaskNode createReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate, final boolean firstTimeAdded) { + return createReportingTask(type, id, bundleCoordinate, Collections.emptySet(), firstTimeAdded, true); + } + + public ReportingTaskNode createReportingTask(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls, + final boolean firstTimeAdded, final boolean register) { + if (type == null || id == null || bundleCoordinate == null) { + throw new NullPointerException(); + } + + // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader + final LogRepository logRepository = LogRepositoryFactory.getRepository(id); + final ExtensionManager extensionManager = flowController.getExtensionManager(); + + final ReportingTaskNode taskNode = new ExtensionBuilder() + .identifier(id) + .type(type) + .bundleCoordinate(bundleCoordinate) + .extensionManager(flowController.getExtensionManager()) + .controllerServiceProvider(flowController.getControllerServiceProvider()) + .processScheduler(processScheduler) + .nodeTypeProvider(flowController) + .validationTrigger(flowController.getValidationTrigger()) + .reloadComponent(flowController.getReloadComponent()) + .variableRegistry(flowController.getVariableRegistry()) + .addClasspathUrls(additionalUrls) + .kerberosConfig(flowController.createKerberosConfig(nifiProperties)) + .flowController(flowController) + .extensionManager(extensionManager) + .buildReportingTask(); + + LogRepositoryFactory.getRepository(taskNode.getIdentifier()).setLogger(taskNode.getLogger()); + + if (firstTimeAdded) { + final Class taskClass = taskNode.getReportingTask().getClass(); + final String identifier = taskNode.getReportingTask().getIdentifier(); + + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), taskClass, identifier)) { + ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, taskNode.getReportingTask()); + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, taskNode.getReportingTask()); + } catch (final Exception e) { + throw new ComponentLifeCycleException("Failed to invoke On-Added Lifecycle methods of " + taskNode.getReportingTask(), e); + } + } + + if (register) { + allReportingTasks.put(id, taskNode); + + // Register log observer to provide bulletins when reporting task logs anything at WARN level or above + logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, LogLevel.WARN, + new ReportingTaskLogObserver(bulletinRepository, taskNode)); + } + + return taskNode; + } + + public ReportingTaskNode getReportingTaskNode(final String taskId) { + return allReportingTasks.get(taskId); + } + + @Override + public void removeReportingTask(final ReportingTaskNode reportingTaskNode) { + final ReportingTaskNode existing = allReportingTasks.get(reportingTaskNode.getIdentifier()); + if (existing == null || existing != reportingTaskNode) { + throw new IllegalStateException("Reporting Task " + reportingTaskNode + " does not exist in this Flow"); + } + + reportingTaskNode.verifyCanDelete(); + + final Class taskClass = reportingTaskNode.getReportingTask().getClass(); + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), taskClass, reportingTaskNode.getReportingTask().getIdentifier())) { + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, reportingTaskNode.getReportingTask(), reportingTaskNode.getConfigurationContext()); + } + + for (final Map.Entry entry : reportingTaskNode.getProperties().entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + if (descriptor.getControllerServiceDefinition() != null) { + final String value = entry.getValue() == null ? descriptor.getDefaultValue() : entry.getValue(); + if (value != null) { + final ControllerServiceNode serviceNode = flowController.getControllerServiceProvider().getControllerServiceNode(value); + if (serviceNode != null) { + serviceNode.removeReference(reportingTaskNode); + } + } + } + } + + allReportingTasks.remove(reportingTaskNode.getIdentifier()); + LogRepositoryFactory.removeRepository(reportingTaskNode.getIdentifier()); + processScheduler.onReportingTaskRemoved(reportingTaskNode); + + flowController.getExtensionManager().removeInstanceClassLoader(reportingTaskNode.getIdentifier()); + } + + @Override + public Set getAllReportingTasks() { + return new HashSet<>(allReportingTasks.values()); + } + + public Set getRootControllerServices() { + return new HashSet<>(rootControllerServices.values()); + } + + public void addRootControllerService(final ControllerServiceNode serviceNode) { + final ControllerServiceNode existing = rootControllerServices.putIfAbsent(serviceNode.getIdentifier(), serviceNode); + if (existing != null) { + throw new IllegalStateException("Controller Service with ID " + serviceNode.getIdentifier() + " already exists at the Controller level"); + } + } + + public ControllerServiceNode getRootControllerService(final String serviceIdentifier) { + return rootControllerServices.get(serviceIdentifier); + } + + public void removeRootControllerService(final ControllerServiceNode service) { + final ControllerServiceNode existing = rootControllerServices.get(requireNonNull(service).getIdentifier()); + if (existing == null) { + throw new IllegalStateException(service + " is not a member of this Process Group"); + } + + service.verifyCanDelete(); + + final ExtensionManager extensionManager = flowController.getExtensionManager(); + final VariableRegistry variableRegistry = flowController.getVariableRegistry(); + + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, service.getControllerServiceImplementation().getClass(), service.getIdentifier())) { + final ConfigurationContext configurationContext = new StandardConfigurationContext(service, flowController.getControllerServiceProvider(), null, variableRegistry); + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, service.getControllerServiceImplementation(), configurationContext); + } + + for (final Map.Entry entry : service.getProperties().entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + if (descriptor.getControllerServiceDefinition() != null) { + final String value = entry.getValue() == null ? descriptor.getDefaultValue() : entry.getValue(); + if (value != null) { + final ControllerServiceNode referencedNode = getRootControllerService(value); + if (referencedNode != null) { + referencedNode.removeReference(service); + } + } + } + } + + rootControllerServices.remove(service.getIdentifier()); + flowController.getStateManagerProvider().onComponentRemoved(service.getIdentifier()); + + extensionManager.removeInstanceClassLoader(service.getIdentifier()); + + logger.info("{} removed from Flow Controller", service, this); + } + + public ControllerServiceNode createControllerService(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls, final boolean firstTimeAdded, + final boolean registerLogObserver) { + // make sure the first reference to LogRepository happens outside of a NarCloseable so that we use the framework's ClassLoader + final LogRepository logRepository = LogRepositoryFactory.getRepository(id); + final ExtensionManager extensionManager = flowController.getExtensionManager(); + final ControllerServiceProvider controllerServiceProvider = flowController.getControllerServiceProvider(); + + final ControllerServiceNode serviceNode = new ExtensionBuilder() + .identifier(id) + .type(type) + .bundleCoordinate(bundleCoordinate) + .controllerServiceProvider(flowController.getControllerServiceProvider()) + .processScheduler(processScheduler) + .nodeTypeProvider(flowController) + .validationTrigger(flowController.getValidationTrigger()) + .reloadComponent(flowController.getReloadComponent()) + .variableRegistry(flowController.getVariableRegistry()) + .addClasspathUrls(additionalUrls) + .kerberosConfig(flowController.createKerberosConfig(nifiProperties)) + .stateManagerProvider(flowController.getStateManagerProvider()) + .extensionManager(extensionManager) + .buildControllerService(); + + LogRepositoryFactory.getRepository(serviceNode.getIdentifier()).setLogger(serviceNode.getLogger()); + if (registerLogObserver) { + // Register log observer to provide bulletins when reporting task logs anything at WARN level or above + logRepository.addObserver(StandardProcessorNode.BULLETIN_OBSERVER_ID, LogLevel.WARN, new ControllerServiceLogObserver(bulletinRepository, serviceNode)); + } + + if (firstTimeAdded) { + final ControllerService service = serviceNode.getControllerServiceImplementation(); + + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(extensionManager, service.getClass(), service.getIdentifier())) { + ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnConfigurationRestored.class, service); + } + + final ControllerService serviceImpl = serviceNode.getControllerServiceImplementation(); + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, serviceImpl.getClass(), serviceImpl.getIdentifier())) { + ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, serviceImpl); + } catch (final Exception e) { + throw new ComponentLifeCycleException("Failed to invoke On-Added Lifecycle methods of " + serviceImpl, e); + } + } + + controllerServiceProvider.onControllerServiceAdded(serviceNode); + + return serviceNode; + } + + public Set getAllControllerServices() { + final Set allServiceNodes = new HashSet<>(); + allServiceNodes.addAll(flowController.getControllerServiceProvider().getNonRootControllerServices()); + allServiceNodes.addAll(rootControllerServices.values()); + return allServiceNodes; + } + + public ControllerServiceNode getControllerServiceNode(final String id) { + return flowController.getControllerServiceProvider().getControllerServiceNode(id); + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/kerberos/KerberosConfig.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/kerberos/KerberosConfig.java new file mode 100644 index 000000000000..8a6f93929769 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/kerberos/KerberosConfig.java @@ -0,0 +1,45 @@ +/* + * 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.controller.kerberos; + +import java.io.File; + +public class KerberosConfig { + private final String principal; + private final File keytabLocation; + private final File configFile; + + public KerberosConfig(final String principal, final File keytabLocation, final File kerberosConfigurationFile) { + this.principal = principal; + this.keytabLocation = keytabLocation; + this.configFile = kerberosConfigurationFile; + } + + public String getPrincipal() { + return principal; + } + + public File getKeytabLocation() { + return keytabLocation; + } + + public File getConfigFile() { + return configFile; + } + + public static final KerberosConfig NOT_CONFIGURED = new KerberosConfig(null, null, null); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/AbstractFlowFileQueue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/AbstractFlowFileQueue.java index 5bf75a4cc881..b986791280df 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/AbstractFlowFileQueue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/AbstractFlowFileQueue.java @@ -141,6 +141,10 @@ private MaxQueueSize getMaxQueueSize() { @Override public boolean isFull() { + return isFull(size()); + } + + protected boolean isFull(final QueueSize queueSize) { final MaxQueueSize maxSize = getMaxQueueSize(); // Check if max size is set @@ -148,7 +152,6 @@ public boolean isFull() { return false; } - final QueueSize queueSize = size(); if (maxSize.getMaxCount() > 0 && queueSize.getObjectCount() >= maxSize.getMaxCount()) { return true; } @@ -187,7 +190,6 @@ public ListFlowFileStatus listFlowFiles(final String requestIdentifier, final in @Override public void run() { int position = 0; - int resultCount = 0; final List summaries = new ArrayList<>(); // Create an ArrayList that contains all of the contents of the active queue. @@ -213,7 +215,7 @@ public void run() { } } - logger.debug("{} Finished listing FlowFiles for active queue with a total of {} results", this, resultCount); + logger.debug("{} Finished listing FlowFiles for active queue with a total of {} results out of {} FlowFiles", this, summaries.size(), allFlowFiles.size()); listRequest.setFlowFileSummaries(summaries); listRequest.setState(ListFlowFileState.COMPLETE); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/StandardFlowFileQueue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/StandardFlowFileQueue.java index cab41e8a8eb6..8872ba7e6119 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/StandardFlowFileQueue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/StandardFlowFileQueue.java @@ -74,6 +74,14 @@ public void startLoadBalancing() { public void stopLoadBalancing() { } + @Override + public void offloadQueue() { + } + + @Override + public void resetOffloadedQueue() { + } + @Override public boolean isActivelyLoadBalancing() { return false; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/SwappablePriorityQueue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/SwappablePriorityQueue.java index 66b594db9ff9..058c7149e55d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/SwappablePriorityQueue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/SwappablePriorityQueue.java @@ -103,7 +103,7 @@ private String getQueueIdentifier() { return flowFileQueue.getIdentifier(); } - public synchronized List getPriorities() { + public List getPriorities() { readLock.lock(); try { return Collections.unmodifiableList(priorities); @@ -878,7 +878,7 @@ protected void incrementActiveQueueSize(final int count, final long bytes) { original.getSwappedCount(), original.getSwappedBytes(), original.getSwapFileCount(), original.getUnacknowledgedCount(), original.getUnacknowledgedBytes()); - updated = size.compareAndSet(original, newSize); + updated = updateSize(original, newSize); if (updated) { logIfNegative(original, newSize, "active"); @@ -908,7 +908,8 @@ private void incrementUnacknowledgedQueueSize(final int count, final long bytes) final FlowFileQueueSize newSize = new FlowFileQueueSize(original.getActiveCount(), original.getActiveBytes(), original.getSwappedCount(), original.getSwappedBytes(), original.getSwapFileCount(), original.getUnacknowledgedCount() + count, original.getUnacknowledgedBytes() + bytes); - updated = size.compareAndSet(original, newSize); + + updated = updateSize(original, newSize); if (updated) { logIfNegative(original, newSize, "Unacknowledged"); @@ -949,7 +950,6 @@ public FlowFileQueueContents packageForRebalance(final String newPartitionName) writeLock.lock(); try { final List activeRecords = new ArrayList<>(this.activeQueue); - activeRecords.addAll(this.swapQueue); final List updatedSwapLocations = new ArrayList<>(swapLocations.size()); for (final String swapLocation : swapLocations) { @@ -963,23 +963,27 @@ public FlowFileQueueContents packageForRebalance(final String newPartitionName) this.swapLocations.clear(); this.activeQueue.clear(); - this.swapQueue.clear(); + + final int swapQueueCount = swapQueue.size(); + final long swapQueueBytes = swapQueue.stream().mapToLong(FlowFileRecord::getSize).sum(); + activeRecords.addAll(swapQueue); + swapQueue.clear(); this.swapMode = false; - QueueSize swapSize = new QueueSize(0, 0L); - boolean updated = false; - while (!updated) { + QueueSize swapSize; + boolean updated; + do { final FlowFileQueueSize currentSize = getFlowFileQueueSize(); - swapSize = new QueueSize(currentSize.getSwappedCount(), currentSize.getSwappedBytes()); + swapSize = new QueueSize(currentSize.getSwappedCount() - swapQueueCount, currentSize.getSwappedBytes() - swapQueueBytes); final FlowFileQueueSize updatedSize = new FlowFileQueueSize(0, 0, 0, 0, 0, currentSize.getUnacknowledgedCount(), currentSize.getUnacknowledgedBytes()); updated = updateSize(currentSize, updatedSize); - } + } while (!updated); return new FlowFileQueueContents(activeRecords, updatedSwapLocations, swapSize); } finally { - writeLock.unlock("transfer(SwappablePriorityQueue)"); + writeLock.unlock("packageForRebalance(SwappablePriorityQueue)"); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/SocketLoadBalancedFlowFileQueue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/SocketLoadBalancedFlowFileQueue.java index f2502000d671..353af49f2c7e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/SocketLoadBalancedFlowFileQueue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/SocketLoadBalancedFlowFileQueue.java @@ -19,6 +19,8 @@ import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.coordination.ClusterTopologyEventListener; +import org.apache.nifi.cluster.coordination.node.NodeConnectionState; +import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.controller.ProcessScheduler; import org.apache.nifi.controller.queue.AbstractFlowFileQueue; @@ -40,6 +42,7 @@ import org.apache.nifi.controller.queue.clustered.partition.FlowFilePartitioner; import org.apache.nifi.controller.queue.clustered.partition.LocalPartitionPartitioner; import org.apache.nifi.controller.queue.clustered.partition.LocalQueuePartition; +import org.apache.nifi.controller.queue.clustered.partition.NonLocalPartitionPartitioner; import org.apache.nifi.controller.queue.clustered.partition.QueuePartition; import org.apache.nifi.controller.queue.clustered.partition.RebalancingPartition; import org.apache.nifi.controller.queue.clustered.partition.RemoteQueuePartition; @@ -113,6 +116,7 @@ public class SocketLoadBalancedFlowFileQueue extends AbstractFlowFileQueue imple private QueuePartition[] queuePartitions; private FlowFilePartitioner partitioner; private boolean stopped = true; + private volatile boolean offloaded = false; public SocketLoadBalancedFlowFileQueue(final String identifier, final ConnectionEventListener eventListener, final ProcessScheduler scheduler, final FlowFileRepository flowFileRepo, @@ -182,8 +186,17 @@ public synchronized void setLoadBalanceStrategy(final LoadBalanceStrategy strate return; } - // We are already load balancing but are changing how we are load balancing. - final FlowFilePartitioner partitioner; + if (!offloaded) { + // We are already load balancing but are changing how we are load balancing. + final FlowFilePartitioner partitioner; + partitioner = getPartitionerForLoadBalancingStrategy(strategy, partitioningAttribute); + + setFlowFilePartitioner(partitioner); + } + } + + private FlowFilePartitioner getPartitionerForLoadBalancingStrategy(LoadBalanceStrategy strategy, String partitioningAttribute) { + FlowFilePartitioner partitioner; switch (strategy) { case DO_NOT_LOAD_BALANCE: partitioner = new LocalPartitionPartitioner(); @@ -200,8 +213,66 @@ public synchronized void setLoadBalanceStrategy(final LoadBalanceStrategy strate default: throw new IllegalArgumentException(); } + return partitioner; + } + + @Override + public void offloadQueue() { + if (clusterCoordinator == null) { + // Not clustered, cannot offload the queue to other nodes + return; + } + + logger.debug("Setting queue {} on node {} as offloaded", this, clusterCoordinator.getLocalNodeIdentifier()); + offloaded = true; + + partitionWriteLock.lock(); + try { + final Set nodesToKeep = new HashSet<>(); + + // If we have any nodes that are connected, we only want to send data to the connected nodes. + for (final QueuePartition partition : queuePartitions) { + final Optional nodeIdOption = partition.getNodeIdentifier(); + if (!nodeIdOption.isPresent()) { + continue; + } + + final NodeIdentifier nodeId = nodeIdOption.get(); + final NodeConnectionStatus status = clusterCoordinator.getConnectionStatus(nodeId); + if (status != null && status.getState() == NodeConnectionState.CONNECTED) { + nodesToKeep.add(nodeId); + } + } + + if (!nodesToKeep.isEmpty()) { + setNodeIdentifiers(nodesToKeep, false); + } - setFlowFilePartitioner(partitioner); + // Update our partitioner so that we don't keep any data on the local partition + setFlowFilePartitioner(new NonLocalPartitionPartitioner()); + } finally { + partitionWriteLock.unlock(); + } + } + + @Override + public void resetOffloadedQueue() { + if (clusterCoordinator == null) { + // Not clustered, was not offloading the queue to other nodes + return; + } + + if (offloaded) { + // queue was offloaded previously, allow files to be added to the local partition + offloaded = false; + logger.debug("Queue {} on node {} was previously offloaded, resetting offloaded status to {}", + this, clusterCoordinator.getLocalNodeIdentifier(), offloaded); + // reset the partitioner based on the load balancing strategy, since offloading previously changed the partitioner + FlowFilePartitioner partitioner = getPartitionerForLoadBalancingStrategy(getLoadBalanceStrategy(), getPartitioningAttribute()); + setFlowFilePartitioner(partitioner); + logger.debug("Queue {} is no longer offloaded, restored load balance strategy to {} and partitioning attribute to \"{}\"", + this, getLoadBalanceStrategy(), getPartitioningAttribute()); + } } public synchronized void startLoadBalancing() { @@ -530,6 +601,11 @@ public void onAbort(final Collection flowFiles) { adjustSize(-flowFiles.size(), -flowFiles.stream().mapToLong(FlowFileRecord::getSize).sum()); } + @Override + public boolean isLocalPartitionFull() { + return isFull(localPartition.size()); + } + /** * Determines which QueuePartition the given FlowFile belongs to. Must be called with partition read lock held. * @@ -571,9 +647,9 @@ public void setNodeIdentifiers(final Set updatedNodeIdentifiers, // Re-define 'queuePartitions' array final List sortedNodeIdentifiers = new ArrayList<>(updatedNodeIdentifiers); - sortedNodeIdentifiers.sort(Comparator.comparing(NodeIdentifier::getApiAddress)); + sortedNodeIdentifiers.sort(Comparator.comparing(nodeId -> nodeId.getApiAddress() + ":" + nodeId.getApiPort())); - final QueuePartition[] updatedQueuePartitions; + QueuePartition[] updatedQueuePartitions; if (sortedNodeIdentifiers.isEmpty()) { updatedQueuePartitions = new QueuePartition[] { localPartition }; } else { @@ -581,10 +657,12 @@ public void setNodeIdentifiers(final Set updatedNodeIdentifiers, } // Populate the new QueuePartitions. + boolean localPartitionIncluded = false; for (int i = 0; i < sortedNodeIdentifiers.size(); i++) { final NodeIdentifier nodeId = sortedNodeIdentifiers.get(i); if (nodeId.equals(clusterCoordinator.getLocalNodeIdentifier())) { updatedQueuePartitions[i] = localPartition; + localPartitionIncluded = true; // If we have RemoteQueuePartition with this Node ID with data, that data must be migrated to the local partition. // This can happen if we didn't previously know our Node UUID. @@ -602,6 +680,13 @@ public void setNodeIdentifiers(final Set updatedNodeIdentifiers, updatedQueuePartitions[i] = existingPartition == null ? createRemotePartition(nodeId) : existingPartition; } + if (!localPartitionIncluded) { + final QueuePartition[] withLocal = new QueuePartition[updatedQueuePartitions.length + 1]; + System.arraycopy(updatedQueuePartitions, 0, withLocal, 0, updatedQueuePartitions.length); + withLocal[withLocal.length - 1] = localPartition; + updatedQueuePartitions = withLocal; + } + // If the partition requires that all partitions be re-balanced when the number of partitions changes, then do so. // Otherwise, just rebalance the data from any Partitions that were removed, if any. if (partitioner.isRebalanceOnClusterResize()) { @@ -649,6 +734,7 @@ public void setNodeIdentifiers(final Set updatedNodeIdentifiers, } protected void rebalance(final QueuePartition partition) { + logger.debug("Rebalancing Partition {}", partition); final FlowFileQueueContents contents = partition.packageForRebalance(rebalancingPartition.getSwapPartitionName()); rebalancingPartition.rebalance(contents); } @@ -683,8 +769,26 @@ public void receiveFromPeer(final Collection flowFiles) { putAll(flowFiles); } else { logger.debug("Received the following FlowFiles from Peer: {}. Will accept FlowFiles to the local partition", flowFiles); - localPartition.putAll(flowFiles); + + // As explained in the putAllAndGetPartitions() method, we must ensure that we call adjustSize() before we + // put the FlowFiles on the queue. Otherwise, we will encounter a race condition. Specifically, that race condition + // can play out like so: + // + // Thread 1: Call localPartition.putAll() when the queue is empty (has a queue size of 0) but has not yet adjusted the size. + // Thread 2: Call poll() to obtain the FlowFile just received. + // Thread 2: Transfer the FlowFile to some Relationship + // Thread 2: Commit the session, which will call acknowledge on this queue. + // Thread 2: The acknowledge() method attempts to decrement the size of the queue to -1. + // This causes an Exception to be thrown and the queue size to remain at 0. + // However, the FlowFile has already been successfully transferred to the next Queue. + // Thread 1: Call adjustSize() to increment the size of the queue to 1 FlowFile. + // + // In this scenario, we now have no FlowFiles in the queue. However, the queue size is set to 1. + // We can avoid this race condition by simply ensuring that we call adjustSize() before making the FlowFiles + // available on the queue. This way, we cannot possibly obtain the FlowFiles and process/acknowledge them before the queue + // size has been updated to account for them and therefore we will not attempt to assign a negative queue size. adjustSize(flowFiles.size(), flowFiles.stream().mapToLong(FlowFileRecord::getSize).sum()); + localPartition.putAll(flowFiles); } } finally { partitionReadLock.unlock(); @@ -830,8 +934,9 @@ public FlowFileRecord getFlowFile(final String flowFileUuid) throws IOException @Override public boolean isPropagateBackpressureAcrossNodes() { - // TODO: We will want to modify this when we have the ability to offload flowfiles from a node. - return true; + // If offloaded = false, the queue is not offloading; return true to honor backpressure + // If offloaded = true, the queue is offloading or has finished offloading; return false to ignore backpressure + return !offloaded; } @Override @@ -973,7 +1078,10 @@ public void onNodeRemoved(final NodeIdentifier nodeId) { partitionWriteLock.lock(); try { final Set updatedNodeIds = new HashSet<>(nodeIdentifiers); - updatedNodeIds.remove(nodeId); + final boolean removed = updatedNodeIds.remove(nodeId); + if (!removed) { + return; + } logger.debug("Node Identifier {} removed from cluster. Node ID's changing from {} to {}", nodeId, nodeIdentifiers, updatedNodeIds); setNodeIdentifiers(updatedNodeIds, false); @@ -990,6 +1098,14 @@ public void onLocalNodeIdentifierSet(final NodeIdentifier localNodeId) { return; } + if (!nodeIdentifiers.contains(localNodeId)) { + final Set updatedNodeIds = new HashSet<>(nodeIdentifiers); + updatedNodeIds.add(localNodeId); + + logger.debug("Local Node Identifier has now been determined to be {}. Adding to set of Node Identifiers for {}", localNodeId, SocketLoadBalancedFlowFileQueue.this); + setNodeIdentifiers(updatedNodeIds, false); + } + logger.debug("Local Node Identifier set to {}; current partitions = {}", localNodeId, queuePartitions); for (final QueuePartition partition : queuePartitions) { @@ -1009,7 +1125,9 @@ public void onLocalNodeIdentifierSet(final NodeIdentifier localNodeId) { logger.debug("{} Local Node Identifier set to {} and found Queue Partition {} with that Node Identifier. Will force update of partitions", SocketLoadBalancedFlowFileQueue.this, localNodeId, partition); - setNodeIdentifiers(SocketLoadBalancedFlowFileQueue.this.nodeIdentifiers, true); + final Set updatedNodeIds = new HashSet<>(nodeIdentifiers); + updatedNodeIds.add(localNodeId); + setNodeIdentifiers(updatedNodeIds, true); return; } } @@ -1019,6 +1137,33 @@ public void onLocalNodeIdentifierSet(final NodeIdentifier localNodeId) { partitionWriteLock.unlock(); } } + + @Override + public void onNodeStateChange(final NodeIdentifier nodeId, final NodeConnectionState newState) { + partitionWriteLock.lock(); + try { + if (!offloaded) { + return; + } + + switch (newState) { + case CONNECTED: + if (nodeId != null && nodeId.equals(clusterCoordinator.getLocalNodeIdentifier())) { + // the node with this queue was connected to the cluster, make sure the queue is not offloaded + resetOffloadedQueue(); + } + break; + case OFFLOADED: + case OFFLOADING: + case DISCONNECTED: + case DISCONNECTING: + onNodeRemoved(nodeId); + break; + } + } finally { + partitionWriteLock.unlock(); + } + } } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/AsyncLoadBalanceClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/AsyncLoadBalanceClient.java index 1bb405321f0b..8673a8b3eb49 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/AsyncLoadBalanceClient.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/AsyncLoadBalanceClient.java @@ -39,6 +39,8 @@ void register(String connectionId, BooleanSupplier emptySupplier, Supplier flowFilesSent); + void onTransactionComplete(List flowFilesSent, NodeIdentifier nodeIdentifier); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java index 066b597f596f..e55dfcd78c58 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClient.java @@ -119,6 +119,10 @@ public synchronized void unregister(final String connectionId) { registeredPartitions.remove(connectionId); } + public synchronized int getRegisteredConnectionCount() { + return registeredPartitions.size(); + } + private synchronized Map getRegisteredPartitions() { return new HashMap<>(registeredPartitions); } @@ -252,7 +256,7 @@ public boolean communicate() throws IOException { } while (success); if (loadBalanceSession.isComplete()) { - loadBalanceSession.getPartition().getSuccessCallback().onTransactionComplete(loadBalanceSession.getFlowFilesSent()); + loadBalanceSession.getPartition().getSuccessCallback().onTransactionComplete(loadBalanceSession.getFlowFilesSent(), nodeIdentifier); } return anySuccess; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientRegistry.java index 514a58c998f3..3322035db491 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientRegistry.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientRegistry.java @@ -67,15 +67,27 @@ public synchronized void register(final String connectionId, final NodeIdentifie @Override public synchronized void unregister(final String connectionId, final NodeIdentifier nodeId) { - final Set clients = clientMap.remove(nodeId); + final Set clients = clientMap.get(nodeId); if (clients == null) { return; } - clients.forEach(client -> client.unregister(connectionId)); + final Set toRemove = new HashSet<>(); + for (final AsyncLoadBalanceClient client : clients) { + client.unregister(connectionId); + if (client.getRegisteredConnectionCount() == 0) { + toRemove.add(client); + } + } + + clients.removeAll(toRemove); + allClients.removeAll(toRemove); + + if (clients.isEmpty()) { + clientMap.remove(nodeId); + } - allClients.removeAll(clients); - logger.debug("Un-registered Connection with ID {} so that it will no longer send data to Node {}", connectionId, nodeId); + logger.debug("Un-registered Connection with ID {} so that it will no longer send data to Node {}; {} clients were removed", connectionId, nodeId, toRemove.size()); } private Set registerClients(final NodeIdentifier nodeId) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientTask.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientTask.java index 35ea5f9840d7..5c8073aa333d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientTask.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/client/async/nio/NioAsyncLoadBalanceClientTask.java @@ -66,13 +66,9 @@ public void run() { } final NodeConnectionState connectionState = connectionStatus.getState(); - if (connectionState == NodeConnectionState.DISCONNECTED || connectionState == NodeConnectionState.DISCONNECTING) { - client.nodeDisconnected(); - continue; - } - if (connectionState != NodeConnectionState.CONNECTED) { - logger.debug("Client {} is for node that is not currently connected (state = {}) so will not communicate with node", client, connectionState); + logger.debug("Notifying Client {} that node is not connected because current state is {}", client, connectionState); + client.nodeDisconnected(); continue; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/NonLocalPartitionPartitioner.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/NonLocalPartitionPartitioner.java new file mode 100644 index 000000000000..0953ce2c4dbd --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/NonLocalPartitionPartitioner.java @@ -0,0 +1,58 @@ +/* + * 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.controller.queue.clustered.partition; + +import org.apache.nifi.controller.repository.FlowFileRecord; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Returns remote partitions when queried for a partition; never returns the {@link LocalQueuePartition}. + */ +public class NonLocalPartitionPartitioner implements FlowFilePartitioner { + private final AtomicLong counter = new AtomicLong(0L); + + @Override + public QueuePartition getPartition(final FlowFileRecord flowFile, final QueuePartition[] partitions, final QueuePartition localPartition) { + QueuePartition remotePartition = null; + final long startIndex = counter.getAndIncrement(); + for (int i = 0, numPartitions = partitions.length; i < numPartitions && remotePartition == null; ++i) { + int index = (int) ((startIndex + i) % numPartitions); + QueuePartition partition = partitions[index]; + if (!partition.equals(localPartition)) { + remotePartition = partition; + } + } + + if (remotePartition == null) { + throw new IllegalStateException("Could not determine a remote partition"); + } + + return remotePartition; + } + + @Override + public boolean isRebalanceOnClusterResize() { + return true; + } + + + @Override + public boolean isRebalanceOnFailure() { + return true; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/RemoteQueuePartition.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/RemoteQueuePartition.java index a78de553df39..854a3a52e672 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/RemoteQueuePartition.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/partition/RemoteQueuePartition.java @@ -173,7 +173,8 @@ public void onTransactionFailed(final List flowFiles, final Exce final StandardRepositoryRecord repoRecord = new StandardRepositoryRecord(flowFileQueue, flowFile); repoRecord.markForAbort(); - updateRepositories(Collections.emptyList(), Collections.singleton(repoRecord)); + // We can send 'null' for the node identifier only because the list of FlowFiles sent is empty. + updateRepositories(Collections.emptyList(), Collections.singleton(repoRecord), null); // If unable to even connect to the node, go ahead and transfer all FlowFiles for this queue to the failure destination. // In either case, transfer those FlowFiles that we failed to send. @@ -204,12 +205,12 @@ public boolean isRebalanceOnFailure() { final TransactionCompleteCallback successCallback = new TransactionCompleteCallback() { @Override - public void onTransactionComplete(final List flowFilesSent) { + public void onTransactionComplete(final List flowFilesSent, final NodeIdentifier nodeIdentifier) { // We've now completed the transaction. We must now update the repositories and "keep the books", acknowledging the FlowFiles // with the queue so that its size remains accurate. priorityQueue.acknowledge(flowFilesSent); flowFileQueue.onTransfer(flowFilesSent); - updateRepositories(flowFilesSent, Collections.emptyList()); + updateRepositories(flowFilesSent, Collections.emptyList(), nodeIdentifier); } }; @@ -230,7 +231,7 @@ public void onRemoved() { * @param flowFilesSent the FlowFiles that were sent to another node. * @param abortedRecords the Repository Records for any FlowFile whose content was missing. */ - private void updateRepositories(final List flowFilesSent, final Collection abortedRecords) { + private void updateRepositories(final List flowFilesSent, final Collection abortedRecords, final NodeIdentifier nodeIdentifier) { // We update the Provenance Repository first. This way, even if we restart before we update the FlowFile repo, we have the record // that the data was sent in the Provenance Repository. We then update the content claims and finally the FlowFile Repository. We do it // in this order so that when the FlowFile repo is sync'ed to disk, we know which Content Claims are no longer in use. Updating the FlowFile @@ -242,7 +243,7 @@ private void updateRepositories(final List flowFilesSent, final // are ever created. final List provenanceEvents = new ArrayList<>(flowFilesSent.size() * 2 + abortedRecords.size()); for (final FlowFileRecord sent : flowFilesSent) { - provenanceEvents.add(createSendEvent(sent)); + provenanceEvents.add(createSendEvent(sent, nodeIdentifier)); provenanceEvents.add(createDropEvent(sent)); } @@ -279,7 +280,7 @@ private RepositoryRecord createRepositoryRecord(final FlowFileRecord flowFile) { return record; } - private ProvenanceEventRecord createSendEvent(final FlowFileRecord flowFile) { + private ProvenanceEventRecord createSendEvent(final FlowFileRecord flowFile, final NodeIdentifier nodeIdentifier) { final ProvenanceEventBuilder builder = new StandardProvenanceEventRecord.Builder() .fromFlowFile(flowFile) @@ -289,7 +290,7 @@ private ProvenanceEventRecord createSendEvent(final FlowFileRecord flowFile) { .setComponentType("Connection") .setSourceQueueIdentifier(flowFileQueue.getIdentifier()) .setSourceSystemFlowFileIdentifier(flowFile.getAttribute(CoreAttributes.UUID.key())) - .setTransitUri("nifi:connection:" + flowFileQueue.getIdentifier()); + .setTransitUri("nifi://" + nodeIdentifier.getApiAddress() + "/loadbalance/" + flowFileQueue.getIdentifier()); final ContentClaim contentClaim = flowFile.getContentClaim(); if (contentClaim != null) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/ClusterLoadBalanceAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/ClusterLoadBalanceAuthorizer.java index 43187b5e1267..fbd849ce6895 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/ClusterLoadBalanceAuthorizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/ClusterLoadBalanceAuthorizer.java @@ -17,14 +17,23 @@ package org.apache.nifi.controller.queue.clustered.server; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.nifi.cluster.coordination.ClusterCoordinator; import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.events.EventReporter; import org.apache.nifi.reporting.Severity; +import org.apache.nifi.security.util.CertificateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Set; import java.util.stream.Collectors; @@ -33,19 +42,27 @@ public class ClusterLoadBalanceAuthorizer implements LoadBalanceAuthorizer { private final ClusterCoordinator clusterCoordinator; private final EventReporter eventReporter; + private final HostnameVerifier hostnameVerifier; public ClusterLoadBalanceAuthorizer(final ClusterCoordinator clusterCoordinator, final EventReporter eventReporter) { this.clusterCoordinator = clusterCoordinator; this.eventReporter = eventReporter; + this.hostnameVerifier = new DefaultHostnameVerifier(); } @Override - public void authorize(final Collection clientIdentities) throws NotAuthorizedException { - if (clientIdentities == null) { - logger.debug("Client Identities is null, so assuming that Load Balancing communications are not secure. Authorizing client to participate in Load Balancing"); - return; + public String authorize(SSLSocket sslSocket) throws NotAuthorizedException, IOException { + final SSLSession sslSession = sslSocket.getSession(); + + final Set clientIdentities; + try { + clientIdentities = getCertificateIdentities(sslSession); + } catch (final CertificateException e) { + throw new IOException("Failed to extract Client Certificate", e); } + logger.debug("Will perform authorization against Client Identities '{}'", clientIdentities); + final Set nodeIds = clusterCoordinator.getNodeIdentifiers().stream() .map(NodeIdentifier::getApiAddress) .collect(Collectors.toSet()); @@ -53,15 +70,39 @@ public void authorize(final Collection clientIdentities) throws NotAutho for (final String clientId : clientIdentities) { if (nodeIds.contains(clientId)) { logger.debug("Client ID '{}' is in the list of Nodes in the Cluster. Authorizing Client to Load Balance data", clientId); - return; + return clientId; + } + } + + // If there are no matches of Client IDs, try to verify it by HostnameVerifier. In this way, we can support wildcard certificates. + for (final String nodeId : nodeIds) { + if (hostnameVerifier.verify(nodeId, sslSession)) { + final String clientId = sslSocket.getInetAddress().getHostName(); + logger.debug("The request was verified with node '{}'. The hostname derived from the socket is '{}'. Authorizing Client to Load Balance data", nodeId, clientId); + return clientId; } } - final String message = String.format("Authorization failed for Client ID's %s to Load Balance data because none of the ID's are known Cluster Node Identifiers", - clientIdentities); + final String message = "Authorization failed for Client ID's to Load Balance data because none of the ID's are known Cluster Node Identifiers"; logger.warn(message); eventReporter.reportEvent(Severity.WARNING, "Load Balanced Connections", message); throw new NotAuthorizedException("Client ID's " + clientIdentities + " are not authorized to Load Balance data"); } + + private Set getCertificateIdentities(final SSLSession sslSession) throws CertificateException, SSLPeerUnverifiedException { + final Certificate[] certs = sslSession.getPeerCertificates(); + if (certs == null || certs.length == 0) { + throw new SSLPeerUnverifiedException("No certificates found"); + } + + final X509Certificate cert = CertificateUtils.convertAbstractX509Certificate(certs[0]); + cert.checkValidity(); + + final Set identities = CertificateUtils.getSubjectAlternativeNames(cert).stream() + .map(CertificateUtils::extractUsername) + .collect(Collectors.toSet()); + + return identities; + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/LoadBalanceAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/LoadBalanceAuthorizer.java index 3a716e203561..e8c200b2c81a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/LoadBalanceAuthorizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/LoadBalanceAuthorizer.java @@ -17,8 +17,17 @@ package org.apache.nifi.controller.queue.clustered.server; -import java.util.Collection; +import javax.net.ssl.SSLSocket; +import java.io.IOException; public interface LoadBalanceAuthorizer { - void authorize(Collection clientIdentities) throws NotAuthorizedException; + /** + * Checks if the given SSLSocket (which includes identities) is allowed to load balance data. If so, the identity that has been + * permitted or hostname derived from the socket is returned. If not, a NotAuthorizedException is thrown. + * + * @param sslSocket the SSLSocket which includes identities to check + * @return the identity that is authorized, or null if the given collection of identities is null + * @throws NotAuthorizedException if none of the given identities is authorized to load balance data + */ + String authorize(SSLSocket sslSocket) throws NotAuthorizedException, IOException; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/StandardLoadBalanceProtocol.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/StandardLoadBalanceProtocol.java index d6beff3531ce..2168f3e77693 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/StandardLoadBalanceProtocol.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/queue/clustered/server/StandardLoadBalanceProtocol.java @@ -39,15 +39,12 @@ import org.apache.nifi.provenance.StandardProvenanceEventRecord; import org.apache.nifi.remote.StandardVersionNegotiator; import org.apache.nifi.remote.VersionNegotiator; -import org.apache.nifi.security.util.CertificateUtils; import org.apache.nifi.stream.io.ByteCountingInputStream; import org.apache.nifi.stream.io.LimitingInputStream; import org.apache.nifi.stream.io.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -60,14 +57,11 @@ import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.zip.CRC32; @@ -122,25 +116,12 @@ public void receiveFlowFiles(final Socket socket) throws IOException { final InputStream in = new BufferedInputStream(socket.getInputStream()); final OutputStream out = new BufferedOutputStream(socket.getOutputStream()); - String peerDescription = socket.getInetAddress().toString(); + String peerDescription = socket.getInetAddress().getHostName(); if (socket instanceof SSLSocket) { - final SSLSession sslSession = ((SSLSocket) socket).getSession(); + logger.debug("Connection received from peer {}", peerDescription); - final Set certIdentities; - try { - certIdentities = getCertificateIdentities(sslSession); - - final String dn = CertificateUtils.extractPeerDNFromSSLSocket(socket); - peerDescription = CertificateUtils.extractUsername(dn); - } catch (final CertificateException e) { - throw new IOException("Failed to extract Client Certificate", e); - } - - logger.debug("Connection received from peer {}. Will perform authorization against Client Identities '{}'", - peerDescription, certIdentities); - - authorizer.authorize(certIdentities); - logger.debug("Client Identities {} are authorized to load balance data", certIdentities); + peerDescription = authorizer.authorize((SSLSocket) socket); + logger.debug("Client Identities are authorized to load balance data for peer {}", peerDescription); } final int version = negotiateProtocolVersion(in, out, peerDescription); @@ -154,23 +135,7 @@ public void receiveFlowFiles(final Socket socket) throws IOException { return; } - receiveFlowFiles(in, out, peerDescription, version, socket.getInetAddress().getHostName()); - } - - private Set getCertificateIdentities(final SSLSession sslSession) throws CertificateException, SSLPeerUnverifiedException { - final Certificate[] certs = sslSession.getPeerCertificates(); - if (certs == null || certs.length == 0) { - throw new SSLPeerUnverifiedException("No certificates found"); - } - - final X509Certificate cert = CertificateUtils.convertAbstractX509Certificate(certs[0]); - cert.checkValidity(); - - final Set identities = CertificateUtils.getSubjectAlternativeNames(cert).stream() - .map(CertificateUtils::extractUsername) - .collect(Collectors.toSet()); - - return identities; + receiveFlowFiles(in, out, peerDescription, version); } @@ -224,7 +189,7 @@ protected int negotiateProtocolVersion(final InputStream in, final OutputStream } - protected void receiveFlowFiles(final InputStream in, final OutputStream out, final String peerDescription, final int protocolVersion, final String nodeName) throws IOException { + protected void receiveFlowFiles(final InputStream in, final OutputStream out, final String peerDescription, final int protocolVersion) throws IOException { logger.debug("Receiving FlowFiles from {}", peerDescription); final long startTimestamp = System.currentTimeMillis(); @@ -238,7 +203,7 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi return; } - final Connection connection = flowController.getConnection(connectionId); + final Connection connection = flowController.getFlowManager().getConnection(connectionId); if (connection == null) { logger.error("Attempted to receive FlowFiles from Peer {} for Connection with ID {} but no connection exists with that ID", peerDescription, connectionId); throw new TransactionAbortedException("Attempted to receive FlowFiles from Peer " + peerDescription + " for Connection with ID " + connectionId + " but no Connection exists with that ID"); @@ -250,13 +215,15 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi "not configured to allow for Load Balancing"); } + final LoadBalancedFlowFileQueue loadBalancedFlowFileQueue = (LoadBalancedFlowFileQueue) flowFileQueue; + final int spaceCheck = dataIn.read(); if (spaceCheck < 0) { throw new EOFException("Expected to receive a request to determine whether or not space was available for Connection with ID " + connectionId + " from Peer " + peerDescription); } if (spaceCheck == CHECK_SPACE) { - if (flowFileQueue.isFull()) { + if (loadBalancedFlowFileQueue.isLocalPartitionFull()) { logger.debug("Received a 'Check Space' request from Peer {} for Connection with ID {}; responding with QUEUE_FULL", peerDescription, connectionId); out.write(QUEUE_FULL); out.flush(); @@ -285,17 +252,14 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi if (contentClaim == null) { contentClaim = contentRepository.create(false); contentClaimOut = contentRepository.write(contentClaim); - } else { - contentRepository.incrementClaimaintCount(contentClaim); } - final RemoteFlowFileRecord flowFile; - try { - flowFile = receiveFlowFile(dataIn, contentClaimOut, contentClaim, claimOffset, protocolVersion, peerDescription, compression); - } catch (final Exception e) { - contentRepository.decrementClaimantCount(contentClaim); - throw e; - } + final RemoteFlowFileRecord flowFile = receiveFlowFile(dataIn, contentClaimOut, contentClaim, claimOffset, protocolVersion, peerDescription, compression); + + // The FlowFile's Content Claim will either be null or equal to the provided Content Claim. + // Incrementing the FlowFile's content claim will increment the count for the provided Content Claim, if it was + // assigned to the FlowFIle, or call incrementClaimantCount with an argument of null, which will do nothing. + contentRepository.incrementClaimaintCount(flowFile.getFlowFile().getContentClaim()); flowFilesReceived.add(flowFile); @@ -307,8 +271,17 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi } } + // When the Content Claim is created initially, it has a Claimaint Count of 1. We then increment the Claimant Count for each FlowFile that we add to the Content Claim, + // which means that the claimant count is currently 1 larger than it needs to be. So we will decrement the claimant count now. If that results in a count of 0, then + // we can go ahead and remove the Content Claim, since we know it's not being referenced. + final int count = contentRepository.decrementClaimantCount(contentClaim); + verifyChecksum(checksum, in, out, peerDescription, flowFilesReceived.size()); - completeTransaction(in, out, peerDescription, flowFilesReceived, nodeName, connectionId, startTimestamp, (LoadBalancedFlowFileQueue) flowFileQueue); + completeTransaction(in, out, peerDescription, flowFilesReceived, connectionId, startTimestamp, (LoadBalancedFlowFileQueue) flowFileQueue); + + if (count == 0) { + contentRepository.remove(contentClaim); + } } catch (final Exception e) { // If any Exception occurs, we need to decrement the claimant counts for the Content Claims that we wrote to because // they are no longer needed. @@ -316,6 +289,8 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi contentRepository.decrementClaimantCount(remoteFlowFile.getFlowFile().getContentClaim()); } + contentRepository.remove(contentClaim); + throw e; } @@ -323,7 +298,7 @@ protected void receiveFlowFiles(final InputStream in, final OutputStream out, fi } private void completeTransaction(final InputStream in, final OutputStream out, final String peerDescription, final List flowFilesReceived, - final String nodeName, final String connectionId, final long startTimestamp, final LoadBalancedFlowFileQueue flowFileQueue) throws IOException { + final String connectionId, final long startTimestamp, final LoadBalancedFlowFileQueue flowFileQueue) throws IOException { final int completionIndicator = in.read(); if (completionIndicator < 0) { throw new EOFException("Expected to receive a Transaction Completion Indicator from Peer " + peerDescription + " but encountered EOF"); @@ -342,7 +317,7 @@ private void completeTransaction(final InputStream in, final OutputStream out, f } logger.debug("Received Complete Transaction indicator from Peer {}", peerDescription); - registerReceiveProvenanceEvents(flowFilesReceived, nodeName, connectionId, startTimestamp); + registerReceiveProvenanceEvents(flowFilesReceived, peerDescription, connectionId, startTimestamp); updateFlowFileRepository(flowFilesReceived, flowFileQueue); transferFlowFilesToQueue(flowFilesReceived, flowFileQueue); @@ -465,6 +440,7 @@ private RemoteFlowFileRecord receiveFlowFile(final DataInputStream dis, final Ou } final Map attributes = readAttributes(metadataIn); + final String sourceSystemUuid = attributes.get(CoreAttributes.UUID.key()); logger.debug("Received Attributes {} from Peer {}", attributes, peerDescription); @@ -476,6 +452,7 @@ private RemoteFlowFileRecord receiveFlowFile(final DataInputStream dis, final Ou final FlowFileRecord flowFileRecord = new StandardFlowFileRecord.Builder() .id(flowFileRepository.getNextFlowFileSequence()) .addAttributes(attributes) + .addAttribute(CoreAttributes.UUID.key(), UUID.randomUUID().toString()) .contentClaim(contentClaimTriple.getContentClaim()) .contentClaimOffset(contentClaimTriple.getClaimOffset()) .size(contentClaimTriple.getContentLength()) @@ -484,7 +461,7 @@ private RemoteFlowFileRecord receiveFlowFile(final DataInputStream dis, final Ou .build(); logger.debug("Received FlowFile {} with {} attributes and {} bytes of content", flowFileRecord, attributes.size(), contentClaimTriple.getContentLength()); - return new RemoteFlowFileRecord(attributes.get(CoreAttributes.UUID.key()), flowFileRecord); + return new RemoteFlowFileRecord(sourceSystemUuid, flowFileRecord); } private Map readAttributes(final DataInputStream in) throws IOException { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/AbstractReportingTaskNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/AbstractReportingTaskNode.java index 9651add04dca..f1b585e5f41d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/AbstractReportingTaskNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/AbstractReportingTaskNode.java @@ -16,16 +16,11 @@ */ package org.apache.nifi.controller.reporting; -import java.net.URL; -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - import org.apache.nifi.annotation.configuration.DefaultSchedule; import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.components.ConfigurableComponent; import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.components.validation.ValidationTrigger; import org.apache.nifi.controller.AbstractComponentNode; import org.apache.nifi.controller.ConfigurationContext; @@ -40,6 +35,7 @@ import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.StandardConfigurationContext; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.registry.ComponentVariableRegistry; import org.apache.nifi.reporting.ReportingTask; import org.apache.nifi.scheduling.SchedulingStrategy; @@ -50,6 +46,12 @@ import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; +import java.net.URL; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + public abstract class AbstractReportingTaskNode extends AbstractComponentNode implements ReportingTaskNode { private static final Logger LOG = LoggerFactory.getLogger(AbstractReportingTaskNode.class); @@ -67,20 +69,22 @@ public abstract class AbstractReportingTaskNode extends AbstractComponentNode im public AbstractReportingTaskNode(final LoggableComponent reportingTask, final String id, final ControllerServiceProvider controllerServiceProvider, final ProcessScheduler processScheduler, final ValidationContextFactory validationContextFactory, final ComponentVariableRegistry variableRegistry, - final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger) { + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger) { this(reportingTask, id, controllerServiceProvider, processScheduler, validationContextFactory, reportingTask.getComponent().getClass().getSimpleName(), reportingTask.getComponent().getClass().getCanonicalName(), - variableRegistry, reloadComponent, validationTrigger, false); + variableRegistry, reloadComponent, extensionManager, validationTrigger, false); } public AbstractReportingTaskNode(final LoggableComponent reportingTask, final String id, final ControllerServiceProvider controllerServiceProvider, final ProcessScheduler processScheduler, final ValidationContextFactory validationContextFactory, final String componentType, final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry, - final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, + final boolean isExtensionMissing) { - super(id, validationContextFactory, controllerServiceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, validationTrigger, isExtensionMissing); + super(id, validationContextFactory, controllerServiceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, + extensionManager, validationTrigger, isExtensionMissing); this.reportingTaskRef = new AtomicReference<>(new ReportingTaskDetails(reportingTask)); this.processScheduler = processScheduler; this.serviceLookup = controllerServiceProvider; @@ -173,7 +177,7 @@ public boolean isRunning() { @Override public boolean isValidationNecessary() { - return !processScheduler.isScheduled(this); + return !processScheduler.isScheduled(this) || getValidationStatus() != ValidationStatus.VALID; } @Override diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingContext.java index d95a22053d67..66ff5f7ea666 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingContext.java @@ -55,13 +55,13 @@ public class StandardReportingContext implements ReportingContext, ControllerSer private final VariableRegistry variableRegistry; public StandardReportingContext(final FlowController flowController, final BulletinRepository bulletinRepository, - final Map properties, final ControllerServiceProvider serviceProvider, final ReportingTask reportingTask, + final Map properties, final ReportingTask reportingTask, final VariableRegistry variableRegistry) { this.flowController = flowController; - this.eventAccess = flowController; + this.eventAccess = flowController.getEventAccess(); this.bulletinRepository = bulletinRepository; this.properties = Collections.unmodifiableMap(properties); - this.serviceProvider = serviceProvider; + this.serviceProvider = flowController.getControllerServiceProvider(); this.reportingTask = reportingTask; this.variableRegistry = variableRegistry; preparedQueries = new HashMap<>(); @@ -94,7 +94,7 @@ public Bulletin createBulletin(final String category, final Severity severity, f @Override public Bulletin createBulletin(final String componentId, final String category, final Severity severity, final String message) { - final Connectable connectable = flowController.findLocalConnectable(componentId); + final Connectable connectable = flowController.getFlowManager().findConnectable(componentId); if (connectable == null) { throw new IllegalStateException("Cannot create Component-Level Bulletin because no component can be found with ID " + componentId); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingInitializationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingInitializationContext.java index ebe774bdcb84..d66ed35c4ffd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingInitializationContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingInitializationContext.java @@ -16,19 +16,19 @@ */ package org.apache.nifi.controller.reporting; -import java.io.File; -import java.util.Set; -import java.util.concurrent.TimeUnit; - import org.apache.nifi.controller.ControllerService; import org.apache.nifi.controller.ControllerServiceLookup; import org.apache.nifi.controller.NodeTypeProvider; +import org.apache.nifi.controller.kerberos.KerberosConfig; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.reporting.ReportingInitializationContext; import org.apache.nifi.scheduling.SchedulingStrategy; import org.apache.nifi.util.FormatUtils; -import org.apache.nifi.util.NiFiProperties; + +import java.io.File; +import java.util.Set; +import java.util.concurrent.TimeUnit; public class StandardReportingInitializationContext implements ReportingInitializationContext, ControllerServiceLookup { @@ -38,21 +38,19 @@ public class StandardReportingInitializationContext implements ReportingInitiali private final SchedulingStrategy schedulingStrategy; private final ControllerServiceProvider serviceProvider; private final ComponentLog logger; - private final NiFiProperties nifiProperties; + private final KerberosConfig kerberosConfig; private final NodeTypeProvider nodeTypeProvider; - public StandardReportingInitializationContext( - final String id, final String name, final SchedulingStrategy schedulingStrategy, - final String schedulingPeriod, final ComponentLog logger, - final ControllerServiceProvider serviceProvider, final NiFiProperties nifiProperties, - final NodeTypeProvider nodeTypeProvider) { + public StandardReportingInitializationContext(final String id, final String name, final SchedulingStrategy schedulingStrategy, final String schedulingPeriod, + final ComponentLog logger, final ControllerServiceProvider serviceProvider, final KerberosConfig kerberosConfig, + final NodeTypeProvider nodeTypeProvider) { this.id = id; this.name = name; this.schedulingPeriod = schedulingPeriod; this.serviceProvider = serviceProvider; this.schedulingStrategy = schedulingStrategy; this.logger = logger; - this.nifiProperties = nifiProperties; + this.kerberosConfig = kerberosConfig; this.nodeTypeProvider = nodeTypeProvider; } @@ -126,17 +124,17 @@ public ComponentLog getLogger() { @Override public String getKerberosServicePrincipal() { - return nifiProperties.getKerberosServicePrincipal(); + return kerberosConfig.getPrincipal(); } @Override public File getKerberosServiceKeytab() { - return nifiProperties.getKerberosServiceKeytabLocation() == null ? null : new File(nifiProperties.getKerberosServiceKeytabLocation()); + return kerberosConfig.getKeytabLocation(); } @Override public File getKerberosConfigurationFile() { - return nifiProperties.getKerberosConfigurationFile(); + return kerberosConfig.getConfigFile(); } @Override diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingTaskNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingTaskNode.java index 124f142922c3..b63fffddd711 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingTaskNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/reporting/StandardReportingTaskNode.java @@ -29,6 +29,7 @@ import org.apache.nifi.controller.ReloadComponent; import org.apache.nifi.controller.ReportingTaskNode; import org.apache.nifi.controller.ValidationContextFactory; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.registry.ComponentVariableRegistry; import org.apache.nifi.reporting.ReportingContext; import org.apache.nifi.reporting.ReportingTask; @@ -39,17 +40,18 @@ public class StandardReportingTaskNode extends AbstractReportingTaskNode impleme public StandardReportingTaskNode(final LoggableComponent reportingTask, final String id, final FlowController controller, final ProcessScheduler processScheduler, final ValidationContextFactory validationContextFactory, - final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger) { - super(reportingTask, id, controller, processScheduler, validationContextFactory, variableRegistry, reloadComponent, validationTrigger); + final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager extensionManager, + final ValidationTrigger validationTrigger) { + super(reportingTask, id, controller.getControllerServiceProvider(), processScheduler, validationContextFactory, variableRegistry, reloadComponent, extensionManager, validationTrigger); this.flowController = controller; } public StandardReportingTaskNode(final LoggableComponent reportingTask, final String id, final FlowController controller, final ProcessScheduler processScheduler, final ValidationContextFactory validationContextFactory, final String componentType, final String canonicalClassName, final ComponentVariableRegistry variableRegistry, - final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { - super(reportingTask, id, controller, processScheduler, validationContextFactory, componentType, canonicalClassName, - variableRegistry, reloadComponent, validationTrigger, isExtensionMissing); + final ReloadComponent reloadComponent, final ExtensionManager extensionManager, final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { + super(reportingTask, id, controller.getControllerServiceProvider(), processScheduler, validationContextFactory, componentType, canonicalClassName, + variableRegistry, reloadComponent, extensionManager, validationTrigger, isExtensionMissing); this.flowController = controller; } @@ -80,6 +82,6 @@ public boolean isDeprecated() { @Override public ReportingContext getReportingContext() { - return new StandardReportingContext(flowController, flowController.getBulletinRepository(), getProperties(), flowController, getReportingTask(), getVariableRegistry()); + return new StandardReportingContext(flowController, flowController.getBulletinRepository(), getProperties(), getReportingTask(), getVariableRegistry()); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/FileSystemRepository.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/FileSystemRepository.java index 256dba94e5d2..125cd500e84c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/FileSystemRepository.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/FileSystemRepository.java @@ -578,7 +578,7 @@ public ContentClaim create(final boolean lossTolerant) throws IOException { } final long modulatedSectionIndex = currentIndex % SECTIONS_PER_CONTAINER; - final String section = String.valueOf(modulatedSectionIndex); + final String section = String.valueOf(modulatedSectionIndex).intern(); final String claimId = System.currentTimeMillis() + "-" + currentIndex; resourceClaim = resourceClaimManager.newResourceClaim(containerName, section, claimId, lossTolerant, true); @@ -864,9 +864,16 @@ public InputStream read(final ContentClaim claim) throws IOException { } - // see javadocs for claim.getLength() as to why we do this. + // A claim length of -1 indicates that the claim is still being written to and we don't know + // the length. In this case, we don't limit the Input Stream. If the Length has been populated, though, + // it is possible that the Length could then be extended. However, we do want to avoid ever allowing the + // stream to read past the end of the Content Claim. To accomplish this, we use a LimitedInputStream but + // provide a LongSupplier for the length instead of a Long value. this allows us to continue reading until + // we get to the end of the Claim, even if the Claim grows. This may happen, for instance, if we obtain an + // InputStream for this claim, then read from it, write more to the claim, and then attempt to read again. In + // such a case, since we have written to that same Claim, we should still be able to read those bytes. if (claim.getLength() >= 0) { - return new LimitedInputStream(fis, claim.getLength()); + return new LimitedInputStream(fis, claim::getLength); } else { return fis; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java index 3496795a30b3..cc3ac199051f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java @@ -68,6 +68,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -98,6 +99,11 @@ *

*/ public final class StandardProcessSession implements ProcessSession, ProvenanceEventEnricher { + private static final int SOURCE_EVENT_BIT_INDEXES = (1 << ProvenanceEventType.CREATE.ordinal()) + | (1 << ProvenanceEventType.FORK.ordinal()) + | (1 << ProvenanceEventType.JOIN.ordinal()) + | (1 << ProvenanceEventType.RECEIVE.ordinal()) + | (1 << ProvenanceEventType.FETCH.ordinal()); private static final AtomicLong idGenerator = new AtomicLong(0L); private static final AtomicLong enqueuedIndex = new AtomicLong(0L); @@ -110,7 +116,7 @@ public final class StandardProcessSession implements ProcessSession, ProvenanceE private static final Logger claimLog = LoggerFactory.getLogger(StandardProcessSession.class.getSimpleName() + ".claims"); private static final int MAX_ROLLBACK_FLOWFILES_TO_LOG = 5; - private final Map records = new ConcurrentHashMap<>(); + private final Map records = new ConcurrentHashMap<>(); private final Map connectionCounts = new ConcurrentHashMap<>(); private final Map> unacknowledgedFlowFiles = new ConcurrentHashMap<>(); private final Map appendableStreams = new ConcurrentHashMap<>(); @@ -253,7 +259,7 @@ public void checkpoint() { List autoTerminatedEvents = null; // validate that all records have a transfer relationship for them and if so determine the destination node and clone as necessary - final Map toAdd = new HashMap<>(); + final Map toAdd = new HashMap<>(); for (final StandardRepositoryRecord record : records.values()) { if (record.isMarkedForDelete()) { continue; @@ -317,7 +323,7 @@ public void checkpoint() { newRecord.setDestination(destination.getFlowFileQueue()); newRecord.setTransferRelationship(record.getTransferRelationship()); // put the mapping into toAdd because adding to records now will cause a ConcurrentModificationException - toAdd.put(clone, newRecord); + toAdd.put(clone.getId(), newRecord); } } } @@ -365,10 +371,7 @@ private void commit(final Checkpoint checkpoint) { * points to the Original Claim -- which has already been removed! * */ - for (final Map.Entry entry : checkpoint.records.entrySet()) { - final FlowFile flowFile = entry.getKey(); - final StandardRepositoryRecord record = entry.getValue(); - + for (final StandardRepositoryRecord record : checkpoint.records.values()) { if (record.isMarkedForDelete()) { // if the working claim is not the same as the original claim, we can immediately destroy the working claim // because it was created in this session and is to be deleted. We don't need to wait for the FlowFile Repo to sync. @@ -380,10 +383,14 @@ private void commit(final Checkpoint checkpoint) { // an issue if we only updated the FlowFile attributes. decrementClaimCount(record.getOriginalClaim()); } - final long flowFileLife = System.currentTimeMillis() - flowFile.getEntryDate(); - final Connectable connectable = context.getConnectable(); - final Object terminator = connectable instanceof ProcessorNode ? ((ProcessorNode) connectable).getProcessor() : connectable; - LOG.info("{} terminated by {}; life of FlowFile = {} ms", new Object[] {flowFile, terminator, flowFileLife}); + + if (LOG.isInfoEnabled()) { + final FlowFileRecord flowFile = record.getCurrent(); + final long flowFileLife = System.currentTimeMillis() - flowFile.getEntryDate(); + final Connectable connectable = context.getConnectable(); + final Object terminator = connectable instanceof ProcessorNode ? ((ProcessorNode) connectable).getProcessor() : connectable; + LOG.info("{} terminated by {}; life of FlowFile = {} ms", new Object[]{flowFile, terminator, flowFileLife}); + } } else if (record.isWorking() && record.getWorkingClaim() != record.getOriginalClaim()) { // records which have been updated - remove original if exists decrementClaimCount(record.getOriginalClaim()); @@ -544,10 +551,11 @@ private void updateEventRepository(final Checkpoint checkpoint) { flowFileEvent.setFlowFilesSent(flowFilesSent); flowFileEvent.setBytesSent(bytesSent); + final long now = System.currentTimeMillis(); long lineageMillis = 0L; - for (final Map.Entry entry : checkpoint.records.entrySet()) { - final FlowFile flowFile = entry.getKey(); - final long lineageDuration = System.currentTimeMillis() - flowFile.getLineageStartDate(); + for (final StandardRepositoryRecord record : checkpoint.records.values()) { + final FlowFile flowFile = record.getCurrent(); + final long lineageDuration = now - flowFile.getLineageStartDate(); lineageMillis += lineageDuration; } flowFileEvent.setAggregateLineageMillis(lineageMillis); @@ -566,13 +574,16 @@ private void updateEventRepository(final Checkpoint checkpoint) { } private Map combineCounters(final Map first, final Map second) { - if (first == null && second == null) { + final boolean firstEmpty = first == null || first.isEmpty(); + final boolean secondEmpty = second == null || second.isEmpty(); + + if (firstEmpty && secondEmpty) { return null; } - if (first == null) { + if (firstEmpty) { return second; } - if (second == null) { + if (secondEmpty) { return first; } @@ -582,14 +593,13 @@ private Map combineCounters(final Map first, final M return combined; } - private void addEventType(final Map> map, final String id, final ProvenanceEventType eventType) { - Set eventTypes = map.get(id); - if (eventTypes == null) { - eventTypes = new HashSet<>(); - map.put(id, eventTypes); - } + private void addEventType(final Map map, final String id, final ProvenanceEventType eventType) { + final BitSet eventTypes = map.computeIfAbsent(id, key -> new BitSet()); + eventTypes.set(eventType.ordinal()); + } - eventTypes.add(eventType); + private StandardRepositoryRecord getRecord(final FlowFile flowFile) { + return records.get(flowFile.getId()); } private void updateProvenanceRepo(final Checkpoint checkpoint) { @@ -600,7 +610,7 @@ private void updateProvenanceRepo(final Checkpoint checkpoint) { // in case the Processor developer submitted the same events to the reporter. So we use a LinkedHashSet // for this, so that we are able to ensure that the events are submitted in the proper order. final Set recordsToSubmit = new LinkedHashSet<>(); - final Map> eventTypesPerFlowFileId = new HashMap<>(); + final Map eventTypesPerFlowFileId = new HashMap<>(); final Set processorGenerated = checkpoint.reportedEvents; @@ -613,7 +623,7 @@ private void updateProvenanceRepo(final Checkpoint checkpoint) { final ProvenanceEventBuilder builder = entry.getValue(); final FlowFile flowFile = entry.getKey(); - updateEventContentClaims(builder, flowFile, checkpoint.records.get(flowFile)); + updateEventContentClaims(builder, flowFile, checkpoint.getRecord(flowFile)); final ProvenanceEventRecord event = builder.build(); if (!event.getChildUuids().isEmpty() && !isSpuriousForkEvent(event, checkpoint.removedFlowFiles)) { @@ -692,14 +702,15 @@ private void updateProvenanceRepo(final Checkpoint checkpoint) { } if (checkpoint.createdFlowFiles.contains(flowFileId)) { - final Set registeredTypes = eventTypesPerFlowFileId.get(flowFileId); + final BitSet registeredTypes = eventTypesPerFlowFileId.get(flowFileId); boolean creationEventRegistered = false; if (registeredTypes != null) { - if (registeredTypes.contains(ProvenanceEventType.CREATE) - || registeredTypes.contains(ProvenanceEventType.FORK) - || registeredTypes.contains(ProvenanceEventType.JOIN) - || registeredTypes.contains(ProvenanceEventType.RECEIVE) - || registeredTypes.contains(ProvenanceEventType.FETCH)) { + if (registeredTypes.get(ProvenanceEventType.CREATE.ordinal()) + || registeredTypes.get(ProvenanceEventType.FORK.ordinal()) + || registeredTypes.get(ProvenanceEventType.JOIN.ordinal()) + || registeredTypes.get(ProvenanceEventType.RECEIVE.ordinal()) + || registeredTypes.get(ProvenanceEventType.FETCH.ordinal())) { + creationEventRegistered = true; } } @@ -802,7 +813,7 @@ private void updateEventContentClaims(final ProvenanceEventBuilder builder, fina public StandardProvenanceEventRecord enrich(final ProvenanceEventRecord rawEvent, final FlowFile flowFile, final long commitNanos) { verifyTaskActive(); - final StandardRepositoryRecord repoRecord = records.get(flowFile); + final StandardRepositoryRecord repoRecord = getRecord(flowFile); if (repoRecord == null) { throw new FlowFileHandlingException(flowFile + " is not known in this session (" + toString() + ")"); } @@ -839,12 +850,12 @@ public StandardProvenanceEventRecord enrich(final ProvenanceEventRecord rawEvent } private StandardProvenanceEventRecord enrich( - final ProvenanceEventRecord rawEvent, final Map flowFileRecordMap, final Map records, + final ProvenanceEventRecord rawEvent, final Map flowFileRecordMap, final Map records, final boolean updateAttributes, final long commitNanos) { final StandardProvenanceEventRecord.Builder recordBuilder = new StandardProvenanceEventRecord.Builder().fromEvent(rawEvent); final FlowFileRecord eventFlowFile = flowFileRecordMap.get(rawEvent.getFlowFileUuid()); if (eventFlowFile != null) { - final StandardRepositoryRecord repoRecord = records.get(eventFlowFile); + final StandardRepositoryRecord repoRecord = records.get(eventFlowFile.getId()); if (repoRecord.getCurrent() != null && repoRecord.getCurrentClaim() != null) { final ContentClaim currentClaim = repoRecord.getCurrentClaim(); @@ -910,7 +921,7 @@ private boolean isSpuriousForkEvent(final ProvenanceEventRecord event, final Set * @param records records * @return true if spurious route */ - private boolean isSpuriousRouteEvent(final ProvenanceEventRecord event, final Map records) { + private boolean isSpuriousRouteEvent(final ProvenanceEventRecord event, final Map records) { if (event.getEventType() == ProvenanceEventType.ROUTE) { final String relationshipName = event.getRelationship(); final Relationship relationship = new Relationship.Builder().name(relationshipName).build(); @@ -919,10 +930,9 @@ private boolean isSpuriousRouteEvent(final ProvenanceEventRecord event, final Ma // If the number of connections for this relationship is not 1, then we can't ignore this ROUTE event, // as it may be cloning the FlowFile and adding to multiple connections. if (connectionsForRelationship.size() == 1) { - for (final Map.Entry entry : records.entrySet()) { - final FlowFileRecord flowFileRecord = entry.getKey(); + for (final StandardRepositoryRecord repoRecord : records.values()) { + final FlowFileRecord flowFileRecord = repoRecord.getCurrent(); if (event.getFlowFileUuid().equals(flowFileRecord.getAttribute(CoreAttributes.UUID.key()))) { - final StandardRepositoryRecord repoRecord = entry.getValue(); if (repoRecord.getOriginalQueue() == null) { return false; } @@ -1077,35 +1087,35 @@ private String loggableFlowfileInfo() { final StringBuilder details = new StringBuilder(1024).append("["); final int initLen = details.length(); int filesListed = 0; - for (Map.Entry entry : records.entrySet()) { + for (StandardRepositoryRecord repoRecord : records.values()) { if (filesListed >= MAX_ROLLBACK_FLOWFILES_TO_LOG) { break; } filesListed++; - final FlowFileRecord entryKey = entry.getKey(); - final StandardRepositoryRecord entryValue = entry.getValue(); + if (details.length() > initLen) { details.append(", "); } - if (entryValue.getOriginalQueue() != null && entryValue.getOriginalQueue().getIdentifier() != null) { + if (repoRecord.getOriginalQueue() != null && repoRecord.getOriginalQueue().getIdentifier() != null) { details.append("queue=") - .append(entryValue.getOriginalQueue().getIdentifier()) + .append(repoRecord.getOriginalQueue().getIdentifier()) .append("/"); } details.append("filename=") - .append(entryKey.getAttribute(CoreAttributes.FILENAME.key())) + .append(repoRecord.getCurrent().getAttribute(CoreAttributes.FILENAME.key())) .append("/uuid=") - .append(entryKey.getAttribute(CoreAttributes.UUID.key())); + .append(repoRecord.getCurrent().getAttribute(CoreAttributes.UUID.key())); } - if (records.entrySet().size() > MAX_ROLLBACK_FLOWFILES_TO_LOG) { + if (records.size() > MAX_ROLLBACK_FLOWFILES_TO_LOG) { if (details.length() > initLen) { details.append(", "); } - details.append(records.entrySet().size() - MAX_ROLLBACK_FLOWFILES_TO_LOG) + details.append(records.size() - MAX_ROLLBACK_FLOWFILES_TO_LOG) .append(" additional Flowfiles not listed"); } else if (filesListed == 0) { details.append("none"); } + details.append("]"); return details.toString(); } @@ -1216,7 +1226,7 @@ private void migrate(final StandardProcessSession newOwner, Collection throw new IllegalStateException(flowFile + " already in use for an active callback or OutputStream created by ProcessSession.write(FlowFile) has not been closed"); } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); if (record == null) { throw new FlowFileHandlingException(flowFile + " is not known in this session (" + toString() + ")"); } @@ -1275,8 +1285,8 @@ private void migrate(final StandardProcessSession newOwner, Collection for (final FlowFile flowFile : flowFiles) { final FlowFileRecord flowFileRecord = (FlowFileRecord) flowFile; - final StandardRepositoryRecord repoRecord = this.records.remove(flowFile); - newOwner.records.put(flowFileRecord, repoRecord); + final StandardRepositoryRecord repoRecord = this.records.remove(flowFile.getId()); + newOwner.records.put(flowFileRecord.getId(), repoRecord); // Adjust the counts for Connections for each FlowFile that was pulled from a Connection. // We do not have to worry about accounting for 'input counts' on connections because those @@ -1348,9 +1358,9 @@ private String summarizeEvents(final Checkpoint checkpoint) { final Set modifiedFlowFileIds = new HashSet<>(); int largestTransferSetSize = 0; - for (final Map.Entry entry : checkpoint.records.entrySet()) { - final FlowFile flowFile = entry.getKey(); + for (final Map.Entry entry : checkpoint.records.entrySet()) { final StandardRepositoryRecord record = entry.getValue(); + final FlowFile flowFile = record.getCurrent(); final Relationship relationship = record.getTransferRelationship(); if (Relationship.SELF.equals(relationship)) { @@ -1479,7 +1489,7 @@ private void incrementConnectionOutputCounts(final String connectionId, final in private void registerDequeuedRecord(final FlowFileRecord flowFile, final Connection connection) { final StandardRepositoryRecord record = new StandardRepositoryRecord(connection.getFlowFileQueue(), flowFile); - records.put(flowFile, record); + records.put(flowFile.getId(), record); flowFilesIn++; contentSizeIn += flowFile.getSize(); @@ -1655,16 +1665,17 @@ public FlowFile create() { verifyTaskActive(); final Map attrs = new HashMap<>(); - attrs.put(CoreAttributes.FILENAME.key(), String.valueOf(System.nanoTime())); + final String uuid = UUID.randomUUID().toString(); + attrs.put(CoreAttributes.FILENAME.key(), uuid); attrs.put(CoreAttributes.PATH.key(), DEFAULT_FLOWFILE_PATH); - attrs.put(CoreAttributes.UUID.key(), UUID.randomUUID().toString()); + attrs.put(CoreAttributes.UUID.key(), uuid); final FlowFileRecord fFile = new StandardFlowFileRecord.Builder().id(context.getNextFlowFileSequence()) .addAttributes(attrs) .build(); final StandardRepositoryRecord record = new StandardRepositoryRecord(null); record.setWorking(fFile, attrs); - records.put(fFile, record); + records.put(fFile.getId(), record); createdFlowFiles.add(fFile.getAttribute(CoreAttributes.UUID.key())); return fFile; } @@ -1681,7 +1692,7 @@ public FlowFile clone(FlowFile example, final long offset, final long size) { verifyTaskActive(); example = validateRecordState(example); - final StandardRepositoryRecord exampleRepoRecord = records.get(example); + final StandardRepositoryRecord exampleRepoRecord = getRecord(example); final FlowFileRecord currRec = exampleRepoRecord.getCurrent(); final ContentClaim claim = exampleRepoRecord.getCurrentClaim(); if (offset + size > example.getSize()) { @@ -1702,7 +1713,7 @@ public FlowFile clone(FlowFile example, final long offset, final long size) { } final StandardRepositoryRecord record = new StandardRepositoryRecord(null); record.setWorking(clone, clone.getAttributes()); - records.put(clone, record); + records.put(clone.getId(), record); if (offset == 0L && size == example.getSize()) { provenanceReporter.clone(example, clone); @@ -1730,7 +1741,7 @@ private void registerForkEvent(final FlowFile parent, final FlowFile child) { eventBuilder.setComponentType(processorType); eventBuilder.addParentFlowFile(parent); - updateEventContentClaims(eventBuilder, parent, records.get(parent)); + updateEventContentClaims(eventBuilder, parent, getRecord(parent)); forkEventBuilders.put(parent, eventBuilder); } @@ -1752,7 +1763,7 @@ public FlowFile penalize(FlowFile flowFile) { verifyTaskActive(); flowFile = validateRecordState(flowFile); - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final long expirationEpochMillis = System.currentTimeMillis() + context.getConnectable().getPenalizationPeriod(TimeUnit.MILLISECONDS); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(record.getCurrent()).penaltyExpirationTime(expirationEpochMillis).build(); record.setWorking(newFile); @@ -1768,7 +1779,7 @@ public FlowFile putAttribute(FlowFile flowFile, final String key, final String v return flowFile; } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(record.getCurrent()).addAttribute(key, value).build(); record.setWorking(newFile, key, value); @@ -1780,7 +1791,7 @@ public FlowFile putAllAttributes(FlowFile flowFile, final Map at verifyTaskActive(); flowFile = validateRecordState(flowFile); - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final Map updatedAttributes; if (attributes.containsKey(CoreAttributes.UUID.key())) { @@ -1794,6 +1805,7 @@ public FlowFile putAllAttributes(FlowFile flowFile, final Map at final FlowFileRecord newFile = ffBuilder.build(); record.setWorking(newFile, updatedAttributes); + return newFile; } @@ -1806,7 +1818,7 @@ public FlowFile removeAttribute(FlowFile flowFile, final String key) { return flowFile; } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(record.getCurrent()).removeAttributes(key).build(); record.setWorking(newFile, key, null); return newFile; @@ -1821,7 +1833,7 @@ public FlowFile removeAllAttributes(FlowFile flowFile, final Set keys) { return flowFile; } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(record.getCurrent()).removeAttributes(keys).build(); final Map updatedAttrs = new HashMap<>(); @@ -1842,7 +1854,7 @@ public FlowFile removeAllAttributes(FlowFile flowFile, final Pattern keyPattern) verifyTaskActive(); flowFile = validateRecordState(flowFile); - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(record.getCurrent()).removeAttributes(keyPattern).build(); if (keyPattern == null) { @@ -1895,7 +1907,7 @@ public void transfer(FlowFile flowFile, final Relationship relationship) { // the relationship specified is not known in this session/context throw new IllegalArgumentException("Relationship '" + relationship.getName() + "' is not known"); } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); record.setTransferRelationship(relationship); updateLastQueuedDate(record); @@ -1913,7 +1925,7 @@ public void transfer(FlowFile flowFile) { verifyTaskActive(); flowFile = validateRecordState(flowFile); - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); if (record.getOriginalQueue() == null) { throw new IllegalArgumentException("Cannot transfer FlowFiles that are created in this Session back to self"); } @@ -1951,7 +1963,8 @@ public void transfer(Collection flowFiles, final Relationship relation final long queuedTime = System.currentTimeMillis(); long contentSize = 0L; for (final FlowFile flowFile : flowFiles) { - final StandardRepositoryRecord record = records.get(flowFile); + final FlowFileRecord flowFileRecord = (FlowFileRecord) flowFile; + final StandardRepositoryRecord record = getRecord(flowFileRecord); record.setTransferRelationship(relationship); updateLastQueuedDate(record, queuedTime); @@ -1972,7 +1985,7 @@ public void remove(FlowFile flowFile) { verifyTaskActive(); flowFile = validateRecordState(flowFile); - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); record.markForDelete(); removedFlowFiles.add(flowFile.getAttribute(CoreAttributes.UUID.key())); @@ -1996,7 +2009,7 @@ public void remove(Collection flowFiles) { flowFiles = validateRecordState(flowFiles); for (final FlowFile flowFile : flowFiles) { - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); record.markForDelete(); removedFlowFiles.add(flowFile.getAttribute(CoreAttributes.UUID.key())); @@ -2195,7 +2208,7 @@ public void read(FlowFile source, boolean allowSessionStreamManagement, InputStr verifyTaskActive(); source = validateRecordState(source, true); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); try { ensureNotAppending(record.getCurrentClaim()); @@ -2251,10 +2264,12 @@ public InputStream read(FlowFile source) { verifyTaskActive(); source = validateRecordState(source, true); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); try { - ensureNotAppending(record.getCurrentClaim()); + final ContentClaim currentClaim = record.getCurrentClaim(); + ensureNotAppending(currentClaim); + claimCache.flush(currentClaim); } catch (final IOException e) { throw new FlowFileAccessException("Failed to access ContentClaim for " + source.toString(), e); } @@ -2400,7 +2415,7 @@ public FlowFile merge(Collection sources, FlowFile destination, final final Collection sourceRecords = new ArrayList<>(); for (final FlowFile source : sources) { - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); sourceRecords.add(record); try { @@ -2411,7 +2426,7 @@ public FlowFile merge(Collection sources, FlowFile destination, final } } - final StandardRepositoryRecord destinationRecord = records.get(destination); + final StandardRepositoryRecord destinationRecord = getRecord(destination); final ContentRepository contentRepo = context.getContentRepository(); final ContentClaim newClaim; try { @@ -2437,7 +2452,7 @@ public FlowFile merge(Collection sources, FlowFile destination, final final boolean useDemarcator = demarcator != null && demarcator.length > 0; final int numSources = sources.size(); for (final FlowFile source : sources) { - final StandardRepositoryRecord sourceRecord = records.get(source); + final StandardRepositoryRecord sourceRecord = getRecord(source); final long copied = contentRepo.exportTo(sourceRecord.getCurrentClaim(), out, sourceRecord.getCurrentClaimOffset(), source.getSize()); writtenCount += copied; @@ -2473,7 +2488,6 @@ public FlowFile merge(Collection sources, FlowFile destination, final removeTemporaryClaim(destinationRecord); final FlowFileRecord newFile = new StandardFlowFileRecord.Builder().fromFlowFile(destinationRecord.getCurrent()).contentClaim(newClaim).contentClaimOffset(0L).size(writtenCount).build(); destinationRecord.setWorking(newFile); - records.put(newFile, destinationRecord); return newFile; } @@ -2495,7 +2509,7 @@ private void ensureNotAppending(final ContentClaim claim) throws IOException { public OutputStream write(FlowFile source) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); ContentClaim newClaim = null; try { @@ -2618,7 +2632,7 @@ public void close() throws IOException { public FlowFile write(FlowFile source, final OutputStreamCallback writer) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); long writtenToFlowFile = 0L; ContentClaim newClaim = null; @@ -2677,7 +2691,7 @@ public FlowFile append(FlowFile source, final OutputStreamCallback writer) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); long newSize = 0L; // Get the current Content Claim from the record and see if we already have @@ -2858,7 +2872,7 @@ private void resetReadClaim() { public FlowFile write(FlowFile source, final StreamCallback writer) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); final ContentClaim currClaim = record.getCurrentClaim(); long writtenToFlowFile = 0L; @@ -2932,6 +2946,7 @@ public FlowFile write(FlowFile source, final StreamCallback writer) { .build(); record.setWorking(newFile); + return newFile; } @@ -2946,7 +2961,7 @@ public FlowFile importFrom(final Path source, final boolean keepSourceFile, Flow throw new FlowFileAccessException("Cannot write to path " + source.getParent().toFile().getAbsolutePath() + " so cannot delete file; will not import."); } - final StandardRepositoryRecord record = records.get(destination); + final StandardRepositoryRecord record = getRecord(destination); final ContentClaim newClaim; final long claimOffset; @@ -2992,7 +3007,7 @@ public FlowFile importFrom(final InputStream source, FlowFile destination) { verifyTaskActive(); destination = validateRecordState(destination); - final StandardRepositoryRecord record = records.get(destination); + final StandardRepositoryRecord record = getRecord(destination); ContentClaim newClaim = null; final long claimOffset = 0L; @@ -3030,7 +3045,7 @@ public FlowFile importFrom(final InputStream source, FlowFile destination) { public void exportTo(FlowFile source, final Path destination, final boolean append) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); try { ensureNotAppending(record.getCurrentClaim()); @@ -3049,7 +3064,7 @@ public void exportTo(FlowFile source, final Path destination, final boolean appe public void exportTo(FlowFile source, final OutputStream destination) { verifyTaskActive(); source = validateRecordState(source); - final StandardRepositoryRecord record = records.get(source); + final StandardRepositoryRecord record = getRecord(source); if(record.getCurrentClaim() == null) { return; @@ -3137,7 +3152,7 @@ private FlowFile validateRecordState(final FlowFile flowFile, final boolean allo throw new IllegalStateException(flowFile + " already in use for an active callback or an OutputStream created by ProcessSession.write(FlowFile) has not been closed"); } - final StandardRepositoryRecord record = records.get(flowFile); + final StandardRepositoryRecord record = getRecord(flowFile); if (record == null) { rollback(); throw new FlowFileHandlingException(flowFile + " is not known in this session (" + toString() + ")"); @@ -3170,11 +3185,11 @@ private List validateRecordState(final Collection flowFiles) * false otherwise. */ boolean isFlowFileKnown(final FlowFile flowFile) { - return records.containsKey(flowFile); + return records.containsKey(flowFile.getId()); } private FlowFile getMostRecent(final FlowFile flowFile) { - final StandardRepositoryRecord existingRecord = records.get(flowFile); + final StandardRepositoryRecord existingRecord = getRecord(flowFile); return existingRecord == null ? flowFile : existingRecord.getCurrent(); } @@ -3183,10 +3198,12 @@ public FlowFile create(FlowFile parent) { verifyTaskActive(); parent = getMostRecent(parent); + final String uuid = UUID.randomUUID().toString(); + final Map newAttributes = new HashMap<>(3); - newAttributes.put(CoreAttributes.FILENAME.key(), String.valueOf(System.nanoTime())); + newAttributes.put(CoreAttributes.FILENAME.key(), uuid); newAttributes.put(CoreAttributes.PATH.key(), DEFAULT_FLOWFILE_PATH); - newAttributes.put(CoreAttributes.UUID.key(), UUID.randomUUID().toString()); + newAttributes.put(CoreAttributes.UUID.key(), uuid); final StandardFlowFileRecord.Builder fFileBuilder = new StandardFlowFileRecord.Builder().id(context.getNextFlowFileSequence()); @@ -3210,7 +3227,7 @@ public FlowFile create(FlowFile parent) { final FlowFileRecord fFile = fFileBuilder.build(); final StandardRepositoryRecord record = new StandardRepositoryRecord(null); record.setWorking(fFile, newAttributes); - records.put(fFile, record); + records.put(fFile.getId(), record); createdFlowFiles.add(fFile.getAttribute(CoreAttributes.UUID.key())); registerForkEvent(parent, fFile); @@ -3247,9 +3264,10 @@ public FlowFile create(Collection parents) { } } - newAttributes.put(CoreAttributes.FILENAME.key(), String.valueOf(System.nanoTime())); + final String uuid = UUID.randomUUID().toString(); + newAttributes.put(CoreAttributes.FILENAME.key(), uuid); newAttributes.put(CoreAttributes.PATH.key(), DEFAULT_FLOWFILE_PATH); - newAttributes.put(CoreAttributes.UUID.key(), UUID.randomUUID().toString()); + newAttributes.put(CoreAttributes.UUID.key(), uuid); final FlowFileRecord fFile = new StandardFlowFileRecord.Builder().id(context.getNextFlowFileSequence()) .addAttributes(newAttributes) @@ -3258,7 +3276,7 @@ public FlowFile create(Collection parents) { final StandardRepositoryRecord record = new StandardRepositoryRecord(null); record.setWorking(fFile, newAttributes); - records.put(fFile, record); + records.put(fFile.getId(), record); createdFlowFiles.add(fFile.getAttribute(CoreAttributes.UUID.key())); registerJoinEvent(fFile, parents); @@ -3339,7 +3357,7 @@ private static class Checkpoint { private final List autoTerminatedEvents = new ArrayList<>(); private final Set reportedEvents = new LinkedHashSet<>(); - private final Map records = new ConcurrentHashMap<>(); + private final Map records = new ConcurrentHashMap<>(); private final Map connectionCounts = new ConcurrentHashMap<>(); private final Map> unacknowledgedFlowFiles = new ConcurrentHashMap<>(); @@ -3392,5 +3410,9 @@ private void checkpoint(final StandardProcessSession session, final List getAllQueues() { + final Collection connections = flowController.getFlowManager().findAllConnections(); + final List queues = new ArrayList<>(connections.size()); + for (final Connection connection : connections) { + queues.add(connection.getFlowFileQueue()); + } + + return queues; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/io/LimitedInputStream.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/io/LimitedInputStream.java index 74597ae51e20..7c32cc8c080d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/io/LimitedInputStream.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/io/LimitedInputStream.java @@ -18,21 +18,36 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Objects; +import java.util.function.LongSupplier; public class LimitedInputStream extends InputStream { private final InputStream in; - private long limit; + private final long limit; + private final LongSupplier limitSupplier; private long bytesRead = 0; + private long markOffset = -1L; + + public LimitedInputStream(final InputStream in, final LongSupplier limitSupplier) { + this.in = in; + this.limitSupplier = Objects.requireNonNull(limitSupplier); + this.limit = -1; + } public LimitedInputStream(final InputStream in, final long limit) { this.in = in; this.limit = limit; + this.limitSupplier = null; + } + + private long getLimit() { + return limitSupplier == null ? limit : limitSupplier.getAsLong(); } @Override public int read() throws IOException { - if (bytesRead >= limit) { + if (bytesRead >= getLimit()) { return -1; } @@ -45,6 +60,7 @@ public int read() throws IOException { @Override public int read(final byte[] b) throws IOException { + final long limit = getLimit(); if (bytesRead >= limit) { return -1; } @@ -60,6 +76,7 @@ public int read(final byte[] b) throws IOException { @Override public int read(byte[] b, int off, int len) throws IOException { + final long limit = getLimit(); if (bytesRead >= limit) { return -1; } @@ -75,14 +92,14 @@ public int read(byte[] b, int off, int len) throws IOException { @Override public long skip(final long n) throws IOException { - final long skipped = in.skip(Math.min(n, limit - bytesRead)); + final long skipped = in.skip(Math.min(n, getLimit() - bytesRead)); bytesRead += skipped; return skipped; } @Override public int available() throws IOException { - return (int)(limit - bytesRead); + return (int)(getLimit() - bytesRead); } @Override @@ -93,8 +110,7 @@ public void close() throws IOException { @Override public void mark(int readlimit) { in.mark(readlimit); - limit -= bytesRead; - bytesRead = 0; + markOffset = bytesRead; } @Override @@ -105,6 +121,10 @@ public boolean markSupported() { @Override public void reset() throws IOException { in.reset(); - bytesRead = 0; + + if (markOffset >= 0) { + bytesRead = markOffset; + } + markOffset = -1; } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/metrics/EventSumValue.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/metrics/EventSumValue.java index 210f7ace8288..a618812bf2f4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/metrics/EventSumValue.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/metrics/EventSumValue.java @@ -180,7 +180,7 @@ public synchronized void subtract(final EventSumValue other) { final String counterName = entry.getKey(); final Long counterValue = entry.getValue(); - counters.compute(counterName, (key, value) -> value == null ? counterValue : counterValue - value); + counters.compute(counterName, (key, value) -> value == null ? -counterValue : value - counterValue); } } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/EventDrivenSchedulingAgent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/EventDrivenSchedulingAgent.java index de972251d7e8..52dc89c4b9b2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/EventDrivenSchedulingAgent.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/EventDrivenSchedulingAgent.java @@ -36,6 +36,7 @@ import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.processor.ProcessSessionFactory; import org.apache.nifi.processor.SimpleProcessLogger; @@ -65,6 +66,7 @@ public class EventDrivenSchedulingAgent extends AbstractSchedulingAgent { private final AtomicInteger maxThreadCount; private final AtomicInteger activeThreadCount = new AtomicInteger(0); private final StringEncryptor encryptor; + private final ExtensionManager extensionManager; private volatile String adminYieldDuration = "1 sec"; @@ -72,7 +74,8 @@ public class EventDrivenSchedulingAgent extends AbstractSchedulingAgent { private final ConcurrentMap scheduleStates = new ConcurrentHashMap<>(); public EventDrivenSchedulingAgent(final FlowEngine flowEngine, final ControllerServiceProvider serviceProvider, final StateManagerProvider stateManagerProvider, - final EventDrivenWorkerQueue workerQueue, final RepositoryContextFactory contextFactory, final int maxThreadCount, final StringEncryptor encryptor) { + final EventDrivenWorkerQueue workerQueue, final RepositoryContextFactory contextFactory, final int maxThreadCount, + final StringEncryptor encryptor, final ExtensionManager extensionManager) { super(flowEngine); this.serviceProvider = serviceProvider; this.stateManagerProvider = stateManagerProvider; @@ -80,6 +83,7 @@ public EventDrivenSchedulingAgent(final FlowEngine flowEngine, final ControllerS this.contextFactory = contextFactory; this.maxThreadCount = new AtomicInteger(maxThreadCount); this.encryptor = encryptor; + this.extensionManager = extensionManager; for (int i = 0; i < maxThreadCount; i++) { final Runnable eventDrivenTask = new EventDrivenTask(workerQueue, activeThreadCount); @@ -305,7 +309,7 @@ private void trigger(final Connectable worker, final LifecycleState scheduleStat } try { - try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(worker.getClass(), worker.getIdentifier())) { + try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(extensionManager, worker.getClass(), worker.getIdentifier())) { worker.onTrigger(processContext, sessionFactory); } catch (final ProcessException pe) { logger.error("{} failed to process session due to {}", worker, pe.toString()); @@ -323,7 +327,7 @@ private void trigger(final Connectable worker, final LifecycleState scheduleStat } } finally { if (!scheduleState.isScheduled() && scheduleState.getActiveThreadCount() == 1 && scheduleState.mustCallOnStoppedMethods()) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(worker.getClass(), worker.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, worker.getClass(), worker.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, worker, processContext); } } @@ -346,7 +350,7 @@ private void trigger(final ProcessorNode worker, final RepositoryContext context } try { - try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(worker.getProcessor().getClass(), worker.getIdentifier())) { + try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(extensionManager, worker.getProcessor().getClass(), worker.getIdentifier())) { worker.onTrigger(processContext, sessionFactory); } catch (final ProcessException pe) { final ComponentLog procLog = new SimpleProcessLogger(worker.getIdentifier(), worker.getProcessor()); @@ -365,7 +369,7 @@ private void trigger(final ProcessorNode worker, final RepositoryContext context // if the processor is no longer scheduled to run and this is the last thread, // invoke the OnStopped methods if (!scheduleState.isScheduled() && scheduleState.getActiveThreadCount() == 1 && scheduleState.mustCallOnStoppedMethods()) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(worker.getProcessor().getClass(), worker.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, worker.getProcessor().getClass(), worker.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, worker.getProcessor(), processContext); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/QuartzSchedulingAgent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/QuartzSchedulingAgent.java index 43c5e56eae1a..0f73c0e3ade3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/QuartzSchedulingAgent.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/QuartzSchedulingAgent.java @@ -74,7 +74,7 @@ public void doSchedule(final ReportingTaskNode taskNode, final LifecycleState sc throw new IllegalStateException("Cannot schedule Reporting Task " + taskNode.getReportingTask().getIdentifier() + " to run because its scheduling period is not valid"); } - final ReportingTaskWrapper taskWrapper = new ReportingTaskWrapper(taskNode, scheduleState); + final ReportingTaskWrapper taskWrapper = new ReportingTaskWrapper(taskNode, scheduleState, flowController.getExtensionManager()); final AtomicBoolean canceled = new AtomicBoolean(false); final Date initialDate = cronExpression.getTimeAfter(new Date()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java index b23e76356e4a..a7d5fd8a1d61 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/StandardProcessScheduler.java @@ -16,24 +16,12 @@ */ package org.apache.nifi.controller.scheduling; -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.annotation.lifecycle.OnStopped; import org.apache.nifi.annotation.lifecycle.OnUnscheduled; import org.apache.nifi.components.state.StateManager; import org.apache.nifi.components.state.StateManagerProvider; +import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.Funnel; import org.apache.nifi.connectable.Port; @@ -53,6 +41,7 @@ import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.SimpleProcessLogger; import org.apache.nifi.processor.StandardProcessContext; @@ -64,19 +53,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + /** - * Responsible for scheduling Processors, Ports, and Funnels to run at regular - * intervals + * Responsible for scheduling Processors, Ports, and Funnels to run at regular intervals */ public final class StandardProcessScheduler implements ProcessScheduler { private static final Logger LOG = LoggerFactory.getLogger(StandardProcessScheduler.class); - private final ControllerServiceProvider controllerServiceProvider; private final FlowController flowController; private final long administrativeYieldMillis; private final String administrativeYieldDuration; private final StateManagerProvider stateManagerProvider; + private final long processorStartTimeoutMillis; private final ConcurrentMap lifecycleStates = new ConcurrentHashMap<>(); private final ScheduledExecutorService frameworkTaskExecutor; @@ -91,7 +92,6 @@ public final class StandardProcessScheduler implements ProcessScheduler { public StandardProcessScheduler(final FlowEngine componentLifecycleThreadPool, final FlowController flowController, final StringEncryptor encryptor, final StateManagerProvider stateManagerProvider, final NiFiProperties nifiProperties) { this.componentLifeCycleThreadPool = componentLifecycleThreadPool; - this.controllerServiceProvider = flowController; this.flowController = flowController; this.encryptor = encryptor; this.stateManagerProvider = stateManagerProvider; @@ -99,9 +99,16 @@ public StandardProcessScheduler(final FlowEngine componentLifecycleThreadPool, f administrativeYieldDuration = nifiProperties.getAdministrativeYieldDuration(); administrativeYieldMillis = FormatUtils.getTimeDuration(administrativeYieldDuration, TimeUnit.MILLISECONDS); + final String timeoutString = nifiProperties.getProperty(NiFiProperties.PROCESSOR_SCHEDULING_TIMEOUT); + processorStartTimeoutMillis = timeoutString == null ? 60000 : FormatUtils.getTimeDuration(timeoutString.trim(), TimeUnit.MILLISECONDS); + frameworkTaskExecutor = new FlowEngine(4, "Framework Task Thread"); } + public ControllerServiceProvider getControllerServiceProvider() { + return flowController.getControllerServiceProvider(); + } + private StateManager getStateManager(final String componentId) { return stateManagerProvider.getStateManager(componentId); } @@ -180,13 +187,6 @@ public void schedule(final ReportingTaskNode taskNode) { throw new IllegalStateException("Reporting Task " + taskNode.getName() + " cannot be started because it has " + activeThreadCount + " threads still running"); } - switch (taskNode.getValidationStatus()) { - case INVALID: - throw new IllegalStateException("Reporting Task " + taskNode.getName() + " is not in a valid state for the following reasons: " + taskNode.getValidationErrors()); - case VALIDATING: - throw new IllegalStateException("Reporting Task " + taskNode.getName() + " cannot be scheduled because it is in the process of validating its configuration"); - } - final SchedulingAgent agent = getSchedulingAgent(taskNode.getSchedulingStrategy()); lifecycleState.setScheduled(true); @@ -196,44 +196,49 @@ public void run() { final long lastStopTime = lifecycleState.getLastStopTime(); final ReportingTask reportingTask = taskNode.getReportingTask(); - // Continually attempt to start the Reporting Task, and if we fail sleep for a bit each time. - while (true) { - try { - synchronized (lifecycleState) { - // if no longer scheduled to run, then we're finished. This can happen, for example, - // if the @OnScheduled method throws an Exception and the user stops the reporting task - // while we're administratively yielded. - // we also check if the schedule state's last start time is equal to what it was before. - // if not, then means that the reporting task has been stopped and started again, so we should just - // bail; another thread will be responsible for invoking the @OnScheduled methods. - if (!lifecycleState.isScheduled() || lifecycleState.getLastStopTime() != lastStopTime) { - return; - } - - try (final NarCloseable x = NarCloseable.withComponentNarLoader(reportingTask.getClass(), reportingTask.getIdentifier())) { - ReflectionUtils.invokeMethodsWithAnnotation(OnScheduled.class, reportingTask, taskNode.getConfigurationContext()); - } - - agent.schedule(taskNode, lifecycleState); + // Attempt to start the Reporting Task, and if we fail re-schedule the task again after #administrativeYielMillis milliseconds + try { + synchronized (lifecycleState) { + // if no longer scheduled to run, then we're finished. This can happen, for example, + // if the @OnScheduled method throws an Exception and the user stops the reporting task + // while we're administratively yielded. + // we also check if the schedule state's last start time is equal to what it was before. + // if not, then means that the reporting task has been stopped and started again, so we should just + // bail; another thread will be responsible for invoking the @OnScheduled methods. + if (!lifecycleState.isScheduled() || lifecycleState.getLastStopTime() != lastStopTime) { + LOG.debug("Did not complete invocation of @OnScheduled task for {} but Lifecycle State is no longer scheduled. Will not attempt to invoke task anymore", reportingTask); return; } - } catch (final Exception e) { - final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e; - final ComponentLog componentLog = new SimpleProcessLogger(reportingTask.getIdentifier(), reportingTask); - componentLog.error("Failed to invoke @OnEnabled method due to {}", cause); - LOG.error("Failed to invoke the On-Scheduled Lifecycle methods of {} due to {}; administratively yielding this " - + "ReportingTask and will attempt to schedule it again after {}", - new Object[]{reportingTask, e.toString(), administrativeYieldDuration}, e); + final ValidationStatus validationStatus = taskNode.getValidationStatus(); + if (validationStatus != ValidationStatus.VALID) { + LOG.debug("Cannot schedule {} to run because it is currently invalid. Will try again in 5 seconds", taskNode); + componentLifeCycleThreadPool.schedule(this, 5, TimeUnit.SECONDS); + return; + } + + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), reportingTask.getClass(), reportingTask.getIdentifier())) { + ReflectionUtils.invokeMethodsWithAnnotation(OnScheduled.class, reportingTask, taskNode.getConfigurationContext()); + } + + agent.schedule(taskNode, lifecycleState); + } + } catch (final Exception e) { + final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e; + final ComponentLog componentLog = new SimpleProcessLogger(reportingTask.getIdentifier(), reportingTask); + componentLog.error("Failed to invoke @OnScheduled method due to {}", cause); + + LOG.error("Failed to invoke the On-Scheduled Lifecycle methods of {} due to {}; administratively yielding this " + + "ReportingTask and will attempt to schedule it again after {}", + new Object[]{reportingTask, e.toString(), administrativeYieldDuration}, e); + + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), reportingTask.getClass(), reportingTask.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnUnscheduled.class, reportingTask, taskNode.getConfigurationContext()); ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, reportingTask, taskNode.getConfigurationContext()); - - try { - Thread.sleep(administrativeYieldMillis); - } catch (final InterruptedException ie) { - } } + + componentLifeCycleThreadPool.schedule(this, administrativeYieldMillis, TimeUnit.MILLISECONDS); } } }; @@ -262,10 +267,8 @@ public void run() { synchronized (lifecycleState) { lifecycleState.setScheduled(false); - try { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(reportingTask.getClass(), reportingTask.getIdentifier())) { - ReflectionUtils.invokeMethodsWithAnnotation(OnUnscheduled.class, reportingTask, configurationContext); - } + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), reportingTask.getClass(), reportingTask.getIdentifier())) { + ReflectionUtils.invokeMethodsWithAnnotation(OnUnscheduled.class, reportingTask, configurationContext); } catch (final Exception e) { final Throwable cause = e instanceof InvocationTargetException ? e.getCause() : e; final ComponentLog componentLog = new SimpleProcessLogger(reportingTask.getIdentifier(), reportingTask); @@ -274,11 +277,6 @@ public void run() { LOG.error("Failed to invoke the @OnUnscheduled methods of {} due to {}; administratively yielding this ReportingTask and will attempt to schedule it again after {}", reportingTask, cause.toString(), administrativeYieldDuration); LOG.error("", cause); - - try { - Thread.sleep(administrativeYieldMillis); - } catch (final InterruptedException ie) { - } } agent.unschedule(taskNode, lifecycleState); @@ -295,16 +293,16 @@ public void run() { /** * Starts the given {@link Processor} by invoking its - * {@link ProcessorNode#start(ScheduledExecutorService, long, org.apache.nifi.processor.ProcessContext, Runnable)} + * {@link ProcessorNode#start(ScheduledExecutorService, long, long, ProcessContext, SchedulingAgentCallback, boolean)} * method. * - * @see StandardProcessorNode#start(ScheduledExecutorService, long, org.apache.nifi.processor.ProcessContext, Runnable) + * @see StandardProcessorNode#start(ScheduledExecutorService, long, long, ProcessContext, SchedulingAgentCallback, boolean) */ @Override public synchronized CompletableFuture startProcessor(final ProcessorNode procNode, final boolean failIfStopping) { final LifecycleState lifecycleState = getLifecycleState(requireNonNull(procNode), true); - final StandardProcessContext processContext = new StandardProcessContext(procNode, this.controllerServiceProvider, + final StandardProcessContext processContext = new StandardProcessContext(procNode, getControllerServiceProvider(), this.encryptor, getStateManager(procNode.getIdentifier()), lifecycleState::isTerminated); final CompletableFuture future = new CompletableFuture<>(); @@ -329,22 +327,22 @@ public void onTaskComplete() { }; LOG.info("Starting {}", procNode); - procNode.start(this.componentMonitoringThreadPool, this.administrativeYieldMillis, processContext, callback, failIfStopping); + procNode.start(componentMonitoringThreadPool, administrativeYieldMillis, processorStartTimeoutMillis, processContext, callback, failIfStopping); return future; } /** * Stops the given {@link Processor} by invoking its - * {@link ProcessorNode#stop(ScheduledExecutorService, org.apache.nifi.processor.ProcessContext, SchedulingAgent, LifecycleState)} + * {@link ProcessorNode#stop(ProcessScheduler, ScheduledExecutorService, ProcessContext, SchedulingAgent, LifecycleState)} * method. * - * @see StandardProcessorNode#stop(ScheduledExecutorService, org.apache.nifi.processor.ProcessContext, SchedulingAgent, LifecycleState) + * @see StandardProcessorNode#stop(ProcessScheduler, ScheduledExecutorService, ProcessContext, SchedulingAgent, LifecycleState) */ @Override public synchronized CompletableFuture stopProcessor(final ProcessorNode procNode) { final LifecycleState lifecycleState = getLifecycleState(procNode, false); - StandardProcessContext processContext = new StandardProcessContext(procNode, this.controllerServiceProvider, + StandardProcessContext processContext = new StandardProcessContext(procNode, getControllerServiceProvider(), this.encryptor, getStateManager(procNode.getIdentifier()), lifecycleState::isTerminated); LOG.info("Stopping {}", procNode); @@ -371,7 +369,7 @@ public synchronized void terminateProcessor(final ProcessorNode procNode) { getSchedulingAgent(procNode).incrementMaxThreadCount(tasksTerminated); try { - flowController.reload(procNode, procNode.getProcessor().getClass().getName(), procNode.getBundleCoordinate(), Collections.emptySet()); + flowController.getReloadComponent().reload(procNode, procNode.getProcessor().getClass().getName(), procNode.getBundleCoordinate(), Collections.emptySet()); } catch (final ProcessorInstantiationException e) { // This shouldn't happen because we already have been able to instantiate the processor before LOG.error("Failed to replace instance of Processor for {} when terminating Processor", procNode); @@ -495,7 +493,7 @@ private synchronized void stopConnectable(final Connectable connectable) { if (!state.isScheduled() && state.getActiveThreadCount() == 0 && state.mustCallOnStoppedMethods()) { final ConnectableProcessContext processContext = new ConnectableProcessContext(connectable, encryptor, getStateManager(connectable.getIdentifier())); - try (final NarCloseable x = NarCloseable.withComponentNarLoader(connectable.getClass(), connectable.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), connectable.getClass(), connectable.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, connectable, processContext); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/TimerDrivenSchedulingAgent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/TimerDrivenSchedulingAgent.java index 806fc6714000..db937e9cdbfc 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/TimerDrivenSchedulingAgent.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/TimerDrivenSchedulingAgent.java @@ -68,7 +68,7 @@ public void shutdown() { @Override public void doSchedule(final ReportingTaskNode taskNode, final LifecycleState scheduleState) { - final Runnable reportingTaskWrapper = new ReportingTaskWrapper(taskNode, scheduleState); + final Runnable reportingTaskWrapper = new ReportingTaskWrapper(taskNode, scheduleState, flowController.getExtensionManager()); final long schedulingNanos = taskNode.getSchedulingPeriod(TimeUnit.NANOSECONDS); final ScheduledFuture future = flowEngine.scheduleWithFixedDelay(reportingTaskWrapper, 0L, schedulingNanos, TimeUnit.NANOSECONDS); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ScheduledStateLookup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ScheduledStateLookup.java index 39693b8987fe..4a6b4206b699 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ScheduledStateLookup.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/ScheduledStateLookup.java @@ -30,7 +30,7 @@ public interface ScheduledStateLookup { public static final ScheduledStateLookup IDENTITY_LOOKUP = new ScheduledStateLookup() { @Override public ScheduledState getScheduledState(final ProcessorNode procNode) { - return procNode.getScheduledState(); + return procNode.getDesiredState(); } @Override diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java index 597c8fb4811c..87b654016bd5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java @@ -106,12 +106,12 @@ public Document transform(final FlowController controller, final ScheduledStateL rootNode.appendChild(registriesElement); addFlowRegistries(registriesElement, controller.getFlowRegistryClient()); - addProcessGroup(rootNode, controller.getGroup(controller.getRootGroupId()), "rootGroup", scheduledStateLookup); + addProcessGroup(rootNode, controller.getFlowManager().getRootGroup(), "rootGroup", scheduledStateLookup); // Add root-level controller services final Element controllerServicesNode = doc.createElement("controllerServices"); rootNode.appendChild(controllerServicesNode); - for (final ControllerServiceNode serviceNode : controller.getRootControllerServices()) { + for (final ControllerServiceNode serviceNode : controller.getFlowManager().getRootControllerServices()) { addControllerService(controllerServicesNode, serviceNode); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java index e5192b355ea4..f226bb6fd116 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java @@ -113,7 +113,7 @@ public static Map loadControllerServices(final L for (final Element serviceElement : serviceElements) { final ControllerServiceNode serviceNode = createControllerService(controller, serviceElement, encryptor); if (parentGroup == null) { - controller.addRootControllerService(serviceNode); + controller.getFlowManager().addRootControllerService(serviceNode); } else { parentGroup.addControllerService(serviceNode); } @@ -162,19 +162,20 @@ public static void enableControllerServices(final Collection validate(final ValidationContext context) { + return Collections.singleton(new ValidationResult.Builder() + .input("Any Property") + .subject("Missing Controller Service") + .valid(false) + .explanation("Controller Service is of type " + canonicalClassName + ", but this is not a valid Reporting Task type") + .build()); + } + + @Override + public PropertyDescriptor getPropertyDescriptor(final String propertyName) { + return new PropertyDescriptor.Builder() + .name(propertyName) + .description(propertyName) + .required(true) + .sensitive(true) + .build(); + } + + @Override + public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) { + } + + @Override + public List getPropertyDescriptors() { + return Collections.emptyList(); + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public String toString() { + return "GhostControllerService[id=" + identifier + ", type=" + canonicalClassName + "]"; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInitializationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInitializationContext.java index f169662a1ece..4d2bbee07da6 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInitializationContext.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInitializationContext.java @@ -16,15 +16,15 @@ */ package org.apache.nifi.controller.service; -import java.io.File; -import java.util.Set; - import org.apache.nifi.components.state.StateManager; import org.apache.nifi.controller.ControllerService; import org.apache.nifi.controller.ControllerServiceInitializationContext; import org.apache.nifi.controller.ControllerServiceLookup; +import org.apache.nifi.controller.kerberos.KerberosConfig; import org.apache.nifi.logging.ComponentLog; -import org.apache.nifi.util.NiFiProperties; + +import java.io.File; +import java.util.Set; public class StandardControllerServiceInitializationContext implements ControllerServiceInitializationContext, ControllerServiceLookup { @@ -32,17 +32,17 @@ public class StandardControllerServiceInitializationContext implements Controlle private final ControllerServiceProvider serviceProvider; private final ComponentLog logger; private final StateManager stateManager; - private final NiFiProperties nifiProperties; + private final KerberosConfig kerberosConfig; public StandardControllerServiceInitializationContext( final String identifier, final ComponentLog logger, final ControllerServiceProvider serviceProvider, final StateManager stateManager, - final NiFiProperties nifiProperties) { + final KerberosConfig kerberosConfig) { this.id = identifier; this.logger = logger; this.serviceProvider = serviceProvider; this.stateManager = stateManager; - this.nifiProperties = nifiProperties; + this.kerberosConfig = kerberosConfig; } @Override @@ -97,16 +97,16 @@ public StateManager getStateManager() { @Override public String getKerberosServicePrincipal() { - return nifiProperties.getKerberosServicePrincipal(); + return kerberosConfig.getPrincipal(); } @Override public File getKerberosServiceKeytab() { - return nifiProperties.getKerberosServiceKeytabLocation() == null ? null : new File(nifiProperties.getKerberosServiceKeytabLocation()); + return kerberosConfig.getKeytabLocation(); } @Override public File getKerberosConfigurationFile() { - return nifiProperties.getKerberosConfigurationFile(); + return kerberosConfig.getConfigFile(); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java index ea83edcb3d5d..1347e784fc39 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java @@ -17,6 +17,7 @@ package org.apache.nifi.controller.service; import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import java.lang.reflect.InvocationTargetException; @@ -43,19 +44,21 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer private final ControllerService originalService; private final AtomicReference serviceNodeHolder = new AtomicReference<>(null); + private final ExtensionManager extensionManager; /** * @param originalService the original service being proxied */ - public StandardControllerServiceInvocationHandler(final ControllerService originalService) { - this(originalService, null); + public StandardControllerServiceInvocationHandler(final ExtensionManager extensionManager, final ControllerService originalService) { + this(extensionManager, originalService, null); } /** * @param originalService the original service being proxied * @param serviceNode the node holding the original service which will be used for checking the state (disabled vs running) */ - public StandardControllerServiceInvocationHandler(final ControllerService originalService, final ControllerServiceNode serviceNode) { + public StandardControllerServiceInvocationHandler(final ExtensionManager extensionManager, final ControllerService originalService, final ControllerServiceNode serviceNode) { + this.extensionManager = extensionManager; this.originalService = originalService; this.serviceNodeHolder.set(serviceNode); } @@ -80,7 +83,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl + serviceNodeHolder.get().getIdentifier() + " because the Controller Service's State is currently " + state); } - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(originalService.getClass(), originalService.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, originalService.getClass(), originalService.getIdentifier())) { return method.invoke(originalService, args); } catch (final InvocationTargetException e) { // If the ControllerService throws an Exception, it'll be wrapped in an InvocationTargetException. We want diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java index 2323f0268e12..3d6329f8fa72 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java @@ -16,25 +16,6 @@ */ package org.apache.nifi.controller.service; -import java.lang.reflect.InvocationTargetException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.Restricted; import org.apache.nifi.annotation.documentation.DeprecationNotice; @@ -47,8 +28,7 @@ import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.components.ConfigurableComponent; import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.components.ValidationResult; -import org.apache.nifi.components.validation.ValidationState; +import org.apache.nifi.components.validation.ValidationStatus; import org.apache.nifi.components.validation.ValidationTrigger; import org.apache.nifi.controller.AbstractComponentNode; import org.apache.nifi.controller.ComponentNode; @@ -61,6 +41,7 @@ import org.apache.nifi.controller.exception.ControllerServiceInstantiationException; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.processor.SimpleProcessLogger; import org.apache.nifi.registry.ComponentVariableRegistry; @@ -70,6 +51,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + public class StandardControllerServiceNode extends AbstractComponentNode implements ControllerServiceNode { private static final Logger LOG = LoggerFactory.getLogger(StandardControllerServiceNode.class); @@ -92,19 +91,19 @@ public class StandardControllerServiceNode extends AbstractComponentNode impleme public StandardControllerServiceNode(final LoggableComponent implementation, final LoggableComponent proxiedControllerService, final ControllerServiceInvocationHandler invocationHandler, final String id, final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, - final ValidationTrigger validationTrigger) { + final ExtensionManager extensionManager, final ValidationTrigger validationTrigger) { this(implementation, proxiedControllerService, invocationHandler, id, validationContextFactory, serviceProvider, implementation.getComponent().getClass().getSimpleName(), - implementation.getComponent().getClass().getCanonicalName(), variableRegistry, reloadComponent, validationTrigger, false); + implementation.getComponent().getClass().getCanonicalName(), variableRegistry, reloadComponent, extensionManager, validationTrigger, false); } public StandardControllerServiceNode(final LoggableComponent implementation, final LoggableComponent proxiedControllerService, final ControllerServiceInvocationHandler invocationHandler, final String id, final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, final String componentType, final String componentCanonicalClass, - final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ValidationTrigger validationTrigger, - final boolean isExtensionMissing) { + final ComponentVariableRegistry variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager extensionManager, + final ValidationTrigger validationTrigger, final boolean isExtensionMissing) { - super(id, validationContextFactory, serviceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, validationTrigger, isExtensionMissing); + super(id, validationContextFactory, serviceProvider, componentType, componentCanonicalClass, variableRegistry, reloadComponent, extensionManager, validationTrigger, isExtensionMissing); this.serviceProvider = serviceProvider; this.active = new AtomicBoolean(); setControllerServiceAndProxy(implementation, proxiedControllerService, invocationHandler); @@ -318,14 +317,6 @@ public void verifyCanEnable() { if (getState() != ControllerServiceState.DISABLED) { throw new IllegalStateException(getControllerServiceImplementation().getIdentifier() + " cannot be enabled because it is not disabled"); } - - final ValidationState validationState = getValidationState(); - switch (validationState.getStatus()) { - case INVALID: - throw new IllegalStateException(getControllerServiceImplementation().getIdentifier() + " cannot be enabled because it is not valid: " + validationState.getValidationErrors()); - case VALIDATING: - throw new IllegalStateException(getControllerServiceImplementation().getIdentifier() + " cannot be enabled because its validation has not yet completed"); - } } @Override @@ -333,11 +324,6 @@ public void verifyCanEnable(final Set ignoredReferences) if (getState() != ControllerServiceState.DISABLED) { throw new IllegalStateException(getControllerServiceImplementation().getIdentifier() + " cannot be enabled because it is not disabled"); } - - final Collection validationErrors = getValidationErrors(ignoredReferences); - if (ignoredReferences != null && !validationErrors.isEmpty()) { - throw new IllegalStateException("Controller Service with ID " + getIdentifier() + " cannot be enabled because it is not currently valid: " + validationErrors); - } } @Override @@ -388,8 +374,11 @@ public boolean isValidationNecessary() { case DISABLED: case DISABLING: return true; - case ENABLED: case ENABLING: + // If enabling and currently not valid, then we must trigger validation to occur. This allows the #enable method + // to continue running in the background and complete enabling when the service becomes valid. + return getValidationStatus() != ValidationStatus.VALID; + case ENABLED: default: return false; } @@ -397,7 +386,7 @@ public boolean isValidationNecessary() { /** * Will atomically enable this service by invoking its @OnEnabled operation. - * It uses CAS operation on {@link #stateRef} to transition this service + * It uses CAS operation on {@link #stateTransition} to transition this service * from DISABLED to ENABLING state. If such transition succeeds the service * will be marked as 'active' (see {@link ControllerServiceNode#isActive()}). * If such transition doesn't succeed then no enabling logic will be @@ -428,8 +417,22 @@ public CompletableFuture enable(final ScheduledExecutorService scheduler, scheduler.execute(new Runnable() { @Override public void run() { + if (!isActive()) { + LOG.debug("{} is no longer active so will not attempt to enable it", StandardControllerServiceNode.this); + stateTransition.disable(); + return; + } + + final ValidationStatus validationStatus = getValidationStatus(); + if (validationStatus != ValidationStatus.VALID) { + LOG.debug("Cannot enable {} because it is not currently valid. Will try again in 5 seconds", StandardControllerServiceNode.this); + scheduler.schedule(this, 5, TimeUnit.SECONDS); + future.completeExceptionally(new RuntimeException(this + " cannot be enabled because it is not currently valid. Will try again in 5 seconds.")); + return; + } + try { - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getControllerServiceImplementation().getClass(), getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), getControllerServiceImplementation().getClass(), getIdentifier())) { ReflectionUtils.invokeMethodsWithAnnotation(OnEnabled.class, getControllerServiceImplementation(), configContext); } @@ -445,7 +448,7 @@ public void run() { invokeDisable(configContext); stateTransition.disable(); } else { - LOG.debug("Successfully enabled {}", service); + LOG.info("Successfully enabled {}", service); } } catch (Exception e) { future.completeExceptionally(e); @@ -459,7 +462,7 @@ public void run() { if (isActive()) { scheduler.schedule(this, administrativeYieldMillis, TimeUnit.MILLISECONDS); } else { - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getControllerServiceImplementation().getClass(), getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), getControllerServiceImplementation().getClass(), getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnDisabled.class, getControllerServiceImplementation(), configContext); } stateTransition.disable(); @@ -477,7 +480,7 @@ public void run() { /** * Will atomically disable this service by invoking its @OnDisabled operation. - * It uses CAS operation on {@link #stateRef} to transition this service + * It uses CAS operation on {@link #stateTransition} to transition this service * from ENABLED to DISABLING state. If such transition succeeds the service * will be de-activated (see {@link ControllerServiceNode#isActive()}). * If such transition doesn't succeed (the service is still in ENABLING state) @@ -529,7 +532,7 @@ public void run() { private void invokeDisable(ConfigurationContext configContext) { - try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getControllerServiceImplementation().getClass(), getIdentifier())) { + try (final NarCloseable nc = NarCloseable.withComponentNarLoader(getExtensionManager(), getControllerServiceImplementation().getClass(), getIdentifier())) { ReflectionUtils.invokeMethodsWithAnnotation(OnDisabled.class, StandardControllerServiceNode.this.getControllerServiceImplementation(), configContext); LOG.debug("Successfully disabled {}", this); } catch (Exception e) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java index 3e212c0071c5..e545558e09ad 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java @@ -16,11 +16,24 @@ */ package org.apache.nifi.controller.service; -import static java.util.Objects.requireNonNull; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.controller.ComponentNode; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.ScheduledState; +import org.apache.nifi.controller.flow.FlowManager; +import org.apache.nifi.controller.scheduling.StandardProcessScheduler; +import org.apache.nifi.events.BulletinFactory; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.logging.LogRepositoryFactory; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.reporting.BulletinRepository; +import org.apache.nifi.reporting.Severity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -39,46 +52,7 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import org.apache.commons.lang3.ClassUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.annotation.lifecycle.OnAdded; -import org.apache.nifi.bundle.Bundle; -import org.apache.nifi.bundle.BundleCoordinate; -import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.components.ValidationResult; -import org.apache.nifi.components.state.StateManager; -import org.apache.nifi.components.state.StateManagerProvider; -import org.apache.nifi.components.validation.ValidationTrigger; -import org.apache.nifi.controller.ComponentNode; -import org.apache.nifi.controller.ControllerService; -import org.apache.nifi.controller.FlowController; -import org.apache.nifi.controller.LoggableComponent; -import org.apache.nifi.controller.ProcessorNode; -import org.apache.nifi.controller.ReportingTaskNode; -import org.apache.nifi.controller.ScheduledState; -import org.apache.nifi.controller.TerminationAwareLogger; -import org.apache.nifi.controller.ValidationContextFactory; -import org.apache.nifi.controller.exception.ComponentLifeCycleException; -import org.apache.nifi.controller.exception.ControllerServiceInstantiationException; -import org.apache.nifi.controller.scheduling.StandardProcessScheduler; -import org.apache.nifi.events.BulletinFactory; -import org.apache.nifi.groups.ProcessGroup; -import org.apache.nifi.logging.ComponentLog; -import org.apache.nifi.logging.LogRepositoryFactory; -import org.apache.nifi.nar.ExtensionManager; -import org.apache.nifi.nar.NarCloseable; -import org.apache.nifi.processor.SimpleProcessLogger; -import org.apache.nifi.processor.StandardValidationContextFactory; -import org.apache.nifi.registry.ComponentVariableRegistry; -import org.apache.nifi.registry.VariableRegistry; -import org.apache.nifi.registry.variable.StandardComponentVariableRegistry; -import org.apache.nifi.reporting.BulletinRepository; -import org.apache.nifi.reporting.Severity; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.util.ReflectionUtils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static java.util.Objects.requireNonNull; public class StandardControllerServiceProvider implements ControllerServiceProvider { @@ -86,167 +60,21 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi private final StandardProcessScheduler processScheduler; private final BulletinRepository bulletinRepo; - private final StateManagerProvider stateManagerProvider; - private final VariableRegistry variableRegistry; private final FlowController flowController; - private final NiFiProperties nifiProperties; + private final FlowManager flowManager; private final ConcurrentMap serviceCache = new ConcurrentHashMap<>(); - private final ValidationTrigger validationTrigger; - - public StandardControllerServiceProvider(final FlowController flowController, final StandardProcessScheduler scheduler, final BulletinRepository bulletinRepo, - final StateManagerProvider stateManagerProvider, final VariableRegistry variableRegistry, final NiFiProperties nifiProperties, final ValidationTrigger validationTrigger) { + public StandardControllerServiceProvider(final FlowController flowController, final StandardProcessScheduler scheduler, final BulletinRepository bulletinRepo) { this.flowController = flowController; this.processScheduler = scheduler; this.bulletinRepo = bulletinRepo; - this.stateManagerProvider = stateManagerProvider; - this.variableRegistry = variableRegistry; - this.nifiProperties = nifiProperties; - this.validationTrigger = validationTrigger; - } - - private StateManager getStateManager(final String componentId) { - return stateManagerProvider.getStateManager(componentId); + this.flowManager = flowController.getFlowManager(); } @Override - public ControllerServiceNode createControllerService(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set additionalUrls, final boolean firstTimeAdded) { - if (type == null || id == null || bundleCoordinate == null) { - throw new NullPointerException(); - } - - ClassLoader cl = null; - final ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - final Class rawClass; - try { - final Bundle csBundle = ExtensionManager.getBundle(bundleCoordinate); - if (csBundle == null) { - throw new ControllerServiceInstantiationException("Unable to find bundle for coordinate " + bundleCoordinate.getCoordinate()); - } - - cl = ExtensionManager.createInstanceClassLoader(type, id, csBundle, additionalUrls); - Thread.currentThread().setContextClassLoader(cl); - rawClass = Class.forName(type, false, cl); - } catch (final Exception e) { - logger.error("Could not create Controller Service of type " + type + " for ID " + id + "; creating \"Ghost\" implementation", e); - Thread.currentThread().setContextClassLoader(currentContextClassLoader); - return createGhostControllerService(type, id, bundleCoordinate); - } - - final Class controllerServiceClass = rawClass.asSubclass(ControllerService.class); - - final ControllerService originalService = controllerServiceClass.newInstance(); - final StandardControllerServiceInvocationHandler invocationHandler = new StandardControllerServiceInvocationHandler(originalService); - - // extract all interfaces... controllerServiceClass is non null so getAllInterfaces is non null - final List> interfaceList = ClassUtils.getAllInterfaces(controllerServiceClass); - final Class[] interfaces = interfaceList.toArray(new Class[interfaceList.size()]); - - final ControllerService proxiedService; - if (cl == null) { - proxiedService = (ControllerService) Proxy.newProxyInstance(getClass().getClassLoader(), interfaces, invocationHandler); - } else { - proxiedService = (ControllerService) Proxy.newProxyInstance(cl, interfaces, invocationHandler); - } - logger.info("Created Controller Service of type {} with identifier {}", type, id); - - final ComponentLog serviceLogger = new SimpleProcessLogger(id, originalService); - final TerminationAwareLogger terminationAwareLogger = new TerminationAwareLogger(serviceLogger); - - originalService.initialize(new StandardControllerServiceInitializationContext(id, terminationAwareLogger, this, getStateManager(id), nifiProperties)); - - final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(this, variableRegistry); - - final LoggableComponent originalLoggableComponent = new LoggableComponent<>(originalService, bundleCoordinate, terminationAwareLogger); - final LoggableComponent proxiedLoggableComponent = new LoggableComponent<>(proxiedService, bundleCoordinate, terminationAwareLogger); - - final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); - final ControllerServiceNode serviceNode = new StandardControllerServiceNode(originalLoggableComponent, proxiedLoggableComponent, invocationHandler, - id, validationContextFactory, this, componentVarRegistry, flowController, validationTrigger); - serviceNode.setName(rawClass.getSimpleName()); - - invocationHandler.setServiceNode(serviceNode); - - if (firstTimeAdded) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(originalService.getClass(), originalService.getIdentifier())) { - ReflectionUtils.invokeMethodsWithAnnotation(OnAdded.class, originalService); - } catch (final Exception e) { - throw new ComponentLifeCycleException("Failed to invoke On-Added Lifecycle methods of " + originalService, e); - } - } - - serviceCache.putIfAbsent(id, serviceNode); - - return serviceNode; - } catch (final Throwable t) { - throw new ControllerServiceInstantiationException(t); - } finally { - if (currentContextClassLoader != null) { - Thread.currentThread().setContextClassLoader(currentContextClassLoader); - } - } - } - - private ControllerServiceNode createGhostControllerService(final String type, final String id, final BundleCoordinate bundleCoordinate) { - final ControllerServiceInvocationHandler invocationHandler = new ControllerServiceInvocationHandler() { - @Override - public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { - final String methodName = method.getName(); - - if ("validate".equals(methodName)) { - final ValidationResult result = new ValidationResult.Builder() - .input("Any Property") - .subject("Missing Controller Service") - .valid(false) - .explanation("Controller Service could not be created because the Controller Service Type (" + type + ") could not be found") - .build(); - return Collections.singleton(result); - } else if ("getPropertyDescriptor".equals(methodName)) { - final String propertyName = (String) args[0]; - return new PropertyDescriptor.Builder() - .name(propertyName) - .description(propertyName) - .sensitive(true) - .required(true) - .build(); - } else if ("getPropertyDescriptors".equals(methodName)) { - return Collections.emptyList(); - } else if ("onPropertyModified".equals(methodName)) { - return null; - } else if ("getIdentifier".equals(methodName)) { - return id; - } else if ("toString".equals(methodName)) { - return "GhostControllerService[id=" + id + ", type=" + type + "]"; - } else if ("hashCode".equals(methodName)) { - return 91 * type.hashCode() + 41 * id.hashCode(); - } else if ("equals".equals(methodName)) { - return proxy == args[0]; - } else { - throw new IllegalStateException("Controller Service could not be created because the Controller Service Type (" + type + ") could not be found"); - } - } - @Override - public void setServiceNode(ControllerServiceNode serviceNode) { - // nothing to do - } - }; - - final ControllerService proxiedService = (ControllerService) Proxy.newProxyInstance(getClass().getClassLoader(), - new Class[]{ControllerService.class}, invocationHandler); - - final String simpleClassName = type.contains(".") ? StringUtils.substringAfterLast(type, ".") : type; - final String componentType = "(Missing) " + simpleClassName; - - final LoggableComponent proxiedLoggableComponent = new LoggableComponent<>(proxiedService, bundleCoordinate, null); - - final ComponentVariableRegistry componentVarRegistry = new StandardComponentVariableRegistry(this.variableRegistry); - final ControllerServiceNode serviceNode = new StandardControllerServiceNode(proxiedLoggableComponent, proxiedLoggableComponent, invocationHandler, id, - new StandardValidationContextFactory(this, variableRegistry), this, componentType, type, componentVarRegistry, flowController, validationTrigger, true); - - serviceCache.putIfAbsent(id, serviceNode); - return serviceNode; + public void onControllerServiceAdded(final ControllerServiceNode serviceNode) { + serviceCache.putIfAbsent(serviceNode.getIdentifier(), serviceNode); } @Override @@ -361,7 +189,7 @@ public void enableControllerServices(final Collection ser Iterator serviceIter = serviceNodes.iterator(); while (serviceIter.hasNext() && shouldStart) { ControllerServiceNode controllerServiceNode = serviceIter.next(); - List requiredServices = ((StandardControllerServiceNode) controllerServiceNode).getRequiredControllerServices(); + List requiredServices = controllerServiceNode.getRequiredControllerServices(); for (ControllerServiceNode requiredService : requiredServices) { if (!requiredService.isActive() && !serviceNodes.contains(requiredService)) { shouldStart = false; @@ -408,10 +236,8 @@ public Future enableControllerServicesAsync(final Collection serviceNodes, final CompletableFuture completableFuture) { // validate that we are able to start all of the services. - Iterator serviceIter = serviceNodes.iterator(); - while (serviceIter.hasNext()) { - ControllerServiceNode controllerServiceNode = serviceIter.next(); - List requiredServices = ((StandardControllerServiceNode) controllerServiceNode).getRequiredControllerServices(); + for (final ControllerServiceNode controllerServiceNode : serviceNodes) { + List requiredServices = controllerServiceNode.getRequiredControllerServices(); for (ControllerServiceNode requiredService : requiredServices) { if (!requiredService.isActive() && !serviceNodes.contains(requiredService)) { logger.error("Cannot enable {} because it has a dependency on {}, which is not enabled", controllerServiceNode, requiredService); @@ -499,7 +325,7 @@ static List> determineEnablingOrder(final Map branch = new ArrayList<>(); - determineEnablingOrder(serviceNodeMap, node, branch, new HashSet()); + determineEnablingOrder(serviceNodeMap, node, branch, new HashSet<>()); orderedNodeLists.add(branch); } @@ -598,15 +424,15 @@ public ControllerService getControllerService(final String serviceIdentifier) { } private ProcessGroup getRootGroup() { - return flowController.getGroup(flowController.getRootGroupId()); + return flowManager.getRootGroup(); } @Override public ControllerService getControllerServiceForComponent(final String serviceIdentifier, final String componentId) { // Find the Process Group that owns the component. - ProcessGroup groupOfInterest = null; + ProcessGroup groupOfInterest; - final ProcessorNode procNode = flowController.getProcessorNode(componentId); + final ProcessorNode procNode = flowManager.getProcessorNode(componentId); if (procNode == null) { final ControllerServiceNode serviceNode = getControllerServiceNode(componentId); if (serviceNode == null) { @@ -617,7 +443,7 @@ public ControllerService getControllerServiceForComponent(final String serviceId // we have confirmed that the component is a reporting task. We can only reference Controller Services // that are scoped at the FlowController level in this case. - final ControllerServiceNode rootServiceNode = flowController.getRootControllerService(serviceIdentifier); + final ControllerServiceNode rootServiceNode = flowManager.getRootControllerService(serviceIdentifier); return (rootServiceNode == null) ? null : rootServiceNode.getProxiedControllerService(); } else { groupOfInterest = serviceNode.getProcessGroup(); @@ -627,7 +453,7 @@ public ControllerService getControllerServiceForComponent(final String serviceId } if (groupOfInterest == null) { - final ControllerServiceNode rootServiceNode = flowController.getRootControllerService(serviceIdentifier); + final ControllerServiceNode rootServiceNode = flowManager.getRootControllerService(serviceIdentifier); return (rootServiceNode == null) ? null : rootServiceNode.getProxiedControllerService(); } @@ -660,7 +486,7 @@ public boolean isControllerServiceEnabling(final String serviceIdentifier) { @Override public ControllerServiceNode getControllerServiceNode(final String serviceIdentifier) { - final ControllerServiceNode rootServiceNode = flowController.getRootControllerService(serviceIdentifier); + final ControllerServiceNode rootServiceNode = flowManager.getRootControllerService(serviceIdentifier); if (rootServiceNode != null) { return rootServiceNode; } @@ -672,10 +498,10 @@ public ControllerServiceNode getControllerServiceNode(final String serviceIdenti public Set getControllerServiceIdentifiers(final Class serviceType, final String groupId) { final Set serviceNodes; if (groupId == null) { - serviceNodes = flowController.getRootControllerServices(); + serviceNodes = flowManager.getRootControllerServices(); } else { ProcessGroup group = getRootGroup(); - if (!FlowController.ROOT_GROUP_ID_ALIAS.equals(groupId) && !group.getIdentifier().equals(groupId)) { + if (!FlowManager.ROOT_GROUP_ID_ALIAS.equals(groupId) && !group.getIdentifier().equals(groupId)) { group = group.findProcessGroup(groupId); } @@ -703,23 +529,20 @@ public String getControllerServiceName(final String serviceIdentifier) { public void removeControllerService(final ControllerServiceNode serviceNode) { final ProcessGroup group = requireNonNull(serviceNode).getProcessGroup(); if (group == null) { - flowController.removeRootControllerService(serviceNode); + flowManager.removeRootControllerService(serviceNode); return; } group.removeControllerService(serviceNode); LogRepositoryFactory.removeRepository(serviceNode.getIdentifier()); - ExtensionManager.removeInstanceClassLoader(serviceNode.getIdentifier()); + final ExtensionManager extensionManager = flowController.getExtensionManager(); + extensionManager.removeInstanceClassLoader(serviceNode.getIdentifier()); serviceCache.remove(serviceNode.getIdentifier()); } @Override - public Set getAllControllerServices() { - final Set allServices = new HashSet<>(); - allServices.addAll(flowController.getRootControllerServices()); - allServices.addAll(serviceCache.values()); - - return allServices; + public Collection getNonRootControllerServices() { + return serviceCache.values(); } @@ -818,4 +641,9 @@ public void verifyCanStopReferencingComponents(final ControllerServiceNode servi public Set getControllerServiceIdentifiers(final Class serviceType) throws IllegalArgumentException { throw new UnsupportedOperationException("Cannot obtain Controller Service Identifiers for service type " + serviceType + " without providing a Process Group Identifier"); } + + @Override + public ExtensionManager getExtensionManager() { + return flowController.getExtensionManager(); + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/StandardStateManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/StandardStateManager.java index 639f8a25bbdd..6a60058d6e70 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/StandardStateManager.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/StandardStateManager.java @@ -17,9 +17,6 @@ package org.apache.nifi.controller.state; -import java.io.IOException; -import java.util.Map; - import org.apache.nifi.components.state.Scope; import org.apache.nifi.components.state.StateManager; import org.apache.nifi.components.state.StateMap; @@ -29,6 +26,9 @@ import org.apache.nifi.logging.LogRepositoryFactory; import org.apache.nifi.processor.SimpleProcessLogger; +import java.io.IOException; +import java.util.Map; + public class StandardStateManager implements StateManager { private final StateProvider localProvider; private final StateProvider clusterProvider; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java index d3248b507876..dbdbb6bc8070 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/manager/StandardStateManagerProvider.java @@ -70,16 +70,17 @@ private StandardStateManagerProvider(final StateProvider localStateProvider, fin this.clusterStateProvider = clusterStateProvider; } - public static synchronized StateManagerProvider create(final NiFiProperties properties, final VariableRegistry variableRegistry) throws ConfigParseException, IOException { + public static synchronized StateManagerProvider create(final NiFiProperties properties, final VariableRegistry variableRegistry, final ExtensionManager extensionManager) + throws ConfigParseException, IOException { if (provider != null) { return provider; } - final StateProvider localProvider = createLocalStateProvider(properties,variableRegistry); + final StateProvider localProvider = createLocalStateProvider(properties,variableRegistry, extensionManager); final StateProvider clusterProvider; if (properties.isNode()) { - clusterProvider = createClusteredStateProvider(properties,variableRegistry); + clusterProvider = createClusteredStateProvider(properties,variableRegistry, extensionManager); } else { clusterProvider = null; } @@ -88,20 +89,23 @@ public static synchronized StateManagerProvider create(final NiFiProperties prop return provider; } - private static StateProvider createLocalStateProvider(final NiFiProperties properties, final VariableRegistry variableRegistry) throws IOException, ConfigParseException { + private static StateProvider createLocalStateProvider(final NiFiProperties properties, final VariableRegistry variableRegistry, final ExtensionManager extensionManager) + throws IOException, ConfigParseException { final File configFile = properties.getStateManagementConfigFile(); - return createStateProvider(configFile, Scope.LOCAL, properties, variableRegistry); + return createStateProvider(configFile, Scope.LOCAL, properties, variableRegistry, extensionManager); } - private static StateProvider createClusteredStateProvider(final NiFiProperties properties, final VariableRegistry variableRegistry) throws IOException, ConfigParseException { + private static StateProvider createClusteredStateProvider(final NiFiProperties properties, final VariableRegistry variableRegistry, final ExtensionManager extensionManager) + throws IOException, ConfigParseException { final File configFile = properties.getStateManagementConfigFile(); - return createStateProvider(configFile, Scope.CLUSTER, properties, variableRegistry); + return createStateProvider(configFile, Scope.CLUSTER, properties, variableRegistry, extensionManager); } private static StateProvider createStateProvider(final File configFile, final Scope scope, final NiFiProperties properties, - final VariableRegistry variableRegistry) throws ConfigParseException, IOException { + final VariableRegistry variableRegistry, final ExtensionManager extensionManager) + throws ConfigParseException, IOException { final String providerId; final String providerIdPropertyName; final String providerDescription; @@ -169,7 +173,7 @@ private static StateProvider createStateProvider(final File configFile, final Sc final StateProvider provider; try { - provider = instantiateStateProvider(providerClassName); + provider = instantiateStateProvider(extensionManager, providerClassName); } catch (final Exception e) { throw new RuntimeException("Cannot create " + providerDescription + " of type " + providerClassName, e); } @@ -194,7 +198,7 @@ private static StateProvider createStateProvider(final File configFile, final Sc propertyMap.put(descriptor, new StandardPropertyValue(entry.getValue(),null, variableRegistry)); } - final SSLContext sslContext = SslContextFactory.createSslContext(properties, false); + final SSLContext sslContext = SslContextFactory.createSslContext(properties); final ComponentLog logger = new SimpleProcessLogger(providerId, provider); final StateProviderInitializationContext initContext = new StandardStateProviderInitializationContext(providerId, propertyMap, sslContext, logger); @@ -223,10 +227,10 @@ private static StateProvider createStateProvider(final File configFile, final Sc return provider; } - private static StateProvider instantiateStateProvider(final String type) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + private static StateProvider instantiateStateProvider(final ExtensionManager extensionManager, final String type) throws ClassNotFoundException, InstantiationException, IllegalAccessException { final ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader(); try { - final List bundles = ExtensionManager.getBundles(type); + final List bundles = extensionManager.getBundles(type); if (bundles.size() == 0) { throw new IllegalStateException(String.format("The specified class '%s' is not known to this nifi.", type)); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/AbstractMetricDescriptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/AbstractMetricDescriptor.java new file mode 100644 index 000000000000..980efdda918a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/AbstractMetricDescriptor.java @@ -0,0 +1,98 @@ +/* + * 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.controller.status.history; + +import java.util.List; + +public abstract class AbstractMetricDescriptor implements MetricDescriptor { + private final IndexableMetric indexableMetric; + private final String field; + private final String label; + private final String description; + private final MetricDescriptor.Formatter formatter; + private final ValueMapper valueMapper; + private final ValueReducer reducer; + + public AbstractMetricDescriptor(final IndexableMetric indexableMetric, final String field, final String label, final String description, + final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction) { + this(indexableMetric, field, label, description, formatter, valueFunction, null); + } + + public AbstractMetricDescriptor(final IndexableMetric indexableMetric, final String field, final String label, final String description, + final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction, final ValueReducer reducer) { + this.indexableMetric = indexableMetric; + this.field = field; + this.label = label; + this.description = description; + this.formatter = formatter; + this.valueMapper = valueFunction; + this.reducer = reducer == null ? new SumReducer() : reducer; + } + + @Override + public int getMetricIdentifier() { + return indexableMetric.getIndex(); + } + + @Override + public String getField() { + return field; + } + + @Override + public boolean isCounter() { + return false; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public MetricDescriptor.Formatter getFormatter() { + return formatter; + } + + @Override + public ValueMapper getValueFunction() { + return valueMapper; + } + + @Override + public ValueReducer getValueReducer() { + return reducer; + } + + class SumReducer implements ValueReducer { + + @Override + public Long reduce(final List values) { + long sum = 0; + for (final StatusSnapshot snapshot : values) { + sum += snapshot.getStatusMetric(AbstractMetricDescriptor.this); + } + + return sum; + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/CounterMetricDescriptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/CounterMetricDescriptor.java new file mode 100644 index 000000000000..5aa7a72a355e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/CounterMetricDescriptor.java @@ -0,0 +1,63 @@ +/* + * 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.controller.status.history; + +public class CounterMetricDescriptor extends AbstractMetricDescriptor { + + private static final IndexableMetric ILLEGAL_INDEXABLE_METRIC = () -> { + throw new UnsupportedOperationException(); + }; + + public CounterMetricDescriptor(final String field, final String label, final String description, + final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction) { + super(ILLEGAL_INDEXABLE_METRIC, field, label, description, formatter, valueFunction, null); + } + + public CounterMetricDescriptor(final String field, final String label, final String description, + final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction, final ValueReducer reducer) { + super(ILLEGAL_INDEXABLE_METRIC, field, label, description, formatter, valueFunction, reducer); + } + + @Override + public boolean isCounter() { + return true; + } + + @Override + public String toString() { + return "StandardMetricDescriptor[" + getLabel() + "]"; + } + + @Override + public int hashCode() { + return 239891 + getFormatter().name().hashCode() + 4 * getLabel().hashCode() + 8 * getField().hashCode() + 28 * getDescription().hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CounterMetricDescriptor)) { + return false; + } + + MetricDescriptor other = (MetricDescriptor) obj; + return getField().equals(other.getField()); + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardMetricDescriptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardMetricDescriptor.java index de5e0b55e19c..1b6f65f9ec41 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardMetricDescriptor.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardMetricDescriptor.java @@ -16,77 +16,31 @@ */ package org.apache.nifi.controller.status.history; -import java.util.List; - -public class StandardMetricDescriptor implements MetricDescriptor { - - private final IndexableMetric indexableMetric; - private final String field; - private final String label; - private final String description; - private final MetricDescriptor.Formatter formatter; - private final ValueMapper valueMapper; - private final ValueReducer reducer; +public class StandardMetricDescriptor extends AbstractMetricDescriptor { public StandardMetricDescriptor(final IndexableMetric indexableMetric, final String field, final String label, final String description, final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction) { - this(indexableMetric, field, label, description, formatter, valueFunction, null); + super(indexableMetric, field, label, description, formatter, valueFunction); } public StandardMetricDescriptor(final IndexableMetric indexableMetric, final String field, final String label, final String description, final MetricDescriptor.Formatter formatter, final ValueMapper valueFunction, final ValueReducer reducer) { - this.indexableMetric = indexableMetric; - this.field = field; - this.label = label; - this.description = description; - this.formatter = formatter; - this.valueMapper = valueFunction; - this.reducer = reducer == null ? new SumReducer() : reducer; - } - - @Override - public int getMetricIdentifier() { - return indexableMetric.getIndex(); - } - - @Override - public String getField() { - return field; - } - - @Override - public String getLabel() { - return label; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public MetricDescriptor.Formatter getFormatter() { - return formatter; - } - - @Override - public ValueMapper getValueFunction() { - return valueMapper; + super(indexableMetric, field, label, description, formatter, valueFunction, reducer); } @Override - public ValueReducer getValueReducer() { - return reducer; + public boolean isCounter() { + return false; } @Override public String toString() { - return "StandardMetricDescriptor[" + label + "]"; + return "StandardMetricDescriptor[" + getLabel() + "]"; } @Override public int hashCode() { - return 23987 + formatter.name().hashCode() + 4 * label.hashCode() + 8 * field.hashCode() + 28 * description.hashCode(); + return 23987 + getFormatter().name().hashCode() + 4 * getLabel().hashCode() + 8 * getField().hashCode() + 28 * getDescription().hashCode(); } @Override @@ -99,19 +53,7 @@ public boolean equals(final Object obj) { } MetricDescriptor other = (MetricDescriptor) obj; - return field.equals(other.getField()); + return getField().equals(other.getField()); } - class SumReducer implements ValueReducer { - - @Override - public Long reduce(final List values) { - long sum = 0; - for (final StatusSnapshot snapshot : values) { - sum += snapshot.getStatusMetric(StandardMetricDescriptor.this); - } - - return sum; - } - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardStatusSnapshot.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardStatusSnapshot.java index fb9112b55caf..8e78c5ea01e9 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardStatusSnapshot.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/StandardStatusSnapshot.java @@ -65,7 +65,11 @@ public Set> getMetricDescriptors() { @Override public Long getStatusMetric(final MetricDescriptor descriptor) { - return values[descriptor.getMetricIdentifier()]; + if (descriptor.isCounter()) { + return counterValues.get(descriptor); + } else { + return values[descriptor.getMetricIdentifier()]; + } } public void setTimestamp(final Date timestamp) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ConnectableTask.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ConnectableTask.java index e68aba8e8b30..5a49c729d887 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ConnectableTask.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ConnectableTask.java @@ -19,10 +19,12 @@ import org.apache.nifi.components.state.StateManager; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.ConnectableType; +import org.apache.nifi.connectable.Connection; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.ProcessorNode; import org.apache.nifi.controller.ScheduledState; import org.apache.nifi.controller.lifecycle.TaskTerminationAwareStateManager; +import org.apache.nifi.controller.queue.FlowFileQueue; import org.apache.nifi.controller.repository.ActiveProcessSessionFactory; import org.apache.nifi.controller.repository.BatchingSessionFactory; import org.apache.nifi.controller.repository.RepositoryContext; @@ -80,7 +82,7 @@ public ConnectableTask(final SchedulingAgent schedulingAgent, final Connectable final StateManager stateManager = new TaskTerminationAwareStateManager(flowController.getStateManagerProvider().getStateManager(connectable.getIdentifier()), scheduleState::isTerminated); if (connectable instanceof ProcessorNode) { - processContext = new StandardProcessContext((ProcessorNode) connectable, flowController, encryptor, stateManager, scheduleState::isTerminated); + processContext = new StandardProcessContext((ProcessorNode) connectable, flowController.getControllerServiceProvider(), encryptor, stateManager, scheduleState::isTerminated); } else { processContext = new ConnectableProcessContext(connectable, encryptor, stateManager); } @@ -142,8 +144,8 @@ private boolean isWorkToDo() { private boolean isBackPressureEngaged() { return connectable.getIncomingConnections().stream() .filter(con -> con.getSource() == connectable) - .map(con -> con.getFlowFileQueue()) - .anyMatch(queue -> queue.isFull()); + .map(Connection::getFlowFileQueue) + .anyMatch(FlowFileQueue::isFull); } public InvocationResult invoke() { @@ -197,7 +199,7 @@ public InvocationResult invoke() { final String originalThreadName = Thread.currentThread().getName(); try { - try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(connectable.getRunnableComponent().getClass(), connectable.getIdentifier())) { + try (final AutoCloseable ncl = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), connectable.getRunnableComponent().getClass(), connectable.getIdentifier())) { boolean shouldRun = connectable.getScheduledState() == ScheduledState.RUNNING; while (shouldRun) { connectable.onTrigger(processContext, activeSessionFactory); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ExpireFlowFiles.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ExpireFlowFiles.java index 6e2ee4c3be5e..0c8e8a9e7fb2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ExpireFlowFiles.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ExpireFlowFiles.java @@ -16,9 +16,6 @@ */ package org.apache.nifi.controller.tasks; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.Connection; import org.apache.nifi.connectable.Funnel; @@ -35,6 +32,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + /** * This task runs through all Connectable Components and goes through its incoming queues, polling for FlowFiles and accepting none. This causes the desired side effect of expiring old FlowFiles. */ @@ -51,7 +51,7 @@ public ExpireFlowFiles(final FlowController flowController, final RepositoryCont @Override public void run() { - final ProcessGroup rootGroup = flowController.getGroup(flowController.getRootGroupId()); + final ProcessGroup rootGroup = flowController.getFlowManager().getRootGroup(); try { expireFlowFiles(rootGroup); } catch (final Exception e) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ReportingTaskWrapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ReportingTaskWrapper.java index b05321fe2984..b60302ab37ed 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ReportingTaskWrapper.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/tasks/ReportingTaskWrapper.java @@ -20,6 +20,7 @@ import org.apache.nifi.controller.ReportingTaskNode; import org.apache.nifi.controller.scheduling.LifecycleState; import org.apache.nifi.logging.ComponentLog; +import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.processor.SimpleProcessLogger; import org.apache.nifi.util.ReflectionUtils; @@ -28,16 +29,18 @@ public class ReportingTaskWrapper implements Runnable { private final ReportingTaskNode taskNode; private final LifecycleState lifecycleState; + private final ExtensionManager extensionManager; - public ReportingTaskWrapper(final ReportingTaskNode taskNode, final LifecycleState lifecycleState) { + public ReportingTaskWrapper(final ReportingTaskNode taskNode, final LifecycleState lifecycleState, final ExtensionManager extensionManager) { this.taskNode = taskNode; this.lifecycleState = lifecycleState; + this.extensionManager = extensionManager; } @Override public synchronized void run() { lifecycleState.incrementActiveThreadCount(null); - try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { + try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { taskNode.getReportingTask().onTrigger(taskNode.getReportingContext()); } catch (final Throwable t) { final ComponentLog componentLog = new SimpleProcessLogger(taskNode.getIdentifier(), taskNode.getReportingTask()); @@ -50,7 +53,7 @@ public synchronized void run() { // if the reporting task is no longer scheduled to run and this is the last thread, // invoke the OnStopped methods if (!lifecycleState.isScheduled() && lifecycleState.getActiveThreadCount() == 1 && lifecycleState.mustCallOnStoppedMethods()) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, taskNode.getReportingTask().getClass(), taskNode.getIdentifier())) { ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnStopped.class, taskNode.getReportingTask(), taskNode.getConfigurationContext()); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java index 7da570277b7b..999a42d65749 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java @@ -84,11 +84,13 @@ public class FingerprintFactory { private static final String ENCRYPTED_VALUE_SUFFIX = "}"; private final StringEncryptor encryptor; private final DocumentBuilder flowConfigDocBuilder; + private final ExtensionManager extensionManager; private static final Logger logger = LoggerFactory.getLogger(FingerprintFactory.class); - public FingerprintFactory(final StringEncryptor encryptor) { + public FingerprintFactory(final StringEncryptor encryptor, final ExtensionManager extensionManager) { this.encryptor = encryptor; + this.extensionManager = extensionManager; final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); @@ -108,9 +110,10 @@ public FingerprintFactory(final StringEncryptor encryptor) { } } - public FingerprintFactory(final StringEncryptor encryptor, final DocumentBuilder docBuilder) { + public FingerprintFactory(final StringEncryptor encryptor, final DocumentBuilder docBuilder, final ExtensionManager extensionManager) { this.encryptor = encryptor; this.flowConfigDocBuilder = docBuilder; + this.extensionManager = extensionManager; } /** @@ -408,7 +411,7 @@ private StringBuilder addFlowFileProcessorFingerprint(final StringBuilder builde // get the temp instance of the Processor so that we know the default property values final BundleCoordinate coordinate = getCoordinate(className, bundle); - final ConfigurableComponent configurableComponent = ExtensionManager.getTempComponent(className, coordinate); + final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(className, coordinate); if (configurableComponent == null) { logger.warn("Unable to get Processor of type {}; its default properties will be fingerprinted instead of being ignored.", className); } @@ -640,7 +643,7 @@ private void addControllerServiceFingerprint(final StringBuilder builder, final // get the temp instance of the ControllerService so that we know the default property values final BundleCoordinate coordinate = getCoordinate(dto.getType(), dto.getBundle()); - final ConfigurableComponent configurableComponent = ExtensionManager.getTempComponent(dto.getType(), coordinate); + final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(dto.getType(), coordinate); if (configurableComponent == null) { logger.warn("Unable to get ControllerService of type {}; its default properties will be fingerprinted instead of being ignored.", dto.getType()); } @@ -672,7 +675,7 @@ private void addBundleFingerprint(final StringBuilder builder, final BundleDTO b private BundleCoordinate getCoordinate(final String type, final BundleDTO dto) { BundleCoordinate coordinate; try { - coordinate = BundleUtils.getCompatibleBundle(type, dto); + coordinate = BundleUtils.getCompatibleBundle(extensionManager, type, dto); } catch (final IllegalStateException e) { if (dto == null) { coordinate = BundleCoordinate.UNKNOWN_COORDINATE; @@ -697,7 +700,7 @@ private void addReportingTaskFingerprint(final StringBuilder builder, final Repo // get the temp instance of the ReportingTask so that we know the default property values final BundleCoordinate coordinate = getCoordinate(dto.getType(), dto.getBundle()); - final ConfigurableComponent configurableComponent = ExtensionManager.getTempComponent(dto.getType(), coordinate); + final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(dto.getType(), coordinate); if (configurableComponent == null) { logger.warn("Unable to get ReportingTask of type {}; its default properties will be fingerprinted instead of being ignored.", dto.getType()); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java index 2fee76402197..61a902afa8c3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java @@ -52,6 +52,7 @@ import org.apache.nifi.controller.Template; import org.apache.nifi.controller.exception.ComponentLifeCycleException; import org.apache.nifi.controller.exception.ProcessorInstantiationException; +import org.apache.nifi.controller.flow.FlowManager; import org.apache.nifi.controller.label.Label; import org.apache.nifi.controller.queue.FlowFileQueue; import org.apache.nifi.controller.queue.LoadBalanceCompression; @@ -64,8 +65,8 @@ import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.flowfile.FlowFilePrioritizer; import org.apache.nifi.logging.LogLevel; +import org.apache.nifi.logging.LogRepository; import org.apache.nifi.logging.LogRepositoryFactory; -import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.StandardProcessContext; @@ -161,6 +162,7 @@ public final class StandardProcessGroup implements ProcessGroup { private final StandardProcessScheduler scheduler; private final ControllerServiceProvider controllerServiceProvider; private final FlowController flowController; + private final FlowManager flowManager; private final Map inputPorts = new HashMap<>(); private final Map outputPorts = new HashMap<>(); @@ -193,6 +195,7 @@ public StandardProcessGroup(final String id, final ControllerServiceProvider ser this.encryptor = encryptor; this.flowController = flowController; this.variableRegistry = variableRegistry; + this.flowManager = flowController.getFlowManager(); name = new AtomicReference<>(); position = new AtomicReference<>(new Position(0D, 0D)); @@ -423,9 +426,7 @@ public void startProcessing() { } }); - findAllInputPorts().stream().filter(START_PORTS_FILTER).forEach(port -> { - port.getProcessGroup().startInputPort(port); - }); + findAllInputPorts().stream().filter(START_PORTS_FILTER).forEach(port -> port.getProcessGroup().startInputPort(port)); findAllOutputPorts().stream().filter(START_PORTS_FILTER).forEach(port -> { port.getProcessGroup().startOutputPort(port); @@ -447,13 +448,8 @@ public void stopProcessing() { } }); - findAllInputPorts().stream().filter(STOP_PORTS_FILTER).forEach(port -> { - port.getProcessGroup().stopInputPort(port); - }); - - findAllOutputPorts().stream().filter(STOP_PORTS_FILTER).forEach(port -> { - port.getProcessGroup().stopOutputPort(port); - }); + findAllInputPorts().stream().filter(STOP_PORTS_FILTER).forEach(port -> port.getProcessGroup().stopInputPort(port)); + findAllOutputPorts().stream().filter(STOP_PORTS_FILTER).forEach(port -> port.getProcessGroup().stopOutputPort(port)); } finally { readLock.unlock(); } @@ -465,7 +461,7 @@ private StateManager getStateManager(final String componentId) { private void shutdown(final ProcessGroup procGroup) { for (final ProcessorNode node : procGroup.getProcessors()) { - try (final NarCloseable x = NarCloseable.withComponentNarLoader(node.getProcessor().getClass(), node.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), node.getProcessor().getClass(), node.getIdentifier())) { final StandardProcessContext processContext = new StandardProcessContext(node, controllerServiceProvider, encryptor, getStateManager(node.getIdentifier()), () -> false); ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnShutdown.class, node.getProcessor(), processContext); } @@ -514,7 +510,7 @@ public void addInputPort(final Port port) { port.setProcessGroup(this); inputPorts.put(requireNonNull(port).getIdentifier(), port); - flowController.onInputPortAdded(port); + flowManager.onInputPortAdded(port); onComponentModified(); } finally { writeLock.unlock(); @@ -553,7 +549,7 @@ public void removeInputPort(final Port port) { scheduler.onPortRemoved(port); onComponentModified(); - flowController.onInputPortRemoved(port); + flowManager.onInputPortRemoved(port); LOG.info("Input Port {} removed from flow", port); } finally { writeLock.unlock(); @@ -599,7 +595,7 @@ public void addOutputPort(final Port port) { port.setProcessGroup(this); outputPorts.put(port.getIdentifier(), port); - flowController.onOutputPortAdded(port); + flowManager.onOutputPortAdded(port); onComponentModified(); } finally { writeLock.unlock(); @@ -629,7 +625,7 @@ public void removeOutputPort(final Port port) { scheduler.onPortRemoved(port); onComponentModified(); - flowController.onOutputPortRemoved(port); + flowManager.onOutputPortRemoved(port); LOG.info("Output Port {} removed from flow", port); } finally { writeLock.unlock(); @@ -668,10 +664,10 @@ public void addProcessGroup(final ProcessGroup group) { group.getVariableRegistry().setParent(getVariableRegistry()); processGroups.put(Objects.requireNonNull(group).getIdentifier(), group); - flowController.onProcessGroupAdded(group); + flowManager.onProcessGroupAdded(group); - group.findAllControllerServices().stream().forEach(this::updateControllerServiceReferences); - group.findAllProcessors().stream().forEach(this::updateControllerServiceReferences); + group.findAllControllerServices().forEach(this::updateControllerServiceReferences); + group.findAllProcessors().forEach(this::updateControllerServiceReferences); onComponentModified(); } finally { @@ -715,7 +711,7 @@ public void removeProcessGroup(final ProcessGroup group) { processGroups.remove(group.getIdentifier()); onComponentModified(); - flowController.onProcessGroupRemoved(group); + flowManager.onProcessGroupRemoved(group); LOG.info("{} removed from flow", group); } finally { writeLock.unlock(); @@ -753,7 +749,7 @@ private void removeComponents(final ProcessGroup group) { for (final ControllerServiceNode cs : group.getControllerServices(false)) { // Must go through Controller Service here because we need to ensure that it is removed from the cache - flowController.removeControllerService(cs); + flowController.getControllerServiceProvider().removeControllerService(cs); } for (final ProcessGroup childGroup : new ArrayList<>(group.getProcessGroups())) { @@ -821,8 +817,8 @@ public void removeRemoteProcessGroup(final RemoteProcessGroup remoteProcessGroup LOG.warn("Failed to clean up resources for {} due to {}", remoteGroup, e); } - remoteGroup.getInputPorts().stream().forEach(scheduler::onPortRemoved); - remoteGroup.getOutputPorts().stream().forEach(scheduler::onPortRemoved); + remoteGroup.getInputPorts().forEach(scheduler::onPortRemoved); + remoteGroup.getOutputPorts().forEach(scheduler::onPortRemoved); remoteGroups.remove(remoteGroupId); LOG.info("{} removed from flow", remoteProcessGroup); @@ -844,7 +840,7 @@ public void addProcessor(final ProcessorNode processor) { processor.setProcessGroup(this); processor.getVariableRegistry().setParent(getVariableRegistry()); processors.put(processorId, processor); - flowController.onProcessorAdded(processor); + flowManager.onProcessorAdded(processor); updateControllerServiceReferences(processor); onComponentModified(); } finally { @@ -903,7 +899,7 @@ public void removeProcessor(final ProcessorNode processor) { conn.verifyCanDelete(); } - try (final NarCloseable x = NarCloseable.withComponentNarLoader(processor.getProcessor().getClass(), processor.getIdentifier())) { + try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), processor.getProcessor().getClass(), processor.getIdentifier())) { final StandardProcessContext processContext = new StandardProcessContext(processor, controllerServiceProvider, encryptor, getStateManager(processor.getIdentifier()), () -> false); ReflectionUtils.quietlyInvokeMethodsWithAnnotation(OnRemoved.class, processor.getProcessor(), processContext); } catch (final Exception e) { @@ -927,9 +923,12 @@ public void removeProcessor(final ProcessorNode processor) { onComponentModified(); scheduler.onProcessorRemoved(processor); - flowController.onProcessorRemoved(processor); + flowManager.onProcessorRemoved(processor); - LogRepositoryFactory.getRepository(processor.getIdentifier()).removeAllObservers(); + final LogRepository logRepository = LogRepositoryFactory.getRepository(processor.getIdentifier()); + if (logRepository != null) { + logRepository.removeAllObservers(); + } final StateManagerProvider stateManagerProvider = flowController.getStateManagerProvider(); scheduler.submitFrameworkTask(new Runnable() { @@ -951,7 +950,7 @@ public void run() { if (removed) { try { LogRepositoryFactory.removeRepository(processor.getIdentifier()); - ExtensionManager.removeInstanceClassLoader(id); + flowController.getExtensionManager().removeInstanceClassLoader(id); } catch (Throwable t) { } } @@ -1069,7 +1068,7 @@ public void addConnection(final Connection connection) { destination.addConnection(connection); } connections.put(connection.getIdentifier(), connection); - flowController.onConnectionAdded(connection); + flowManager.onConnectionAdded(connection); onComponentModified(); } finally { writeLock.unlock(); @@ -1134,7 +1133,7 @@ public void removeConnection(final Connection connectionToRemove) { LOG.info("{} removed from flow", connection); onComponentModified(); - flowController.onConnectionRemoved(connection); + flowManager.onConnectionRemoved(connection); } finally { writeLock.unlock(); } @@ -1162,7 +1161,7 @@ public Connection getConnection(final String id) { @Override public Connection findConnection(final String id) { - final Connection connection = flowController.getConnection(id); + final Connection connection = flowManager.getConnection(id); if (connection == null) { return null; } @@ -1602,12 +1601,12 @@ public ProcessGroup findProcessGroup(final String id) { return this; } - final ProcessGroup group = flowController.getGroup(id); + final ProcessGroup group = flowManager.getGroup(id); if (group == null) { return null; } - // We found a Processor in the Controller, but we only want to return it if + // We found a Process Group in the Controller, but we only want to return it if // the Process Group is this or is a child of this. if (isOwner(group.getParent())) { return group; @@ -1665,7 +1664,7 @@ private RemoteProcessGroup findRemoteProcessGroup(final String id, final Process @Override public ProcessorNode findProcessor(final String id) { - final ProcessorNode node = flowController.getProcessorNode(id); + final ProcessorNode node = flowManager.getProcessorNode(id); if (node == null) { return null; } @@ -1770,7 +1769,7 @@ private List

$8>$l?jnizC2Hn3@QWsV4I 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%0D}Pa?5#_2h~RTW{a^@p7#^-F97# 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/load_balance_distributed_connection.png b/nifi-docs/src/main/asciidoc/images/load_balance_distributed_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..3b352ffb24bc1f5e306493ee5351d8d866c37dc0 GIT binary patch literal 12577 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/offloading-node-cluster-mgt.png b/nifi-docs/src/main/asciidoc/images/offloading-node-cluster-mgt.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf3d44bb7dccadc36b84010938141f954fa2fb7 GIT binary patch literal 91042 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 literal 0 HcmV?d00001 diff --git a/nifi-docs/src/main/asciidoc/images/primary-node-cluster-mgt.png b/nifi-docs/src/main/asciidoc/images/primary-node-cluster-mgt.png new file mode 100644 index 0000000000000000000000000000000000000000..26ea3f7981adc3c4c2a38e83ce2e0e841c1194f1 GIT binary patch literal 96308 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