NIFI-13732 Initial commit of nifi-api 2.0.0-SNAPSHOT
diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..6022a38
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: Apache-2.0
+
+github:
+  description: "Apache NiFi API"
+  homepage: https://nifi.apache.org
+  labels:
+    - apache
+    - nifi
+  features:
+    wiki: false
+    issues: false
+    projects: false
+  enabled_merge_buttons:
+    squash: true
+  autolink_jira:
+    - NIFI
+    - NIP
+  protected_branches:
+    main:
+      required_signatures: true
+      required_linear_history: true
+notifications:
+  commits: commits@nifi.apache.org
+  issues: issues@nifi.apache.org
+  pullrequests: issues@nifi.apache.org
+  jira_options: link worklog
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..2a1ef8e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+
+# Summary
+
+[NIFI-00000](https://issues.apache.org/jira/browse/NIFI-00000)
+
+# Tracking
+
+Please complete the following tracking steps prior to pull request creation.
+
+### Issue Tracking
+
+- [ ] [Apache NiFi Jira](https://issues.apache.org/jira/browse/NIFI) issue created
+
+### Pull Request Tracking
+
+- [ ] Pull Request title starts with Apache NiFi Jira issue number, such as `NIFI-00000`
+- [ ] Pull Request commit message starts with Apache NiFi Jira issue number, as such `NIFI-00000`
+
+### Pull Request Formatting
+
+- [ ] Pull Request based on current revision of the `main` branch
+- [ ] Pull Request refers to a feature branch with one commit containing changes
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..b9f7518
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: Apache-2.0
+
+name: build
+
+on:
+  push:
+  pull_request:
+    branches:
+      - main
+
+permissions:
+  contents: read
+
+jobs:
+  build-java-21:
+    name: Java 21
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Sources
+        uses: actions/checkout@v4
+      - name: Setup Java JDK
+        uses: actions/setup-java@v4
+        with:
+          java-version: '21'
+          distribution: 'liberica'
+          cache: 'maven'
+      - name: Build
+        run: ./mvnw --batch-mode verify
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c1606a4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: Apache-2.0
+
+# Editors
+.idea/
+.vscode/
+.java-version
+.project
+.settings
+.classpath
+nbactions.xml
+nb-configuration.xml
+.DS_Store
+.metadata
+.recommenders
+*.iml
+*.iws
+*.ipr
+*~
+
+# Binaries
+*.class
+target/
+
+# Packages
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# Logs
+*.log
+hs_err_pid*
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..d58dfb7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# 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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..bc8e448
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache NiFi API
+Copyright 2024 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7a4ae23
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+
+# Apache NiFi API
+
+[![license](https://img.shields.io/github/license/apache/nifi-api)](https://github.com/apache/nifi-api/blob/main/LICENSE)
+[![build](https://github.com/apache/nifi-api/actions/workflows/build.yml/badge.svg)](https://github.com/apache/nifi-api/actions/workflows/build.yml)
+
+The [Apache NiFi](https://nifi.apache.org) API repository contains public interfaces and classes for building extension components.
+
+## Requirements
+
+- Java 21
+- Maven 3.9
+
+## Versioning
+
+The project follows [Semantic Versioning 2.0.0](https://semver.org).
+
+## Building
+
+This project uses [Maven](https://maven.apache.org) to build distribution packages.
+
+This project includes the [Maven Wrapper](https://maven.apache.org/wrapper/) to build with required Maven versions.
+
+```
+./mvnw install
+```
+
+## Verifying
+
+Code and documentation changes must pass build verification to be eligible for review.
+
+```
+./mvnw verify
+```
+
+The project uses several build plugins for static code analysis and license evaluation.
+
+- [Apache Maven Checkstyle Plugin](https://maven.apache.org/plugins/maven-checkstyle-plugin/)
+- [Apache Maven PMD Plugin](https://maven.apache.org/plugins/maven-pmd-plugin/)
+- [Apache Release Audit Tool Plugin](https://creadur.apache.org/rat/apache-rat-plugin/)
+- [JaCoCo Maven Plugin](https://www.jacoco.org/jacoco/trunk/doc/maven.html)
+
+## Documentation
+
+The [Apache NiFi Documentation](https://nifi.apache.org/documentation/) includes reference information for project capabilities.
+
+## Contributing
+
+The [Apache NiFi Contributor Guide](https://cwiki.apache.org/confluence/display/NIFI/Contributor+Guide)
+describes the process for getting involved in the development of this project.
+
+## Issues
+
+This project uses [Jira](https://issues.apache.org/jira/browse/NIFI) for tracking bugs and features.
+
+## Licensing
+
+This project is released under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 0000000..7c87426
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
+<module name="Checker">
+    <module name="NewlineAtEndOfFile"/>
+    <module name="FileLength"/>
+    <module name="LineLength">
+        <property name="max" value="200"/>
+        <property name="fileExtensions" value="java"/>
+    </module>
+    <module name="FileTabCharacter"/>
+    <module name="RegexpSingleline">
+        <property name="format" value="\s+$"/>
+        <property name="minimum" value="0"/>
+        <property name="maximum" value="0"/>
+        <property name="message" value="Line has trailing spaces."/>
+    </module>
+    <module name="TreeWalker">
+        <module name="MethodName"/>
+        <module name="HideUtilityClassConstructor"/>
+        <module name="InvalidJavadocPosition"/>
+        <module name="JavadocMethod"/>
+        <module name="JavadocType"/>
+        <module name="LocalFinalVariableName"/>
+        <module name="LocalVariableName"/>
+        <module name="MemberName"/>
+        <module name="PackageName"/>
+        <module name="ParameterName"/>
+        <module name="StaticVariableName"/>
+        <module name="TypeName"/>
+        <module name="AvoidStarImport"/>
+        <module name="IllegalImport"/>
+        <module name="RedundantImport"/>
+        <module name="UnusedImports"/>
+        <module name="MethodLength">
+            <property name="max" value="250"/>
+        </module>
+        <module name="EmptyForIteratorPad"/>
+        <module name="GenericWhitespace"/>
+        <module name="MethodParamPad"/>
+        <module name="NoWhitespaceAfter"/>
+        <module name="NoWhitespaceBefore"/>
+        <module name="ParenPad"/>
+        <module name="TypecastParenPad"/>
+        <module name="WhitespaceAfter"/>
+        <module name="WhitespaceAround"/>
+        <module name="ModifierOrder"/>
+        <module name="AvoidNestedBlocks"/>
+        <module name="EmptyBlock"/>
+        <module name="LeftCurly"/>
+        <module name="NeedBraces"/>
+        <module name="RightCurly"/>
+        <module name="EmptyStatement"/>
+        <module name="EqualsHashCode"/>
+        <module name="IllegalInstantiation"/>
+        <module name="InnerAssignment"/>
+        <module name="MissingSwitchDefault"/>
+        <module name="MultipleVariableDeclarations"/>
+        <module name="SimplifyBooleanExpression"/>
+        <module name="SimplifyBooleanReturn"/>
+        <module name="InterfaceIsType"/>
+        <module name="ArrayTypeStyle"/>
+        <module name="TodoComment"/>
+        <module name="UpperEll"/>
+        <module name="UnusedLocalVariable"/>
+    </module>
+</module>
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..19529dd
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# 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 Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+#   JAVA_HOME - location of a JDK home dir, required when download maven via java source
+#   MVNW_REPOURL - repo url base for downloading maven distribution
+#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+  [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+  native_path() { cygpath --path --windows "$1"; }
+  ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+  if [ -n "${JAVA_HOME-}" ]; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+      JAVACCMD="$JAVA_HOME/jre/sh/javac"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+      JAVACCMD="$JAVA_HOME/bin/javac"
+
+      if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+        echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+        echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+        return 1
+      fi
+    fi
+  else
+    JAVACMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v java
+    )" || :
+    JAVACCMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v javac
+    )" || :
+
+    if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+      echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+      return 1
+    fi
+  fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+  str="${1:-}" h=0
+  while [ -n "$str" ]; do
+    char="${str%"${str#?}"}"
+    h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+    str="${str#?}"
+  done
+  printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+  printf %s\\n "$1" >&2
+  exit 1
+}
+
+trim() {
+  # MWRAPPER-139:
+  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+  #   Needed for removing poorly interpreted newline sequences when running in more
+  #   exotic environments such as mingw bash on Windows.
+  printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+  case "${key-}" in
+  distributionUrl) distributionUrl=$(trim "${value-}") ;;
+  distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+  esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+  case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+  *)
+    echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+    distributionPlatform=linux-amd64
+    ;;
+  esac
+  distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+  ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+  exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+  verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+  clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+  trap clean HUP INT TERM EXIT
+else
+  die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+  distributionUrl="${distributionUrl%.zip}.tar.gz"
+  distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+  verbose "Found wget ... using wget"
+  wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+  verbose "Found curl ... using curl"
+  curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+  verbose "Falling back to use Java to download"
+  javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+  targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+  cat >"$javaSource" <<-END
+	public class Downloader extends java.net.Authenticator
+	{
+	  protected java.net.PasswordAuthentication getPasswordAuthentication()
+	  {
+	    return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+	  }
+	  public static void main( String[] args ) throws Exception
+	  {
+	    setDefault( new Downloader() );
+	    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+	  }
+	}
+	END
+  # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+  verbose " - Compiling Downloader.java ..."
+  "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+  verbose " - Running Downloader.java ..."
+  "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+  distributionSha256Result=false
+  if [ "$MVN_CMD" = mvnd.sh ]; then
+    echo "Checksum validation is not supported for maven-mvnd." >&2
+    echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  elif command -v sha256sum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  elif command -v shasum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  else
+    echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+    echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  fi
+  if [ $distributionSha256Result = false ]; then
+    echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+    echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+    exit 1
+  fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+  unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+  tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..249bdf3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c6eeb46
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,281 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache</groupId>
+        <artifactId>apache</artifactId>
+        <version>33</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>org.apache.nifi</groupId>
+    <artifactId>nifi-api</artifactId>
+    <version>2.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>nifi-api</name>
+    <description>Apache NiFi API</description>
+    <url>https://nifi.apache.org</url>
+    <inceptionYear>2014</inceptionYear>
+    <organization>
+        <name>Apache NiFi Project</name>
+        <url>https://nifi.apache.org</url>
+    </organization>
+    <licenses>
+        <license>
+            <name>Apache License 2.0</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0</url>
+            <distribution>repo</distribution>
+        </license>
+    </licenses>
+    <developers>
+        <developer>
+            <name>Apache NiFi Developers</name>
+            <email>dev@nifi.apache.org</email>
+            <organization>Apache NiFi Project</organization>
+            <organizationUrl>https://nifi.apache.org</organizationUrl>
+        </developer>
+    </developers>
+    <mailingLists>
+        <mailingList>
+            <name>Developers</name>
+            <subscribe>dev-subscribe@nifi.apache.org</subscribe>
+            <unsubscribe>dev-unsubscribe@nifi.apache.org</unsubscribe>
+            <post>dev@nifi.apache.org</post>
+            <archive>https://lists.apache.org/list.html?dev@nifi.apache.org</archive>
+        </mailingList>
+        <mailingList>
+            <name>Users</name>
+            <subscribe>users-subscribe@nifi.apache.org</subscribe>
+            <unsubscribe>users-unsubscribe@nifi.apache.org</unsubscribe>
+            <post>users@nifi.apache.org</post>
+            <archive>https://lists.apache.org/list.html?users@nifi.apache.org</archive>
+        </mailingList>
+        <mailingList>
+            <name>Commits</name>
+            <subscribe>commits-subscribe@nifi.apache.org</subscribe>
+            <unsubscribe>commits-unsubscribe@nifi.apache.org</unsubscribe>
+            <post>commits@nifi.apache.org</post>
+            <archive>https://lists.apache.org/list.html?commits@nifi.apache.org</archive>
+        </mailingList>
+    </mailingLists>
+    <scm>
+        <connection>scm:git:https://github.com/apache/nifi-api</connection>
+        <developerConnection>scm:git:https://github.com/apache/nifi-api</developerConnection>
+        <url>https://github.com/apache/nifi-api</url>
+    </scm>
+    <issueManagement>
+        <system>jira</system>
+        <url>https://issues.apache.org/jira/browse/NIFI</url>
+    </issueManagement>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <maven.compiler.release>21</maven.compiler.release>
+        <maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
+        <minimalJavaBuildVersion>21</minimalJavaBuildVersion>
+        <minimalMavenBuildVersion>3.9.9</minimalMavenBuildVersion>
+
+        <checkstyle.version>10.18.1</checkstyle.version>
+        <junit-bom.version>5.11.0</junit-bom.version>
+        <jacoco.version>0.8.12</jacoco.version>
+        <maven-pmd-plugin.version>3.25.0</maven-pmd-plugin.version>
+        <mockito.version>5.13.0</mockito.version>
+        <pmd.version>7.5.0</pmd.version>
+        <swagger-annotations.version>2.2.23</swagger-annotations.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.junit</groupId>
+                <artifactId>junit-bom</artifactId>
+                <version>${junit-bom.version}</version>
+                <scope>import</scope>
+                <type>pom</type>
+            </dependency>
+            <dependency>
+                <groupId>org.mockito</groupId>
+                <artifactId>mockito-bom</artifactId>
+                <version>${mockito.version}</version>
+                <scope>import</scope>
+                <type>pom</type>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- Optional dependency for generating OpenAPI Specification -->
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>${swagger-annotations.version}</version>
+            <scope>compile</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.jacoco</groupId>
+                    <artifactId>jacoco-maven-plugin</artifactId>
+                    <version>${jacoco.version}</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-checkstyle-plugin</artifactId>
+                    <version>${version.maven-checkstyle-plugin}</version>
+                    <dependencies>
+                        <dependency>
+                            <groupId>com.puppycrawl.tools</groupId>
+                            <artifactId>checkstyle</artifactId>
+                            <version>${checkstyle.version}</version>
+                        </dependency>
+                    </dependencies>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-pmd-plugin</artifactId>
+                    <version>${maven-pmd-plugin.version}</version>
+                    <dependencies>
+                        <dependency>
+                            <groupId>net.sourceforge.pmd</groupId>
+                            <artifactId>pmd-core</artifactId>
+                            <version>${pmd.version}</version>
+                        </dependency>
+                        <dependency>
+                            <groupId>net.sourceforge.pmd</groupId>
+                            <artifactId>pmd-java</artifactId>
+                            <version>${pmd.version}</version>
+                        </dependency>
+                    </dependencies>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <useIdeaDefaultExcludes>true</useIdeaDefaultExcludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                        <phase>verify</phase>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <configuration>
+                    <configLocation>checkstyle.xml</configLocation>
+                    <includeTestSourceDirectory>true</includeTestSourceDirectory>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>check</id>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <skipIfEmpty>true</skipIfEmpty>
+                    <archive>
+                        <manifest>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                        </manifest>
+                        <manifestEntries>
+                            <Automatic-Module-Name>${project.groupId}</Automatic-Module-Name>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <quiet>true</quiet>
+                    <failOnError>true</failOnError>
+                    <failOnWarnings>true</failOnWarnings>
+                    <doclint>all,-missing</doclint>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-pmd-plugin</artifactId>
+                <configuration>
+                    <printFailingErrors>true</printFailingErrors>
+                    <linkXRef>false</linkXRef>
+                    <targetJdk>${maven.compiler.release}</targetJdk>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>check</id>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>prepare-agent</id>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>report</id>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/src/main/java/org/apache/nifi/action/Action.java b/src/main/java/org/apache/nifi/action/Action.java
new file mode 100644
index 0000000..44c28a6
--- /dev/null
+++ b/src/main/java/org/apache/nifi/action/Action.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.action;
+
+import java.io.Serializable;
+import java.util.Date;
+import org.apache.nifi.action.component.details.ComponentDetails;
+import org.apache.nifi.action.details.ActionDetails;
+
+/**
+ * An action taken on the flow by a user.
+ */
+public interface Action extends Serializable {
+
+    Integer getId();
+
+    Date getTimestamp();
+
+    String getUserIdentity();
+
+    String getSourceId();
+
+    String getSourceName();
+
+    Component getSourceType();
+
+    ComponentDetails getComponentDetails();
+
+    Operation getOperation();
+
+    ActionDetails getActionDetails();
+
+}
diff --git a/src/main/java/org/apache/nifi/action/Component.java b/src/main/java/org/apache/nifi/action/Component.java
new file mode 100644
index 0000000..9a65185
--- /dev/null
+++ b/src/main/java/org/apache/nifi/action/Component.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.action;
+
+/**
+ * Defines possible components for a given action.
+ */
+public enum Component {
+
+    Controller,
+    Processor,
+    InputPort,
+    OutputPort,
+    ProcessGroup,
+    RemoteProcessGroup,
+    Funnel,
+    Connection,
+    ControllerService,
+    ReportingTask,
+    FlowAnalysisRule,
+    FlowRegistryClient,
+    ParameterContext,
+    ParameterProvider,
+    AccessPolicy,
+    User,
+    UserGroup,
+    Label;
+}
diff --git a/src/main/java/org/apache/nifi/action/Operation.java b/src/main/java/org/apache/nifi/action/Operation.java
new file mode 100644
index 0000000..1a199c5
--- /dev/null
+++ b/src/main/java/org/apache/nifi/action/Operation.java
@@ -0,0 +1,55 @@
+/*
+ * 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.action;
+
+/**
+ * Defines possible operations for a given action.
+ */
+public enum Operation {
+
+    Add("Add"),
+    Remove("Remove"),
+    Paste("Paste"),
+    Configure("Configure"),
+    Move("Move"),
+    Disconnect("Disconnect"),
+    Connect("Connect"),
+    Start("Start"),
+    Stop("Stop"),
+    Enable("Enable"),
+    Disable("Disable"),
+    Batch("Batch"),
+    Purge("Purge"),
+    ClearState("Clear State"),
+    StartVersionControl("Start Version Control"),
+    StopVersionControl("Stop Version Control"),
+    CommitLocalChanges("Commit Local Changes"),
+    RevertLocalChanges("Revert Local Changes"),
+    ChangeVersion("Change Version"),
+    RunOnce("RunOnce");
+
+    private final String label;
+
+    Operation(String label) {
+        this.label = label;
+    }
+
+    @Override
+    public String toString() {
+        return label;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/action/component/details/ComponentDetails.java b/src/main/java/org/apache/nifi/action/component/details/ComponentDetails.java
new file mode 100644
index 0000000..1eb35c1
--- /dev/null
+++ b/src/main/java/org/apache/nifi/action/component/details/ComponentDetails.java
@@ -0,0 +1,26 @@
+/*
+ * 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.action.component.details;
+
+import java.io.Serializable;
+
+/**
+ * Base interface for providing component details to an Action.
+ */
+public interface ComponentDetails extends Serializable {
+
+}
diff --git a/src/main/java/org/apache/nifi/action/details/ActionDetails.java b/src/main/java/org/apache/nifi/action/details/ActionDetails.java
new file mode 100644
index 0000000..75ebd9d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/action/details/ActionDetails.java
@@ -0,0 +1,26 @@
+/*
+ * 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.action.details;
+
+import java.io.Serializable;
+
+/**
+ * Provides additional details about a given action.
+ */
+public interface ActionDetails extends Serializable {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/DefaultRunDuration.java b/src/main/java/org/apache/nifi/annotation/behavior/DefaultRunDuration.java
new file mode 100644
index 0000000..2ef6aa8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/DefaultRunDuration.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.annotation.behavior;
+
+import java.time.Duration;
+
+public enum DefaultRunDuration {
+    NO_BATCHING(Duration.ZERO),
+    TWENTY_FIVE_MILLIS(Duration.ofMillis(25)),
+    FIFTY_MILLIS(Duration.ofMillis(50)),
+    ONE_HUNDRED_MILLIS(Duration.ofMillis(100)),
+    TWO_HUNDRED_FIFTY_MILLIS(Duration.ofMillis(250)),
+    FIVE_HUNDRED_MILLIS(Duration.ofMillis(500)),
+    ONE_SECOND(Duration.ofSeconds(1)),
+    TWO_SECONDS(Duration.ofSeconds(2));
+
+    private final Duration duration;
+
+    DefaultRunDuration(final Duration duration) {
+        this.duration = duration;
+    }
+
+    public Duration getDuration() {
+        return duration;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperties.java b/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperties.java
new file mode 100644
index 0000000..9670535
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperties.java
@@ -0,0 +1,37 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a component has more than one dynamic property
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DynamicProperties {
+
+    DynamicProperty[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperty.java b/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperty.java
new file mode 100644
index 0000000..5bccd48
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/DynamicProperty.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+
+/**
+ * An annotation that may be placed on a {@link ConfigurableComponent} to
+ * indicate that it supports a dynamic property.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DynamicProperty {
+
+    String name();
+
+    String value();
+
+    String description();
+
+    ExpressionLanguageScope expressionLanguageScope() default ExpressionLanguageScope.NONE;
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/DynamicRelationship.java b/src/main/java/org/apache/nifi/annotation/behavior/DynamicRelationship.java
new file mode 100644
index 0000000..006f6ad
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/DynamicRelationship.java
@@ -0,0 +1,44 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.Relationship;
+
+/**
+ * Annotation to indicate that a {@link Processor} supports dynamic
+ * relationship. A dynamic {@link Relationship} is one where the relationship is
+ * generated based on a user defined {@link PropertyDescriptor}
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DynamicRelationship {
+
+    String name();
+
+    String description();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/InputRequirement.java b/src/main/java/org/apache/nifi/annotation/behavior/InputRequirement.java
new file mode 100644
index 0000000..372d80c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/InputRequirement.java
@@ -0,0 +1,67 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker interface that a Processor can use to indicate whether it accepts, requires, or forbids
+ * input from other Processors. This information is used by the framework in order to ensure that
+ * a Processor is marked as invalid if it is missing necessary input or has input that will be ignored.
+ * This information also is used by the NiFi UI in order to prevent users from making connections
+ * to Processors that don't make sense.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface InputRequirement {
+    Requirement value();
+
+    enum Requirement {
+        /**
+         * This value is used to indicate that the Processor requires input from other Processors
+         * in order to run. As a result, the Processor will not be valid if it does not have any
+         * incoming connections.
+         */
+        INPUT_REQUIRED,
+
+        /**
+         * This value is used to indicate that the Processor will consume data from an incoming
+         * connection but does not require an incoming connection in order to perform its task.
+         * If the {@link InputRequirement} annotation is not present, this is the default value
+         * that is used.
+         */
+        INPUT_ALLOWED,
+
+        /**
+         * This value is used to indicate that the Processor is a "Source Processor" and does
+         * not accept incoming connections. Because the Processor does not pull FlowFiles from
+         * an incoming connection, it can be very confusing for users who create incoming connections
+         * to the Processor. As a result, this value can be used in order to clarify that incoming
+         * connections will not be used. This prevents the user from even creating such a connection.
+         */
+        INPUT_FORBIDDEN;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/PrimaryNodeOnly.java b/src/main/java/org/apache/nifi/annotation/behavior/PrimaryNodeOnly.java
new file mode 100644
index 0000000..6443383
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/PrimaryNodeOnly.java
@@ -0,0 +1,36 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation a {@link org.apache.nifi.processor.Processor} implementation
+ * can use to indicate that the {@link org.apache.nifi.scheduling.ExecutionNode}
+ * of the processor is to be set to PRIMARY
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface PrimaryNodeOnly {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttribute.java b/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttribute.java
new file mode 100644
index 0000000..7094b92
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttribute.java
@@ -0,0 +1,41 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.processor.Processor Processor} indicating that this
+ * processor reads a specific FlowFile attribute.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ReadsAttribute {
+
+    String attribute();
+
+    String description() default "";
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttributes.java b/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttributes.java
new file mode 100644
index 0000000..fe1f8bd
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/ReadsAttributes.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.processor.Processor Processor} indicating that this
+ * processor reads specific FlowFile attributes.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ReadsAttributes {
+
+    ReadsAttribute[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/RequiresInstanceClassLoading.java b/src/main/java/org/apache/nifi/annotation/behavior/RequiresInstanceClassLoading.java
new file mode 100644
index 0000000..0796199
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/RequiresInstanceClassLoading.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *  Marker annotation a component can use to indicate that the framework should create a new ClassLoader
+ *  for each instance of the component, copying all resources from the component's NARClassLoader to a
+ *  new ClassLoader which will only be used by a given instance of the component.
+ *
+ *  If cloneAncestorResources is set to true, the instance ClassLoader will include ancestor resources up to the
+ *  first ClassLoader containing a controller service API referenced by the component, or up to the Jetty NAR.
+ *
+ *  Example #1 - PutHDFS has this flag set to true and does not reference any controller services, so it will include
+ *  resources from nifi-hadoop-nar, nifi-hadoop-libraries-nar, and nifi-standard-services-api-nar, stopping at nifi-jetty-nar.
+ *
+ *  Example #2 - If PutHDFS referenced an SSLContext and has this flag set to true, then it would include
+ *  resources from nifi-hadoop-nar, nifi-hadoop-libraries-nar, and stop before nifi-standard-services-api-nar.
+ *
+ *  Example #3 - HBaseClientService_1_1_2 does not have this flag set so it defaults to false, and therefore includes
+ *  only resources from the nifi-hbase-client-service-1_1_2-nar.
+ *
+ *  NOTE: When this annotation is used it is important to note that each added instance of the component will increase
+ *  the overall memory footprint more than that of a component without this annotation.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface RequiresInstanceClassLoading {
+
+    boolean cloneAncestorResources() default false;
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/Restricted.java b/src/main/java/org/apache/nifi/annotation/behavior/Restricted.java
new file mode 100644
index 0000000..8411c6c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/Restricted.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marks the usage of a component as restricted to users with elevated privileges.
+ * </p>
+ * <p>
+ * A {@code Restricted} component is one that can be used to execute arbitrary unsanitized
+ * code provided by the operator through the NiFi REST API/UI or can be used to obtain
+ * or alter data on the NiFi host system using the NiFi OS credentials. These components
+ * could be used by an otherwise authorized NiFi user to go beyond the intended use of
+ * the application, escalate privilege, or could expose data about the internals of the
+ * NiFi process or the host system. All of these capabilities should be considered
+ * privileged, and admins should be aware of these capabilities and explicitly enable
+ * them for a subset of trusted users.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface Restricted {
+    /**
+     * Provides a description of why the component usage is restricted. If using granular
+     * restrictions, specific explanations should be set in the Restriction.
+     *
+     * @return Description of restriction
+     */
+    String value() default "";
+
+    /**
+     * Provides a listing of specific Restrictions. If unspecified, this component will
+     * require access to restricted components regardless of restrictions.
+     *
+     * @return Array of restrictions
+     */
+    Restriction[] restrictions() default {};
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/Restriction.java b/src/main/java/org/apache/nifi/annotation/behavior/Restriction.java
new file mode 100644
index 0000000..63efc98
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/Restriction.java
@@ -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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.RequiredPermission;
+
+/**
+ * Specific restriction for a component. Indicates what the required permission is and why the restriction exists.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface Restriction {
+
+    /**
+     * Provides a listing of RequiredPermissions.
+     *
+     * @return Permission required for this restriction
+     */
+    RequiredPermission requiredPermission();
+
+    /**
+     * Provides an explanation of why the component usage is restricted
+     *
+     * @return Explanation for this restriction
+     */
+    String explanation();
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SideEffectFree.java b/src/main/java/org/apache/nifi/annotation/behavior/SideEffectFree.java
new file mode 100644
index 0000000..9a64f4b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SideEffectFree.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor}
+ * implementation can use to indicate that its operations on FlowFiles can be
+ * safely repeated across process sessions. If a processor has this annotation
+ * and it allows the framework to manage session commit and rollback then the
+ * framework may elect to cascade a
+ * {@link org.apache.nifi.processor.ProcessSession ProcessSession} given to this
+ * processor's onTrigger method to the onTrigger method of another processor. It
+ * can do this knowing that if something fails along a series of processors
+ * using this same session that it can all be safely rolled back without any ill
+ * effects on external services which could not be rolled back and thus all the
+ * processes could be safely repeated (implied idempotent behavior).
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface SideEffectFree {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java b/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java
new file mode 100644
index 0000000..cfcc820
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/Stateful.java
@@ -0,0 +1,57 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateManager;
+
+/**
+ * <p>
+ * Annotation that a Processor, ReportingTask, FlowAnalysisRule, ParameterProvider, or Controller Service can use to indicate
+ * that the component makes use of the {@link StateManager}. This annotation provides the
+ * user with a description of what information is being stored so that the user is able to
+ * understand what is shown to them and know what they are clearing should they choose to
+ * clear the state. Additionally, the UI will not show any state information to users if
+ * this annotation is not present.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface Stateful {
+    /**
+     * Provides a description of what information is being stored in the {@link StateManager}
+     *
+     * @return Description of information stored
+     */
+    String description();
+
+    /**
+     * Indicates the Scope(s) associated with the State that is stored and retrieved.
+     *
+     * @return Array of scopes for information stored
+     */
+    Scope[] scopes();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SupportsBatching.java b/src/main/java/org/apache/nifi/annotation/behavior/SupportsBatching.java
new file mode 100644
index 0000000..49136ad
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SupportsBatching.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation a Processor implementation can use to indicate that users
+ * should be able to supply a Batch Duration for the Processor. If a Processor
+ * uses this annotation, it is allowing the Framework to batch
+ * {@link org.apache.nifi.processor.ProcessSession ProcessSession}s' commits, as well as
+ * allowing the Framework to return the same ProcessSession multiple times from
+ * subsequent calls to
+ * {@link org.apache.nifi.processor.ProcessSessionFactory ProcessSessionFactory}.
+ * {@link org.apache.nifi.processor.ProcessSessionFactory#createSession() createSession()}.
+ *
+ * When this Annotation is used, it is important to note that calls to
+ * {@link org.apache.nifi.processor.ProcessSession#commit() ProcessSession.commit()} may
+ * not provide a guarantee that the data has been safely stored in NiFi's
+ * Content Repository or FlowFile Repository. Therefore, it is not appropriate,
+ * for instance, to use this annotation if the Processor will call
+ * ProcessSession.commit() to ensure data is persisted before deleting the data
+ * from a remote source.
+ *
+ * When the defaultDuration parameter is set, the processor is created with the supplied duration time, which can be adjusted afterwards.
+ * The supplied values can be selected from {@link org.apache.nifi.annotation.behavior.DefaultRunDuration}.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface SupportsBatching {
+    DefaultRunDuration defaultDuration() default DefaultRunDuration.NO_BATCHING;
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SupportsSensitiveDynamicProperties.java b/src/main/java/org/apache/nifi/annotation/behavior/SupportsSensitiveDynamicProperties.java
new file mode 100644
index 0000000..9927641
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SupportsSensitiveDynamicProperties.java
@@ -0,0 +1,36 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Component annotation indicating support for dynamic properties that can be designated as sensitive for the purpose of
+ * persistence and framework processing
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface SupportsSensitiveDynamicProperties {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SystemResource.java b/src/main/java/org/apache/nifi/annotation/behavior/SystemResource.java
new file mode 100644
index 0000000..cea3c70
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SystemResource.java
@@ -0,0 +1,26 @@
+/*
+ * 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.annotation.behavior;
+
+/**
+ * Represents a system resource.
+ */
+public enum SystemResource {
+
+    CPU, DISK, MEMORY, NETWORK
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsideration.java b/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsideration.java
new file mode 100644
index 0000000..66c8abb
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsideration.java
@@ -0,0 +1,55 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.components.ConfigurableComponent Component} describes how this component may impact a
+ * system resource based on its configuration.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Repeatable(SystemResourceConsiderations.class)
+public @interface SystemResourceConsideration {
+
+    String DEFAULT_DESCRIPTION = "An instance of this component can cause high usage of this system resource.  " +
+            "Multiple instances or high concurrency settings may result a degradation of performance.";
+
+    /**
+     * The {@link SystemResource SystemResource} which may be affected by this component.
+     *
+     * @return System Resource to be considered
+     */
+    SystemResource resource();
+
+    /**
+     * A description of how this component and its configuration may affect system resource usage.
+     *
+     * @return Description of resource consideration
+     */
+    String description() default DEFAULT_DESCRIPTION;
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsiderations.java b/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsiderations.java
new file mode 100644
index 0000000..cd6c8c2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/SystemResourceConsiderations.java
@@ -0,0 +1,37 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.components.ConfigurableComponent Component} describes how this component may impact
+ * system resources based on its configuration.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface SystemResourceConsiderations {
+    SystemResourceConsideration[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/TriggerSerially.java b/src/main/java/org/apache/nifi/annotation/behavior/TriggerSerially.java
new file mode 100644
index 0000000..974e358
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/TriggerSerially.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor}
+ * implementation can use to indicate that the Processor is not safe for
+ * concurrent execution of its onTrigger() method. By default, Processors are
+ * assumed to be safe for concurrent execution.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TriggerSerially {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenAnyDestinationAvailable.java b/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenAnyDestinationAvailable.java
new file mode 100644
index 0000000..db115cb
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenAnyDestinationAvailable.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor}
+ * implementation can use to indicate that the Processor is to be triggered if
+ * any of its destinations has available space for incoming FlowFiles. By
+ * default, Processors are triggered only when all destinations report that they
+ * have available space (i.e., none of the outgoing Connections is full).
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TriggerWhenAnyDestinationAvailable {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenEmpty.java b/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenEmpty.java
new file mode 100644
index 0000000..6fac03b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/TriggerWhenEmpty.java
@@ -0,0 +1,57 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor}
+ * implementation can use to indicate that the Processor should still be
+ * triggered even when it has no data in its work queue.
+ * </p>
+ *
+ * <p>
+ * A Processor is scheduled to be triggered based on its configured Scheduling Period
+ * and Scheduling Strategy. However, when the scheduling period elapses, the Processor
+ * will not be scheduled if it has no work to do. Normally, a Processor is said to have
+ * work to do if one of the following circumstances is true:
+ * </p>
+ *
+ * <ul>
+ * <li>An incoming Connection has data in its queue</li>
+ * <li>The Processor has no incoming Connections.</li>
+ * <li>All incoming Connections are self-loops (both the source and destination of the Connection are the same Processor).
+ * </ul>
+ *
+ * <p>
+ * If the Processor needs to be triggered to run even when the above conditions are all
+ * <code>false</code>, the Processor's class can be annotated with this annotation, which
+ * will cause the Processor to be triggered, even if its incoming queues are empty.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TriggerWhenEmpty {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/WritesAttribute.java b/src/main/java/org/apache/nifi/annotation/behavior/WritesAttribute.java
new file mode 100644
index 0000000..d06af8f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/WritesAttribute.java
@@ -0,0 +1,41 @@
+/*
+ * 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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.processor.Processor Processor} indicating that this
+ * processor writes/updates a specific FlowFile attribute.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface WritesAttribute {
+
+    String attribute();
+
+    String description() default "";
+}
diff --git a/src/main/java/org/apache/nifi/annotation/behavior/WritesAttributes.java b/src/main/java/org/apache/nifi/annotation/behavior/WritesAttributes.java
new file mode 100644
index 0000000..603eb86
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/behavior/WritesAttributes.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.annotation.behavior;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a
+ * {@link org.apache.nifi.processor.Processor Processor} indicating that this
+ * processor writes/updates specific FlowFile attributes.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface WritesAttributes {
+
+    WritesAttribute[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/configuration/DefaultSchedule.java b/src/main/java/org/apache/nifi/annotation/configuration/DefaultSchedule.java
new file mode 100644
index 0000000..f1a145a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/configuration/DefaultSchedule.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.annotation.configuration;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+
+/**
+ * <p>
+ * Marker interface that a Processor can use to configure default settings for the schedule strategy, the period and the number of concurrent tasks.
+ * Marker interface that a ReportingTask can use to configure default settings the  schedule strategy and the period.
+ * Note that the number of Concurrent tasks will be ignored if the annotation @TriggerSerialy is used
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DefaultSchedule {
+
+    SchedulingStrategy strategy() default  SchedulingStrategy.TIMER_DRIVEN;
+    String period() default "0 sec";
+    int concurrentTasks() default 1;
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/configuration/DefaultSettings.java b/src/main/java/org/apache/nifi/annotation/configuration/DefaultSettings.java
new file mode 100644
index 0000000..09402c7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/configuration/DefaultSettings.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.annotation.configuration;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.logging.LogLevel;
+
+/**
+ * <p>
+ * Marker interface that a Processor can use to configure the yield duration, the  penalty duration and the bulletin log level.
+ * Note that the number of Concurrent tasks will be ignored if the annotation @TriggerSerialy is used
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface DefaultSettings {
+    String yieldDuration() default "1 sec";
+    String penaltyDuration() default "30 sec";
+    LogLevel bulletinLevel() default LogLevel.WARN;
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java b/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.java
new file mode 100644
index 0000000..5ac0e97
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/CapabilityDescription.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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that may be placed on a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} allowing for a
+ * description to be provided. This description can be provided to a user in
+ * logs, UI, etc.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CapabilityDescription {
+
+    String value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/DeprecationNotice.java b/src/main/java/org/apache/nifi/annotation/documentation/DeprecationNotice.java
new file mode 100644
index 0000000..e385843
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/DeprecationNotice.java
@@ -0,0 +1,46 @@
+/*
+ * 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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.ConfigurableComponent;
+
+/**
+ * Annotation that can be applied to a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} in order to
+ * warn about the deprecation of the component. The deprecation warning is informational only
+ * and doesn't affect the processor run time behavior in any way
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+
+public @interface DeprecationNotice {
+    Class<? extends ConfigurableComponent>[] alternatives() default {};
+
+    String[] classNames() default {};
+
+    String reason() default "";
+}
+
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCase.java b/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCase.java
new file mode 100644
index 0000000..562647e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCase.java
@@ -0,0 +1,77 @@
+/*
+ * 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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ *     An annotation that can be used for Processors in order to explain a specific use case that can be
+ *     accomplished using this Processor in conjunction with at least one other Processor.
+ *     For Processors that are able to be used for multiple use cases, the component
+ *     may be annotated with multiple MultiProcessorUseCase annotations.
+ * </p>
+ * <p>
+ *     Note that this annotation differs from {@link UseCase} in that UseCase should describe a use case that is
+ *     accomplished using only the extension that is annotated. In contrast, MultiProcessorUseCase documents a use case
+ *     that is accomplished by using both the Processor that is annotated as well as other Processors.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Repeatable(MultiProcessorUseCases.class)
+public @interface MultiProcessorUseCase {
+
+    /**
+     * A simple 1 (at most 2) sentence description of the use case. This should not include any extraneous details, such
+     * as caveats, examples, etc. Those can be provided using the {@link #notes()} method.
+     *
+     * @return a simple description of the use case
+     */
+    String description();
+
+    /**
+     * Most of the time, 1-2 sentences is sufficient to describe a use case. Those 1-2 sentence should then be returned
+     * by the {@link #description()}. In the event that the description is not sufficient, details may be provided to
+     * further explain, by providing examples, caveats, etc.
+     *
+     * @return any important notes that pertain to the use case
+     */
+    String notes() default "";
+
+    /**
+     * An optional array of keywords that can be associated with the use case.
+     * @return keywords associated with the use case
+     */
+    String[] keywords() default {};
+
+    /**
+     * An array of {@link ProcessorConfiguration}s that are necessary in order to accomplish the task described in this use case.
+     * @return an array of processor configurations
+     */
+    ProcessorConfiguration[] configurations();
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCases.java b/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCases.java
new file mode 100644
index 0000000..a7ae09b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/MultiProcessorUseCases.java
@@ -0,0 +1,36 @@
+/*
+ * 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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An enclosing annotation that can be used in order to use the {@link MultiProcessorUseCase} annotation in a repeated manner.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface MultiProcessorUseCases {
+    MultiProcessorUseCase[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/ProcessorConfiguration.java b/src/main/java/org/apache/nifi/annotation/documentation/ProcessorConfiguration.java
new file mode 100644
index 0000000..ec2ab1f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/ProcessorConfiguration.java
@@ -0,0 +1,57 @@
+/*
+ * 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.annotation.documentation;
+
+
+import org.apache.nifi.processor.Processor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that can be used in conjunction with {@link MultiProcessorUseCase} in order to specify the different
+ * components that are involved in a given use case.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ProcessorConfiguration {
+
+    /**
+     * Returns the class of the Processor that is to be used in the use case, if it is provided. Either the
+     * Processor Class or the Processor Class Name must be provided.
+     *
+     * @return the Processor's class, or <code>Processor</code> if the processor's classname is specified instead
+     */
+    Class<? extends Processor> processorClass() default Processor.class;
+
+    /**
+     * @return the fully qualified classname of the component
+     */
+    String processorClassName() default "";
+
+    /**
+     * @return an explanation of how the Processor should be configured.
+     */
+    String configuration();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java b/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.java
new file mode 100644
index 0000000..5eae159
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/SeeAlso.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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.components.ConfigurableComponent;
+
+/**
+ * Annotation that may be placed on a null {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} that indicates
+ * this component is related to the components listed.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface SeeAlso {
+
+    Class<? extends ConfigurableComponent>[] value() default {};
+
+    String[] classNames() default {};
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/Tags.java b/src/main/java/org/apache/nifi/annotation/documentation/Tags.java
new file mode 100644
index 0000000..a22cb28
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/Tags.java
@@ -0,0 +1,44 @@
+/*
+ * 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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that can be applied to a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} in order to
+ * associate tags (keywords) with the component. These tags do not affect the
+ * component in any way but serve as additional documentation and can be used to
+ * sort/filter Processors.
+ *
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface Tags {
+
+    String[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/UseCase.java b/src/main/java/org/apache/nifi/annotation/documentation/UseCase.java
new file mode 100644
index 0000000..478ba1e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/UseCase.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.annotation.documentation;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ *     An annotation that can be used for extension points in order to explain a specific use case that can be
+ *     accomplished using this extension. For components that are able to be used for multiple use cases, the component
+ *     may be annotated with multiple UseCase annotations.
+ * </p>
+ * <p>
+ *     Note that this annotation differs from {@link CapabilityDescription} in that CapabilityDescription should describe the
+ *     general purpose of the extension point. UseCase, on the other hand, documents one very specific use case that
+ *     can be accomplished. Some extension points may use only a single UseCase annotation while others may accomplish
+ *     many use cases.
+ * </p>
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Repeatable(UseCases.class)
+public @interface UseCase {
+
+    /**
+     * A simple 1 (at most 2) sentence description of the use case. This should not include any extraneous details, such
+     * as caveats, examples, etc. Those can be provided using the {@link #notes()} method.
+     *
+     * @return a simple description of the use case
+     */
+    String description();
+
+    /**
+     * Most of the time, 1-2 sentences is sufficient to describe a use case. Those 1-2 sentence should then be returned
+     * by the {@link #description()}. In the event that the description is not sufficient, details may be provided to
+     * further explain, by providing examples, caveats, etc.
+     *
+     * @return any important notes that pertain to the use case
+     */
+    String notes() default "";
+
+    /**
+     * Most Processors specify an InputRequirement of either {@link InputRequirement.Requirement#INPUT_REQUIRED INPUT_REQUIRED}
+     * or {@link InputRequirement.Requirement#INPUT_FORBIDDEN}. However, some Processors use {@link InputRequirement.Requirement#INPUT_ALLOWED}
+     * because some use cases require input while others do not. The inputRequirement here is only relevant for Processors that use
+     * an InputRequirement of {@link InputRequirement.Requirement#INPUT_ALLOWED} and can indicate whether or not the Processor should have
+     * input (aka incoming Connections) for this particular use case.
+     *
+     * @return the {@link InputRequirement} that corresponds to this use case.
+     */
+    InputRequirement.Requirement inputRequirement() default InputRequirement.Requirement.INPUT_ALLOWED;
+
+    /**
+     * An optional array of keywords that can be associated with the use case.
+     * @return keywords associated with the use case
+     */
+    String[] keywords() default {};
+
+    /**
+     * A description of how to configure the extension for this particular use case.
+     * @return a description of how to configure the extension for this particular use case.
+     */
+    String configuration() default "";
+}
diff --git a/src/main/java/org/apache/nifi/annotation/documentation/UseCases.java b/src/main/java/org/apache/nifi/annotation/documentation/UseCases.java
new file mode 100644
index 0000000..f64c8e5
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/documentation/UseCases.java
@@ -0,0 +1,36 @@
+/*
+ * 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.annotation.documentation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An enclosing annotation that can be used in order to use the {@link UseCase} annotation in a repeated manner.
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface UseCases {
+    UseCase[] value();
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java
new file mode 100644
index 0000000..4933a38
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnAdded.java
@@ -0,0 +1,49 @@
+/*
+ * 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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
+ * can use to indicate a method should be called whenever the component is added
+ * to the flow. This method will be called once for the entire life of a
+ * component instance.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation are called without any arguments, as all
+ * settings and properties can be assumed to be the defaults.
+ * </p>
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnAdded {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnConfigurationRestored.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnConfigurationRestored.java
new file mode 100644
index 0000000..72c5bd3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnConfigurationRestored.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.annotation.lifecycle;
+
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.ProcessContext;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker Annotation that a Processor, Reporting Task, or Controller Service can use to indicate
+ * that the method with this Annotation should be invoked whenever the component's configuration
+ * is restored after a restart of NiFi.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation are permitted to take no arguments or to take a
+ * single argument. If using a single argument, that argument must be of type
+ * {@link ConfigurationContext} if the component is a ReportingTask or a
+ * ControllerService. If the component is a Processor, then the argument must be
+ * of type {@link ProcessContext}.
+ * </p>
+ *
+ * <p>
+ * Whenever a new component is added to the flow, this method will be called immediately, since
+ * there is no configuration to restore (in this case all configuration has already been restored,
+ * since there is no configuration to restore).
+ * </p>
+ *
+ * @since 0.5.0
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnConfigurationRestored {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnDisabled.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnDisabled.java
new file mode 100644
index 0000000..c8d77e3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnDisabled.java
@@ -0,0 +1,61 @@
+/*
+ * 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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.controller.ConfigurationContext;
+
+/**
+ * <p>
+ * Marker annotation a
+ * {@link org.apache.nifi.controller.ControllerService ControllerService} can
+ * use to indicate a method should be called whenever the service is disabled.
+ * </p>
+ *
+ * <p>
+ * Methods using this annotation are permitted to take zero arguments or to take
+ * a single argument of type {@link ConfigurationContext}. If a method with this
+ * annotation throws a Throwable, a log message and bulletin will be issued for
+ * the service, but the service will still be marked as Disabled. The failing
+ * method will not be called again until the service is enabled and disabled again.
+ * This is done in order to prevent a ControllerService from continually failing
+ * in such a way that the service could not be disabled and updated without
+ * restarting the instance of NiFi.
+ * </p>
+ *
+ * <p>
+ * Note that this annotation will be ignored if applied to a ReportingTask, ParameterProvider or
+ * Processor. For a Controller Service, enabling and disabling are considered
+ * lifecycle events, as the action makes them usable or unusable by other
+ * components. However, for a Processor and a Reporting Task, these are not
+ * lifecycle events but rather a mechanism to allow a component to be excluded
+ * when starting or stopping a group of components.
+ * </p>
+ *
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnDisabled {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnEnabled.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnEnabled.java
new file mode 100644
index 0000000..28132c8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnEnabled.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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation a
+ * {@link org.apache.nifi.controller.ControllerService ControllerService} can
+ * use to indicate a method should be called whenever the service is enabled.
+ * Any method that has this annotation will be called every time a user enables
+ * the service. Additionally, each time that NiFi is restarted, if NiFi is
+ * configured to "auto-resume state" and the service is enabled, the method will
+ * be invoked.
+ * </p>
+ *
+ * <p>
+ * Methods using this annotation must take either 0 arguments or a single
+ * argument of type
+ * {@link org.apache.nifi.controller.ConfigurationContext ConfigurationContext}.
+ * </p>
+ *
+ * <p>
+ * If a method with this annotation throws a Throwable, a log message and
+ * bulletin will be issued for the component. In this event, the service will
+ * remain in an 'ENABLING' state and will not be usable. All methods with this
+ * annotation will then be called again after a delay. The service will not be
+ * made available for use until all methods with this annotation have returned
+ * without throwing anything.
+ * </p>
+ *
+ * <p>
+ * Note that this annotation will be ignored if applied to a ReportingTask, ParameterProvider or
+ * Processor. For a Controller Service, enabling and disabling are considered
+ * lifecycle events, as the action makes them usable or unusable by other
+ * components. However, for a Processor and a Reporting Task, these are not
+ * lifecycle events but rather a mechanism to allow a component to be excluded
+ * when starting or stopping a group of components.
+ * </p>
+ *
+ *
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnEnabled {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java
new file mode 100644
index 0000000..172979e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnRemoved.java
@@ -0,0 +1,57 @@
+/*
+ * 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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.ProcessContext;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.registry.flow.FlowRegistryClient FlowRegistryClient},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider},
+ * {@link org.apache.nifi.flowanalysis.FlowAnalysisRule FlowAnalysisRule}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
+ * can use to indicate a method should be called whenever the component is
+ * removed from the flow. This method will be called once for the entire life of
+ * a component instance. If the method throw any Throwable, that Throwable will
+ * be caught and logged but will not prevent subsequent methods with this
+ * annotation or removal of the component from the flow.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation are permitted to take no arguments or to take a
+ * single argument. If using a single argument, that argument must be of type
+ * {@link ConfigurationContext} if the component is a ReportingTask, a ParameterProvider, or a
+ * ControllerService. If the component is a Processor, then the argument must be
+ * of type {@link ProcessContext}.
+ * </p>
+ *
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnRemoved {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnScheduled.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnScheduled.java
new file mode 100644
index 0000000..b320858
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnScheduled.java
@@ -0,0 +1,78 @@
+/*
+ * 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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor} or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
+ * can use to indicate a method should be called whenever the component is
+ * scheduled to run. This will be called before any call to 'onTrigger' and will
+ * be called once each time a Processor or Reporting Task is scheduled to run.
+ * This occurs in one of two ways: either a user clicks to schedule the
+ * component to run, or NiFi is restarted with the "auto-resume state"
+ * configuration set to true (the default value) and the component is already
+ * running.
+ * </p>
+ *
+ * <p>
+ * Methods using this annotation must take either 0 arguments or a single
+ * argument.
+ * </p>
+ *
+ * <p>
+ * If using 1 argument and the component using the annotation is a Processor,
+ * that argument must be of type
+ * {@link org.apache.nifi.processor.ProcessContext ProcessContext}.
+ * </p>
+ *
+ * <p>
+ * If using 1 argument and the component using the annotation is a Reporting
+ * Task, that argument must be of type
+ * {@link org.apache.nifi.controller.ConfigurationContext ConfigurationContext}.
+ * </p>
+ *
+ * <p>
+ * If any method annotated with this annotation throws any Throwable, the
+ * framework will wait a while and then attempt to invoke the method again. This
+ * will continue until the method succeeds, and the component will then be
+ * scheduled to run after this method return successfully.
+ * </p>
+ *
+ * <p><b>Implementation Guidelines:</b>
+ * <ul>
+ *   <li>Methods with this annotation are expected to perform very quick, short-lived tasks. If the function is
+ *       expensive or long-lived, the logic should be performed in the {@code onTrigger} method instead.</li>
+ *   <li>If a method with this annotation does not return (exceptionally or otherwise) within a short period
+ *       of time (the duration is configurable in the properties file), the Thread may be interrupted.</li>
+ *   <li>Methods that make use of this interface should honor Java's Thread interruption mechanisms and not swallow
+ *       {@link InterruptedException}.</li>
+ * </ul>
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnScheduled {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnShutdown.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnShutdown.java
new file mode 100644
index 0000000..88a3ec3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnShutdown.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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.ProcessContext;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor},
+ * {@link org.apache.nifi.controller.ControllerService ControllerService},
+ * {@link org.apache.nifi.parameter.ParameterProvider ParameterProvider}, or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
+ * can use to indicate a method should be called whenever the flow is being
+ * shutdown. This will be called at most once for each component in a JVM
+ * lifetime. It is not, however, guaranteed that this method will be called on
+ * shutdown, as the service may be killed suddenly.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation are permitted to take either 0 or 1 argument. If
+ * an argument is used, it must be of type {@link ConfigurationContext} if the
+ * component is a ReportingTask or Controller Service, or of type
+ * {@link ProcessContext} if the component is a Processor.
+ * </p>
+ *
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnShutdown {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnStopped.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnStopped.java
new file mode 100644
index 0000000..647c2ec
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnStopped.java
@@ -0,0 +1,75 @@
+/*
+ * 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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.ProcessContext;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor} or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} implementation
+ * can use to indicate that a method should be called whenever the component is
+ * no longer scheduled to run. Methods marked with this annotation will be
+ * invoked each time the component is stopped and will be invoked only after the
+ * last thread has returned from the <code>onTrigger</code> method.
+ * </p>
+ *
+ * <p>
+ * This means that the thread executing in this method will be the only thread
+ * executing in any part of the Processor. However, since other threads may
+ * later execute other parts of the code, member variables must still be
+ * protected appropriately. However, access to multiple variables need not be
+ * atomic.
+ * </p>
+ *
+ * <p>
+ * To indicate that a method should be called immediately when a component is no
+ * longer scheduled to run (as opposed to after all threads have returned from
+ * the <code>onTrigger</code> method), see the {@link OnUnscheduled} annotation.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation are permitted to take either 0 or 1 argument. If
+ * an argument is used, it must be of type {@link ConfigurationContext} if the
+ * component is a ReportingTask or of type {@link ProcessContext} if the
+ * component is a Processor.
+ * </p>
+ *
+ * <p><b>Implementation Guidelines:</b>
+ * <ul>
+ *   <li>Methods with this annotation are expected to perform very quick, short-lived tasks. If the function is
+ *       expensive or long-lived, the logic should be performed in the {@code onTrigger} method instead.</li>
+ *   <li>If a method with this annotation does not return (exceptionally or otherwise) within a short period
+ *       of time (the duration is configurable in the properties file), the Thread may be interrupted.</li>
+ *   <li>Methods that make use of this interface should honor Java's Thread interruption mechanisms and not swallow
+ *       {@link InterruptedException}.</li>
+ * </ul>
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnStopped {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/lifecycle/OnUnscheduled.java b/src/main/java/org/apache/nifi/annotation/lifecycle/OnUnscheduled.java
new file mode 100644
index 0000000..ea041ae
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/lifecycle/OnUnscheduled.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.annotation.lifecycle;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation a {@link org.apache.nifi.processor.Processor Processor} or
+ * {@link org.apache.nifi.reporting.ReportingTask ReportingTask} should be
+ * called whenever the component is no longer scheduled to run. Methods marked
+ * with this annotation will be invoked each time the framework is notified to
+ * stop scheduling the component. This method is invoked as other threads are
+ * potentially running. To invoke a method after all threads have finished
+ * processing, see the {@link OnStopped} annotation.
+ * </p>
+ *
+ * <p>
+ * Methods using this annotation must take either 0 arguments or a single
+ * argument.
+ * </p>
+ *
+ * <p>
+ * If using 1 argument and the component using the annotation is a Processor,
+ * that argument must be of type
+ * {@link org.apache.nifi.processor.ProcessContext ProcessContext}.
+ * </p>
+ *
+ * <p>
+ * If using 1 argument and the component using the annotation is a Reporting
+ * Task, that argument must be of type
+ * {@link org.apache.nifi.controller.ConfigurationContext ConfigurationContext}.
+ * </p>
+ *
+ * <p><b>Implementation Guidelines:</b>
+ * <ul>
+ *   <li>Methods with this annotation are expected to perform very quick, short-lived tasks. If the function is
+ *       expensive or long-lived, the logic should be performed in the {@code onTrigger} method instead.</li>
+ *   <li>If a method with this annotation does not return (exceptionally or otherwise) within a short period
+ *       of time (the duration is configurable in the properties file), the Thread may be interrupted.</li>
+ *   <li>Methods that make use of this interface should honor Java's Thread interruption mechanisms and not swallow
+ *       {@link InterruptedException}.</li>
+ * </ul>
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnUnscheduled {
+}
diff --git a/src/main/java/org/apache/nifi/annotation/notification/OnPrimaryNodeStateChange.java b/src/main/java/org/apache/nifi/annotation/notification/OnPrimaryNodeStateChange.java
new file mode 100644
index 0000000..e3ff401
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/notification/OnPrimaryNodeStateChange.java
@@ -0,0 +1,44 @@
+/*
+ * 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.annotation.notification;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * Marker annotation that a component can use to indicate that a method should be
+ * called whenever the state of the Primary Node in a cluster has changed.
+ * </p>
+ *
+ * <p>
+ * Methods with this annotation should take either no arguments or one argument of type
+ * {@link PrimaryNodeState}. The {@link PrimaryNodeState} provides context about what changed
+ * so that the component can take appropriate action.
+ * </p>
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface OnPrimaryNodeStateChange {
+
+}
diff --git a/src/main/java/org/apache/nifi/annotation/notification/PrimaryNodeState.java b/src/main/java/org/apache/nifi/annotation/notification/PrimaryNodeState.java
new file mode 100644
index 0000000..539e5f0
--- /dev/null
+++ b/src/main/java/org/apache/nifi/annotation/notification/PrimaryNodeState.java
@@ -0,0 +1,33 @@
+/*
+ * 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.annotation.notification;
+
+/**
+ * Represents a state change that occurred for the Primary Node of a NiFi cluster.
+ */
+public enum PrimaryNodeState {
+    /**
+     * The node receiving this state has been elected the Primary Node of the NiFi cluster.
+     */
+    ELECTED_PRIMARY_NODE,
+
+    /**
+     * The node receiving this state was the Primary Node but has now had its Primary Node
+     * role revoked.
+     */
+    PRIMARY_NODE_REVOKED;
+}
diff --git a/src/main/java/org/apache/nifi/asset/Asset.java b/src/main/java/org/apache/nifi/asset/Asset.java
new file mode 100644
index 0000000..3e8c026
--- /dev/null
+++ b/src/main/java/org/apache/nifi/asset/Asset.java
@@ -0,0 +1,64 @@
+/*
+ * 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.asset;
+
+import java.io.File;
+import java.util.Optional;
+
+/**
+ * An Asset is a representation of some resource that is necessary in order to run a dataflow.
+ * An Asset is always accessed as a local file.
+ */
+public interface Asset {
+
+    /**
+     * Returns a unique identifier for the Asset
+     *
+     * @return Asset Identifier
+     */
+    String getIdentifier();
+
+    /**
+     * Returns the identifier of the parameter context the Asset belongs to
+     *
+     * @return Parameter Context Identifier
+     */
+    String getParameterContextIdentifier();
+
+    /**
+     * Returns the name of the Asset
+     *
+     * @return Asset Name
+     */
+    String getName();
+
+    /**
+     * Returns the local file that the Asset is associated with
+     *
+     * @return Asset File
+     */
+    File getFile();
+
+    /**
+     * Returns the digest of the contents of the local file that the Asset is associated with.
+     * The digest will not be present when the asset is considered missing and the local file does not exist.
+     *
+     * @return Asset Digest or empty when not found
+     */
+    Optional<String> getDigest();
+}
diff --git a/src/main/java/org/apache/nifi/components/AbstractConfigurableComponent.java b/src/main/java/org/apache/nifi/components/AbstractConfigurableComponent.java
new file mode 100644
index 0000000..2b0a061
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/AbstractConfigurableComponent.java
@@ -0,0 +1,261 @@
+/*
+ * 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.components;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public abstract class AbstractConfigurableComponent implements ConfigurableComponent {
+
+    /**
+     * Allows subclasses to perform their own validation on the already set
+     * properties. Since each property is validated as it is set this allows
+     * validation of groups of properties together. Default return is an empty
+     * set.
+     *
+     * This method will be called only when it has been determined that all
+     * property values are valid according to their corresponding
+     * PropertyDescriptor's validators.
+     *
+     * @param validationContext provides a mechanism for obtaining externally
+     * managed values, such as property values and supplies convenience methods
+     * for operating on those values
+     *
+     * @return Collection of ValidationResult objects that will be added to any
+     * other validation findings - may be null
+     */
+    protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) {
+        return Collections.emptySet();
+    }
+
+    /**
+     * @param descriptorName to lookup the descriptor
+     * @return a PropertyDescriptor for the name specified that is fully
+     * populated
+     */
+    @Override
+    public final PropertyDescriptor getPropertyDescriptor(final String descriptorName) {
+        final PropertyDescriptor specDescriptor = new PropertyDescriptor.Builder().name(descriptorName).build();
+        return getPropertyDescriptor(specDescriptor);
+    }
+
+    private PropertyDescriptor getPropertyDescriptor(final PropertyDescriptor specDescriptor) {
+        //check if property supported
+        PropertyDescriptor descriptor = getSupportedPropertyDescriptor(specDescriptor);
+        if (descriptor != null) {
+            return descriptor;
+        }
+
+        descriptor = getSupportedDynamicPropertyDescriptor(specDescriptor.getName());
+        if (descriptor != null && !descriptor.isDynamic()) {
+            descriptor = new PropertyDescriptor.Builder().fromPropertyDescriptor(descriptor).dynamic(true).build();
+        }
+
+        if (descriptor == null) {
+            descriptor = new PropertyDescriptor.Builder().fromPropertyDescriptor(specDescriptor).addValidator(Validator.INVALID).dynamic(true).build();
+        }
+        return descriptor;
+    }
+
+    private PropertyDescriptor getSupportedPropertyDescriptor(final PropertyDescriptor specDescriptor) {
+        final List<PropertyDescriptor> supportedDescriptors = getSupportedPropertyDescriptors();
+        if (supportedDescriptors != null) {
+            for (final PropertyDescriptor desc : supportedDescriptors) { //find actual descriptor
+                if (specDescriptor.equals(desc)) {
+                    return desc;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public final Collection<ValidationResult> validate(final ValidationContext context) {
+        // goes through context properties, should match supported properties + supported dynamic properties
+        final Collection<ValidationResult> results = new ArrayList<>();
+        final Set<PropertyDescriptor> contextDescriptors = context.getProperties().keySet();
+
+        for (final PropertyDescriptor descriptor : contextDescriptors) {
+            // If the property descriptor's dependency is not satisfied, the property does not need to be considered, as it's not relevant to the
+            // component's functionality.
+            final boolean dependencySatisfied = context.isDependencySatisfied(descriptor, this::getPropertyDescriptor);
+            if (!dependencySatisfied) {
+                continue;
+            }
+
+            validateDependencies(descriptor, context, results);
+
+            String value = context.getProperty(descriptor).getValue();
+            if (value == null) {
+                value = descriptor.getDefaultValue();
+            }
+
+            if (value == null && descriptor.isRequired()) {
+                String displayName = descriptor.getDisplayName();
+                ValidationResult.Builder builder = new ValidationResult.Builder().valid(false).input(null).subject(displayName != null ? displayName : descriptor.getName());
+                builder = (displayName != null) ? builder.explanation(displayName + " is required") : builder.explanation(descriptor.getName() + " is required");
+                results.add(builder.build());
+                continue;
+            } else if (value == null) {
+                continue;
+            }
+
+            final ValidationResult result = descriptor.validate(value, context);
+            if (!result.isValid()) {
+                results.add(result);
+            }
+        }
+
+        // only run customValidate if regular validation is successful. This allows Processor developers to not have to check
+        // if values are null or invalid so that they can focus only on the interaction between the properties, etc.
+        if (results.isEmpty()) {
+            final Collection<ValidationResult> customResults = customValidate(context);
+            if (null != customResults) {
+                for (final ValidationResult result : customResults) {
+                    if (!result.isValid()) {
+                        results.add(result);
+                    }
+                }
+            }
+        }
+
+        return results;
+    }
+
+    private void validateDependencies(final PropertyDescriptor descriptor, final ValidationContext context, final Collection<ValidationResult> results) {
+        // Ensure that we don't have any dependencies on non-existent properties.
+        final Set<PropertyDependency> dependencies = descriptor.getDependencies();
+        for (final PropertyDependency dependency : dependencies) {
+            final String dependentPropertyName = dependency.getPropertyName();
+
+            // If there's a supported property descriptor then all is okay.
+            final PropertyDescriptor specDescriptor = new PropertyDescriptor.Builder().name(dependentPropertyName).build();
+            final PropertyDescriptor supportedDescriptor = getSupportedPropertyDescriptor(specDescriptor);
+            if (supportedDescriptor != null) {
+                continue;
+            }
+
+            final PropertyDescriptor dynamicPropertyDescriptor = getSupportedDynamicPropertyDescriptor(dependentPropertyName);
+            if (dynamicPropertyDescriptor == null) {
+                results.add(new ValidationResult.Builder()
+                    .subject(descriptor.getDisplayName())
+                    .valid(false)
+                    .explanation("Property depends on property " + dependentPropertyName + ", which is not a known property")
+                    .build());
+            }
+
+            // Dependent property is supported as a dynamic property. This is okay as long as there is a value set.
+            final PropertyValue value = context.getProperty(dynamicPropertyDescriptor);
+            if (value == null || !value.isSet()) {
+                results.add(new ValidationResult.Builder()
+                    .subject(descriptor.getDisplayName())
+                    .valid(false)
+                    .explanation("Property depends on property " + dependentPropertyName + ", which is not a known property")
+                    .build());
+            }
+        }
+
+    }
+
+    /**
+     * Hook method allowing subclasses to eagerly react to a configuration
+     * change for the given property descriptor. As an alternative to using this
+     * method a processor may simply get the latest value whenever it needs it
+     * and if necessary lazily evaluate it.
+     *
+     * @param descriptor of the modified property
+     * @param oldValue non-null property value (previous)
+     * @param newValue the new property value or if null indicates the property
+     * was removed
+     */
+    @Override
+    public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) {
+    }
+
+    /**
+     * <p>
+     * Used to allow subclasses to determine what PropertyDescriptor if any to
+     * use when a property is requested for which a descriptor is not already
+     * registered. By default this method simply returns a null descriptor. By
+     * overriding this method processor implementations can support dynamic
+     * properties since this allows them to register properties on demand. It is
+     * acceptable for a dynamically generated property to indicate it is
+     * required so long as it is understood it is only required once set.
+     * Dynamic properties by definition cannot be required until used.</p>
+     *
+     * <p>
+     * This method should be side effect free in the subclasses in terms of how
+     * often it is called for a given property name because there is guarantees
+     * how often it will be called for a given property name.</p>
+     *
+     * <p>
+     * Default is null.
+     *
+     * @param propertyDescriptorName used to lookup if any property descriptors exist for that name
+     * @return new property descriptor if supported
+     */
+    protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+        return null;
+    }
+
+    /**
+     * Allows subclasses to register which property descriptor objects are
+     * supported. Default return is an empty set.
+     *
+     * @return PropertyDescriptor objects this processor currently supports
+     */
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public final List<PropertyDescriptor> getPropertyDescriptors() {
+        final List<PropertyDescriptor> supported = getSupportedPropertyDescriptors();
+        return supported == null ? Collections.emptyList() : Collections.unmodifiableList(supported);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof ConfigurableComponent)) {
+            return false;
+        }
+
+        final ConfigurableComponent other = (ConfigurableComponent) obj;
+        return getIdentifier().equals(other.getIdentifier());
+    }
+
+    @Override
+    public int hashCode() {
+        return 235 + getIdentifier().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[id=" + getIdentifier() + "]";
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/components/AllowableValue.java b/src/main/java/org/apache/nifi/components/AllowableValue.java
new file mode 100644
index 0000000..9cae438
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/AllowableValue.java
@@ -0,0 +1,136 @@
+/*
+ * 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.components;
+
+import java.util.Objects;
+
+/**
+ * <p>
+ * Represents a valid value for a {@link PropertyDescriptor}
+ * </p>
+ */
+public class AllowableValue implements DescribedValue {
+
+    private final String value;
+    private final String displayName;
+    private final String description;
+
+    /**
+     * Constructs a new AllowableValue with the given value and and the same
+     * display name and no description.
+     *
+     * @param value that is allowed
+     */
+    public AllowableValue(final String value) {
+        this(value, value);
+    }
+
+    /**
+     * Constructs a new AllowableValue with the given value and display name and
+     * no description
+     *
+     * @param value       that is allowed
+     * @param displayName to display for the value
+     * @throws NullPointerException if either argument is null
+     */
+    public AllowableValue(final String value, final String displayName) {
+        this(value, displayName, null);
+    }
+
+    /**
+     * Constructs a new AllowableValue with the given value, display name, and
+     * description
+     *
+     * @param value       that is valid
+     * @param displayName to show for the value
+     * @param description of the value
+     * @throws NullPointerException if identifier or value is null
+     */
+    public AllowableValue(final String value, final String displayName, final String description) {
+        this.value = Objects.requireNonNull(value);
+        this.displayName = Objects.requireNonNull(displayName);
+        this.description = description;
+    }
+
+    public static AllowableValue fromDescribedValue(final DescribedValue describedValue) {
+        if (describedValue instanceof AllowableValue allowableValue) {
+            return allowableValue;
+        }
+
+        return new AllowableValue(describedValue.getValue(), describedValue.getDisplayName(), describedValue.getDescription());
+    }
+
+    /**
+     * @return the value of this AllowableValue
+     */
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    /**
+     * @return a human-readable name for this AllowableValue
+     */
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    /**
+     * @return a description for this value, or <code>null</code> if no
+     * description was provided
+     */
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * @return true if <code>this</code> is equal to <code>obj</code> of <code>obj</code> is the
+     * same object as <code>this</code> or if <code>obj</code> is an instance of
+     * <code>AllowableValue</code> and both have the same value, or if
+     * <code>obj</code> is a String and is equal to
+     * {@link #getValue() this.getValue()}.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj instanceof AllowableValue other) {
+            return this.value.equals(other.getValue());
+        } else if (obj instanceof String) {
+            return this.value.equals(obj);
+        }
+
+        return false;
+    }
+
+    /**
+     * @return based solely off of the value
+     */
+    @Override
+    public int hashCode() {
+        return 23984731 + 17 * value.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/ClassloaderIsolationKeyProvider.java b/src/main/java/org/apache/nifi/components/ClassloaderIsolationKeyProvider.java
new file mode 100644
index 0000000..9d36193
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/ClassloaderIsolationKeyProvider.java
@@ -0,0 +1,60 @@
+/*
+ * 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.components;
+
+import org.apache.nifi.context.PropertyContext;
+
+/**
+ * <p>
+ * There are times when a component must be created in such a way that each instance gets its own ClassLoader hierarchy,
+ * rather than sharing the ClassLoader with other components (see {@link org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading @RequiresInstanceClassLoading}).
+ * This, however, can be extremely expensive, as all of the classes must be loaded again for each instance of the component. When thousands of these
+ * components are used in a single flow, the startup time can be great, and it can lead to massive amounts of RAM being required.
+ * </p>
+ *
+ * <p>
+ * For components that do require instance ClassLoading that clones ancestor resources, this interface can be optional implemented by the component.
+ * If the interface is implemented, the component is given the opportunity to return a distinct "key" that can be used to identify instances that may share
+ * the same ClassLoader.
+ * </p>
+ */
+public interface ClassloaderIsolationKeyProvider {
+
+    /**
+     * <p>
+     * Determines the key that identifies a shared ClassLoader that this component may use. Any two instances of the same component that return
+     * the same key may be assigned the same base ClassLoader (though it is not guaranteed that this will be the case).
+     * </p>
+     *
+     * <p>
+     * If a subsequent call to this method returns a different value, the component will be recreated with a different ClassLoader.
+     * </p>
+     *
+     * <p>
+     * Implementation Note: for components that implement this interface, this method will be called often. Therefore, performance characteristics
+     * of the implementation are critical. The method is expected to return the value of a configured property or derive a value to return based off
+     * of the values of a few properties. Accessing a remote resource, is too expensive. If the necessary computation is non-trivial, then it should be
+     * performed out of band and the pre-computed value simply returned by this method.
+     * </p>
+     *
+     * @param context the PropertyContext that can be used for determining the key
+     * @return a distinct key that can be used to indicate which shared ClassLoader is allowed to be used
+     */
+    String getClassloaderIsolationKey(PropertyContext context);
+
+}
diff --git a/src/main/java/org/apache/nifi/components/ConfigVerificationResult.java b/src/main/java/org/apache/nifi/components/ConfigVerificationResult.java
new file mode 100644
index 0000000..9f2ab77
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/ConfigVerificationResult.java
@@ -0,0 +1,83 @@
+/*
+ * 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.components;
+
+public class ConfigVerificationResult {
+    private final Outcome outcome;
+    private final String verificationStepName;
+    private final String explanation;
+
+    private ConfigVerificationResult(final Builder builder) {
+        outcome = builder.outcome;
+        verificationStepName = builder.verificationStepName;
+        explanation = builder.explanation;
+    }
+
+    public Outcome getOutcome() {
+        return outcome;
+    }
+
+    public String getVerificationStepName() {
+        return verificationStepName;
+    }
+
+    public String getExplanation() {
+        return explanation;
+    }
+
+    @Override
+    public String toString() {
+        return "ConfigVerificationResult[" +
+            "outcome=" + outcome +
+            ", verificationStepName='" + verificationStepName + "'" +
+            ", explanation='" + explanation + "']";
+    }
+
+    public static class Builder {
+        private Outcome outcome = Outcome.SKIPPED;
+        private String verificationStepName = "Unknown Step Name";
+        private String explanation;
+
+        public Builder outcome(final Outcome outcome) {
+            this.outcome = outcome;
+            return this;
+        }
+
+        public Builder verificationStepName(final String verificationStepName) {
+            this.verificationStepName = verificationStepName;
+            return this;
+        }
+
+        public Builder explanation(final String explanation) {
+            this.explanation = explanation;
+            return this;
+        }
+
+        public ConfigVerificationResult build() {
+            return new ConfigVerificationResult(this);
+        }
+    }
+
+    public enum Outcome {
+        SUCCESSFUL,
+
+        FAILED,
+
+        SKIPPED;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/ConfigurableComponent.java b/src/main/java/org/apache/nifi/components/ConfigurableComponent.java
new file mode 100644
index 0000000..4c5537e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/ConfigurableComponent.java
@@ -0,0 +1,83 @@
+/*
+ * 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.components;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
+
+public interface ConfigurableComponent {
+
+    /**
+     * Validates a set of properties, returning ValidationResults for any
+     * invalid properties. All defined properties will be validated. If they are
+     * not included in the purposed configuration, the default value will
+     * be used.
+     *
+     * @param context of validation
+     * @return Collection of validation result objects for any invalid findings
+     *         only. If the collection is empty then the component is valid. Guaranteed
+     *         non-null
+     */
+    Collection<ValidationResult> validate(ValidationContext context);
+
+    /**
+     * @param name to lookup the descriptor
+     * @return the PropertyDescriptor with the given name, if it exists;
+     * otherwise, returns <code>null</code>
+     */
+    PropertyDescriptor getPropertyDescriptor(String name);
+
+    /**
+     * Hook method allowing subclasses to eagerly react to a configuration
+     * change for the given property descriptor. This method will be invoked
+     * regardless of property validity. As an alternative to using this method,
+     * a component may simply get the latest value whenever it needs it and if
+     * necessary lazily evaluate it. Any throwable that escapes this method will
+     * simply be ignored.
+     *
+     * When NiFi is restarted, this method will be called for each 'dynamic' property that is
+     * added, as well as for each property that is not set to the default value. I.e., if the
+     * Properties are modified from the default values. If it is undesirable for your use case
+     * to react to properties being modified in this situation, you can add the {@link OnConfigurationRestored}
+     * annotation to a method - this will allow the Processor to know when configuration has
+     * been restored, so that it can determine whether or not to perform some action in the
+     * onPropertyModified method.
+     *
+     * @param descriptor the descriptor for the property being modified
+     * @param oldValue the value that was previously set, or null if no value
+     *            was previously set for this property
+     * @param newValue the new property value or if null indicates the property
+     *            was removed
+     */
+    void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue);
+
+    /**
+     * Returns a {@link List} of all {@link PropertyDescriptor}s that this
+     * component supports.
+     *
+     * @return PropertyDescriptor objects this component currently supports
+     */
+    List<PropertyDescriptor> getPropertyDescriptors();
+
+    /**
+     * @return the unique identifier that the framework assigned to this
+     * component
+     */
+    String getIdentifier();
+
+}
diff --git a/src/main/java/org/apache/nifi/components/DescribedValue.java b/src/main/java/org/apache/nifi/components/DescribedValue.java
new file mode 100644
index 0000000..87ffc2f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/DescribedValue.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.components;
+
+/**
+ * Describes a component property value with display name and description.
+ */
+public interface DescribedValue {
+
+    /**
+     * @return the property value as a string
+     */
+    String getValue();
+
+    /**
+     * @return the property display name as a string
+     */
+    String getDisplayName();
+
+    /**
+     * @return the proeprty description as a string
+     */
+    String getDescription();
+}
diff --git a/src/main/java/org/apache/nifi/components/PortFunction.java b/src/main/java/org/apache/nifi/components/PortFunction.java
new file mode 100644
index 0000000..a746eb3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/PortFunction.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.components;
+
+public enum PortFunction {
+    /**
+     * Port functions as a standard port, transferring FlowFiles to all outgoing connections.
+     */
+    STANDARD,
+
+    /**
+     * Port denotes that the invocation of the stateless flow has failed. If run using the Standard Engine, will operate as a Standard port. If running
+     * using the Stateless Engine, the transaction is rolled back, and any input FlowFiles will be transferred to this Port.
+     */
+    FAILURE;
+}
diff --git a/src/main/java/org/apache/nifi/components/PropertyDependency.java b/src/main/java/org/apache/nifi/components/PropertyDependency.java
new file mode 100644
index 0000000..275248e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/PropertyDependency.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.components;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class PropertyDependency {
+    private final String propertyName;
+    private final String displayName;
+    private final Set<String> dependentValues;
+
+    /**
+     * Creates a dependency that is satisfied if any value is set for the property with the given name
+     * @param propertyName the name of the property that is depended upon
+     * @param propertyDisplayName the display name of the property that is depended upon
+     */
+    public PropertyDependency(final String propertyName, final String propertyDisplayName) {
+        this.propertyName = Objects.requireNonNull(propertyName);
+        this.displayName = propertyDisplayName == null ? propertyName : propertyDisplayName;
+        this.dependentValues = null;
+    }
+
+    /**
+     * Creates a dependency that is satisfied only if the property with the given name has a value that is in the given set of dependent values
+     * @param propertyName the name of the property that is depended upon
+     * @param propertyDisplayName the display name of the property that is depended upon
+     * @param dependentValues the values that satisfy the dependency
+     */
+    public PropertyDependency(final String propertyName, final String propertyDisplayName, final Set<String> dependentValues) {
+        this.propertyName = Objects.requireNonNull(propertyName);
+        this.displayName = propertyDisplayName == null ? propertyName : propertyDisplayName;
+        this.dependentValues = Collections.unmodifiableSet(new HashSet<>(Objects.requireNonNull(dependentValues)));
+    }
+
+    /**
+     * @return the name of the property that is depended upon
+     */
+    public String getPropertyName() {
+        return propertyName;
+    }
+
+    /**
+     * @return the display name of the property that is depended upon
+     */
+    public String getPropertyDisplayName() {
+        return displayName;
+    }
+
+    /**
+     * @return the Set of values that satisfy the dependency
+     */
+    public Set<String> getDependentValues() {
+        return dependentValues;
+    }
+
+    @Override
+    public String toString() {
+        return "PropertyDependency[propertyName=" + propertyName + ", displayName=" + displayName + ", dependentValues=" + dependentValues + "]";
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final PropertyDependency that = (PropertyDependency) o;
+        return Objects.equals(getPropertyName(), that.getPropertyName())
+                && Objects.equals(getDependentValues(), that.getDependentValues());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getPropertyName(), getDependentValues());
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/PropertyDescriptor.java b/src/main/java/org/apache/nifi/components/PropertyDescriptor.java
new file mode 100644
index 0000000..8f61545
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/PropertyDescriptor.java
@@ -0,0 +1,911 @@
+/*
+ * 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.components;
+
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceDefinition;
+import org.apache.nifi.components.resource.ResourceReference;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.components.resource.StandardResourceDefinition;
+import org.apache.nifi.components.resource.StandardResourceReferenceFactory;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An immutable object for holding information about a type of component
+ * property.
+ */
+public final class PropertyDescriptor implements Comparable<PropertyDescriptor> {
+
+    public static final PropertyDescriptor NULL_DESCRIPTOR = new PropertyDescriptor.Builder().name("").build();
+
+    /**
+     * The proper name for the property. This is the primary mechanism of
+     * comparing equality.
+     */
+    private final String name;
+
+    /**
+     * The name that should be displayed to user when referencing this property
+     */
+    private final String displayName;
+
+    /**
+     * And explanation of the meaning of the given property. This description is
+     * meant to be displayed to a user or simply provide a mechanism of
+     * documenting intent.
+     */
+    private final String description;
+    /**
+     * The default value for this property
+     */
+    private final String defaultValue;
+    /**
+     * The allowable values for this property. If empty then the allowable
+     * values are not constrained
+     */
+    private final List<AllowableValue> allowableValues;
+    /**
+     * Determines whether the property is required for this processor
+     */
+    private final boolean required;
+    /**
+     * indicates that the value for this property should be considered sensitive
+     * and protected whenever stored or represented
+     */
+    private final boolean sensitive;
+    /**
+     * indicates whether this property well-known for this processor or is
+     * user-defined
+     */
+    private final boolean dynamic;
+    /**
+     * indicates whether or nor this property will evaluate expression language
+     * against the flow file attributes
+     */
+    private final ExpressionLanguageScope expressionLanguageScope;
+    /**
+     * indicates whether or not this property represents resources that should be added
+     * to the classpath and used for loading native libraries for this instance of the component
+     */
+    private final boolean dynamicallyModifiesClasspath;
+
+    /**
+     * the interface of the {@link ControllerService} that this Property refers
+     * to; will be null if not identifying a ControllerService.
+     */
+    private final Class<? extends ControllerService> controllerServiceDefinition;
+
+    /**
+     * The validators that should be used whenever an attempt is made to set
+     * this property value. Any allowable values specified will be checked first
+     * and any validators specified will be ignored.
+     */
+    private final List<Validator> validators;
+
+    /**
+     * The list of dependencies that this property has on other properties
+     */
+    private final Set<PropertyDependency> dependencies;
+
+    /**
+     * The definition of the resource(s) that this property references
+     */
+    private final ResourceDefinition resourceDefinition;
+
+    protected PropertyDescriptor(final Builder builder) {
+        this.displayName = builder.displayName == null ? builder.name : builder.displayName;
+        this.name = builder.name;
+        this.description = builder.description;
+        this.defaultValue = builder.defaultValue;
+        this.allowableValues = builder.allowableValues == null ? null : List.copyOf(builder.allowableValues);
+        this.required = builder.required;
+        this.sensitive = builder.sensitive;
+        this.dynamic = builder.dynamic;
+        this.dynamicallyModifiesClasspath = builder.dynamicallyModifiesClasspath;
+        this.expressionLanguageScope = builder.expressionLanguageScope;
+        this.controllerServiceDefinition = builder.controllerServiceDefinition;
+        this.validators = List.copyOf(builder.validators);
+        this.dependencies = builder.dependencies == null ? Collections.emptySet() : Set.copyOf(builder.dependencies);
+        this.resourceDefinition = builder.resourceDefinition;
+    }
+
+    @Override
+    public int compareTo(final PropertyDescriptor o) {
+        if (o == null) {
+            return -1;
+        }
+        return getName().compareTo(o.getName());
+    }
+
+    /**
+     * Validates the given input against this property descriptor's validator.
+     * If this descriptor has a set of allowable values then the given value is
+     * only checked against the allowable values.
+     *
+     * @param input   the value to validate
+     * @param context the context of validation
+     * @return the result of validating the input
+     */
+    public ValidationResult validate(final String input, final ValidationContext context) {
+        ValidationResult lastResult = Validator.INVALID.validate(this.name, input, context);
+
+        if (allowableValues != null && !allowableValues.isEmpty()) {
+            final ConstrainedSetValidator csValidator = new ConstrainedSetValidator(allowableValues);
+            final ValidationResult csResult = csValidator.validate(this.name, input, context);
+
+            if (csResult.isValid()) {
+                lastResult = csResult;
+            } else {
+                return csResult;
+            }
+        }
+
+        final ResourceDefinition resourceDefinition = getResourceDefinition();
+        if (resourceDefinition != null) {
+            final Validator validator = new ResourceDefinitionValidator(resourceDefinition, this.expressionLanguageScope);
+            final ValidationResult result = validator.validate(this.name, input, context);
+            if (!result.isValid()) {
+                return result;
+            }
+
+            lastResult = result;
+        }
+
+        for (final Validator validator : validators) {
+            lastResult = validator.validate(this.name, input, context);
+            if (!lastResult.isValid()) {
+                break;
+            }
+        }
+
+        if (getControllerServiceDefinition() != null) {
+            final ControllerService service = context.getControllerServiceLookup().getControllerService(input);
+            if (service == null) {
+                return new ValidationResult.Builder()
+                        .input(input)
+                        .subject(getDisplayName())
+                        .valid(false)
+                        .explanation("Property references a Controller Service that does not exist")
+                        .build();
+            } else {
+                return new ValidationResult.Builder()
+                        .valid(true)
+                        .build();
+            }
+        }
+
+        return lastResult;
+    }
+
+    public static final class Builder {
+
+        private String displayName = null;
+        private String name = null;
+        private String description = "";
+        private String defaultValue = null;
+        private List<AllowableValue> allowableValues = null;
+        private Set<PropertyDependency> dependencies = null;
+        private boolean required = false;
+        private boolean sensitive = false;
+        private ExpressionLanguageScope expressionLanguageScope = ExpressionLanguageScope.NONE;
+        private boolean dynamic = false;
+        private boolean dynamicallyModifiesClasspath = false;
+        private Class<? extends ControllerService> controllerServiceDefinition;
+        private ResourceDefinition resourceDefinition;
+        private List<Validator> validators = new ArrayList<>();
+
+        public Builder fromPropertyDescriptor(final PropertyDescriptor specDescriptor) {
+            this.name = specDescriptor.name;
+            this.displayName = specDescriptor.displayName;
+            this.description = specDescriptor.description;
+            this.defaultValue = specDescriptor.defaultValue;
+            this.allowableValues = specDescriptor.allowableValues == null ? null : new ArrayList<>(specDescriptor.allowableValues);
+            this.required = specDescriptor.required;
+            this.sensitive = specDescriptor.sensitive;
+            this.dynamic = specDescriptor.dynamic;
+            this.dynamicallyModifiesClasspath = specDescriptor.dynamicallyModifiesClasspath;
+            this.expressionLanguageScope = specDescriptor.expressionLanguageScope;
+            this.controllerServiceDefinition = specDescriptor.getControllerServiceDefinition();
+            this.validators = new ArrayList<>(specDescriptor.validators);
+            this.dependencies = new HashSet<>(specDescriptor.dependencies);
+            this.resourceDefinition = specDescriptor.resourceDefinition;
+            return this;
+        }
+
+        /**
+         * Sets a unique id for the property. This field is optional and if not
+         * specified the PropertyDescriptor's name will be used as the
+         * identifying attribute. However, by supplying an id, the
+         * PropertyDescriptor's name can be changed without causing problems.
+         * This is beneficial because it allows a User Interface to represent
+         * the name differently.
+         *
+         * @param displayName of the property
+         * @return the builder
+         */
+        public Builder displayName(final String displayName) {
+            if (null != displayName) {
+                this.displayName = displayName;
+            }
+
+            return this;
+        }
+
+        /**
+         * Sets the property name.
+         *
+         * @param name of the property
+         * @return the builder
+         */
+        public Builder name(final String name) {
+            if (null != name) {
+                this.name = name;
+            }
+            return this;
+        }
+
+        /**
+         * Sets the scope of the expression language evaluation
+         *
+         * @param expressionLanguageScope scope of the expression language evaluation
+         * @return the builder
+         */
+        public Builder expressionLanguageSupported(final ExpressionLanguageScope expressionLanguageScope) {
+            this.expressionLanguageScope = expressionLanguageScope;
+            return this;
+        }
+
+        /**
+         * @param description of the property
+         * @return the builder
+         */
+        public Builder description(final String description) {
+            if (null != description) {
+                this.description = description;
+            }
+            return this;
+        }
+
+        /**
+         * Specifies the initial value and the default value that will be used if the user does not specify a value.
+         * <p>
+         * When {@link #build()} is called, if Allowable Values have been set (see {@link #allowableValues(DescribedValue...)} and overloads)
+         * and this value is not one of those Allowable Values, an Exception will be thrown.
+         * If the Allowable Values have been set, the default value should be set to
+         * the "Value" of the {@link DescribedValue} object (see {@link DescribedValue#getValue()}).
+         * There's an overload available for this (see {@link #defaultValue(DescribedValue)}).
+         *
+         * @param value default value
+         * @return the builder
+         */
+        public Builder defaultValue(final String value) {
+            if (null != value) {
+                this.defaultValue = value;
+            }
+            return this;
+        }
+
+        /**
+         * Specifies the initial value and the default value that will be used if the user does not specify a value.
+         * <p>
+         * Sets the default value to the "Value" of the {@link DescribedValue} object.
+         * When {@link #build()} is called, if Allowable Values have been set (see {@link #allowableValues(DescribedValue...)} and overloads)
+         * and this value is not one of those Allowable Values, an Exception will be thrown.
+         * In case there is not a restricted set of Allowable Values {@link #defaultValue(String)} may be used.
+         *
+         * @param value default value holder
+         * @return the builder
+         */
+        public Builder defaultValue(final DescribedValue value) {
+            return defaultValue(value != null ? value.getValue() : null);
+        }
+
+        /**
+         * Clears the initial value and default value from this Property.
+         *
+         * @return the builder
+         */
+        public Builder clearDefaultValue() {
+            this.defaultValue = null;
+            return this;
+        }
+
+        public Builder dynamic(final boolean dynamic) {
+            this.dynamic = dynamic;
+            return this;
+        }
+
+        /**
+         * Specifies that the value of this property represents one or more resources that the
+         * framework should add to the classpath of as well as consider when looking for native
+         * libraries for the given component.
+         * <p>
+         * NOTE: If a component contains a PropertyDescriptor where dynamicallyModifiesClasspath is set to true,
+         * the component may also be annotated with @RequiresInstanceClassloading, in which case every class will
+         * be loaded by a separate InstanceClassLoader for each processor instance.<br/>
+         * It also allows to load native libraries from the extra classpath.
+         * <p>
+         * One can choose to omit the annotation. In this case the loading of native libraries from the extra classpath
+         * is not supported.
+         * Also by default, classes will be loaded by a common NarClassLoader, however it's possible to acquire an
+         * InstanceClassLoader by calling Thread.currentThread().getContextClassLoader() which can be used manually
+         * to load required classes on an instance-by-instance basis
+         * (by calling {@link Class#forName(String, boolean, ClassLoader)} for example).
+         * <p>
+         * Any property descriptor that dynamically modifies the classpath should also make use of the {@link #identifiesExternalResource(ResourceCardinality, ResourceType, ResourceType...)} method
+         * to indicate that the property descriptor references external resources and optionally restrict which types of resources and how many resources the property allows.
+         *
+         * @param dynamicallyModifiesClasspath whether or not this property should be used by the framework to modify the classpath
+         * @return the builder
+         */
+        public Builder dynamicallyModifiesClasspath(final boolean dynamicallyModifiesClasspath) {
+            this.dynamicallyModifiesClasspath = dynamicallyModifiesClasspath;
+            return this;
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         *
+         * @param values constrained set of values
+         * @return the builder
+         */
+        public Builder allowableValues(final Set<String> values) {
+            if (null != values) {
+                this.allowableValues = values.stream().map(AllowableValue::new).toList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         * <p>
+         * Uses the {@link Enum#name()} of each value as "Value" for the {@link AllowableValue}.
+         * In case the enum value is a {@link DescribedValue}, uses the information provided instead
+         * (see {@link DescribedValue#getValue()}, {@link DescribedValue#getDisplayName()}, {@link DescribedValue#getDescription()}).
+         *
+         * @param <E> Type of Allowable Value
+         * @param values constrained set of values
+         * @return the builder
+         */
+        public <E extends Enum<E>> Builder allowableValues(final E[] values) {
+            if (null != values) {
+                this.allowableValues = Arrays.stream(values)
+                        .map(enumValue -> enumValue instanceof DescribedValue describedValue
+                                ? AllowableValue.fromDescribedValue(describedValue) : new AllowableValue(enumValue.name()))
+                        .toList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         * <p>
+         * Uses the {@link Enum#name()} of each value from {@link Class#getEnumConstants()} as "Value" for the {@link AllowableValue}.
+         * In case the enum value is a {@link DescribedValue}, uses the information provided instead
+         * (see {@link DescribedValue#getValue()}, {@link DescribedValue#getDisplayName()}, {@link DescribedValue#getDescription()}).
+         *
+         * @param enumClass an enum class that contains a set of values and optionally implements the DescribedValue interface
+         * @param <E>       generic parameter for an enum class, that may implement the DescribedValue interface
+         * @return the builder
+         */
+        public <E extends Enum<E>> Builder allowableValues(final Class<E> enumClass) {
+            return allowableValues(enumClass.getEnumConstants());
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         * <p>
+         * Uses the {@link Enum#name()} of each value of the {@link EnumSet} as "Value" for the {@link AllowableValue}.
+         * In case the enum value is a {@link DescribedValue}, uses the information provided instead
+         * (see {@link DescribedValue#getValue()}, {@link DescribedValue#getDisplayName()}, {@link DescribedValue#getDescription()}).
+         *
+         * @param enumValues an enum set that contains a set of values and optionally implements the DescribedValue interface
+         * @param <E>        generic parameter for an enum class, that may implement the DescribedValue interface
+         * @return the builder
+         */
+        public <E extends Enum<E>> Builder allowableValues(final EnumSet<E> enumValues) {
+            if (null != enumValues) {
+                this.allowableValues = enumValues.stream()
+                        .map(enumValue -> enumValue instanceof DescribedValue describedValue
+                                ? AllowableValue.fromDescribedValue(describedValue) : new AllowableValue(enumValue.name()))
+                        .toList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         *
+         * @param values constrained set of values
+         * @return the builder
+         */
+        public Builder allowableValues(final String... values) {
+            if (null != values) {
+                this.allowableValues = Arrays.stream(values).map(AllowableValue::new).toList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the Allowable Values for this Property.
+         * <p>
+         * Uses the information provided by each {@link DescribedValue} (see {@link DescribedValue#getValue()}, {@link DescribedValue#getDisplayName()},
+         * {@link DescribedValue#getDescription()}) to populate the {@link AllowableValue}s.
+         *
+         * @param values constrained set of values
+         * @return the builder
+         */
+        public Builder allowableValues(final DescribedValue... values) {
+            if (null != values) {
+                this.allowableValues = Arrays.stream(values).map(AllowableValue::fromDescribedValue).toList();
+            }
+            return this;
+        }
+
+        /**
+         * Clears all Allowable Values from this Property
+         *
+         * @return the builder
+         */
+        public Builder clearAllowableValues() {
+            this.allowableValues = null;
+            return this;
+        }
+
+        /**
+         * @param required true if yes; false otherwise
+         * @return the builder
+         */
+        public Builder required(final boolean required) {
+            this.required = required;
+            return this;
+        }
+
+        /**
+         * @param sensitive true if sensitive; false otherwise
+         * @return the builder
+         */
+        public Builder sensitive(final boolean sensitive) {
+            this.sensitive = sensitive;
+            return this;
+        }
+
+        /**
+         * @param validator for the property
+         * @return the builder
+         */
+        public Builder addValidator(final Validator validator) {
+            if (validator != null) {
+                validators.add(validator);
+            }
+            return this;
+        }
+
+        /**
+         * Clear all Validators from this Property
+         *
+         * @return the builder
+         */
+        public Builder clearValidators() {
+            validators.clear();
+            return this;
+        }
+
+        /**
+         * Specifies that this property provides the identifier of a Controller
+         * Service that implements the given interface
+         *
+         * @param controllerServiceDefinition the interface that is implemented
+         *                                    by the Controller Service
+         * @return the builder
+         */
+        public Builder identifiesControllerService(final Class<? extends ControllerService> controllerServiceDefinition) {
+            if (controllerServiceDefinition != null) {
+                this.controllerServiceDefinition = controllerServiceDefinition;
+            }
+            return this;
+        }
+
+        private boolean isValueAllowed(final String value) {
+            if (allowableValues == null || value == null) {
+                return true;
+            }
+
+            for (final AllowableValue allowableValue : allowableValues) {
+                if (allowableValue.getValue().equals(value)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Specifies that this property references one or more resources that are external to NiFi that the component is meant to consume.
+         * Any property descriptor that identifies an external resource will be automatically validated against the following rules:
+         * <ul>
+         *     <li>If the ResourceCardinality is SINGLE, the given property value must be a file, a directory, or a URL that uses a protocol of http/https/file.</li>
+         *     <li>The given resourceTypes dictate which types of input are allowed. For example, if <code>identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE)</code>
+         *     is used, the input must be a regular file. If <code>identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE, ResourceType.DIRECTORY)</code> is used, then the input
+         *     must be exactly one file OR directory.
+         *     </li>
+         *     <li>If the ResourceCardinality is MULTIPLE, the given property value may consist of one or more resources, each separted by a comma and optional white space.</li>
+         * </ul>
+         * <p>
+         * Generally, any property descriptor that makes use of the {@link #dynamicallyModifiesClasspath(boolean)} method to dynamically update its classpath should also
+         * make use of this method, specifying which types of resources are allowed and how many.
+         *
+         * @param cardinality             specifies how many resources the property should allow
+         * @param resourceType            the type of resource that is allowed
+         * @param additionalResourceTypes if more than one type of resource is allowed, any resource type in addition to the given resource type may be provided
+         * @return the builder
+         */
+        public Builder identifiesExternalResource(final ResourceCardinality cardinality, final ResourceType resourceType, final ResourceType... additionalResourceTypes) {
+            Objects.requireNonNull(cardinality);
+            Objects.requireNonNull(resourceType);
+
+            final Set<ResourceType> resourceTypes = new HashSet<>();
+            resourceTypes.add(resourceType);
+            resourceTypes.addAll(Arrays.asList(additionalResourceTypes));
+
+            this.resourceDefinition = new StandardResourceDefinition(cardinality, resourceTypes);
+            return this;
+        }
+
+        /**
+         * Establishes a relationship between this Property and the given property by declaring that this Property is only relevant if the given Property has a non-null value.
+         * Furthermore, if one or more explicit Allowable Values are provided, this Property will not be relevant unless the given Property's value is equal to one of the given Allowable Values.
+         * If this method is called multiple times, each with a different dependency, then a relationship is established such that this Property is relevant only if all dependencies are satisfied.
+         * <p>
+         * In the case that this property is NOT considered to be relevant (meaning that it depends on a property whose value is not specified, or whose value does not match one of the given
+         * Allowable Values), the property will not be shown in the component's configuration in the User Interface. Additionally, this property's value will not be considered for
+         * validation. That is, if this property is configured with an invalid value and this property depends on Property Foo, and Property Foo does not have a value set, then the component
+         * will still be valid, because the value of this property is irrelevant.
+         * <p>
+         * If the given property is not relevant (because its dependencies are not satisfied), this property is also considered not to be valid.
+         *
+         * @param property        the property that must be set in order for this property to become relevant
+         * @param dependentValues the possible values for the given property for which this Property is relevant
+         * @return the builder
+         */
+        public Builder dependsOn(final PropertyDescriptor property, final AllowableValue... dependentValues) {
+            if (dependencies == null) {
+                dependencies = new HashSet<>();
+            }
+
+            if (dependentValues.length == 0) {
+                dependencies.add(new PropertyDependency(property.getName(), property.getDisplayName()));
+            } else {
+                final Set<String> dependentValueSet = new HashSet<>();
+                for (final AllowableValue value : dependentValues) {
+                    dependentValueSet.add(value.getValue());
+                }
+
+                dependencies.add(new PropertyDependency(property.getName(), property.getDisplayName(), dependentValueSet));
+            }
+
+            return this;
+        }
+
+        /**
+         * Establishes a relationship between this Property and the given property by declaring that this Property is only relevant if the given Property has a value equal to one of the given
+         * <code>String</code> arguments.
+         * If this method is called multiple times, each with a different dependency, then a relationship is established such that this Property is relevant only if all dependencies are satisfied.
+         * <p>
+         * In the case that this property is NOT considered to be relevant (meaning that it depends on a property whose value is not specified, or whose value does not match one of the given
+         * Allowable Values), the property will not be shown in the component's configuration in the User Interface. Additionally, this property's value will not be considered for
+         * validation. That is, if this property is configured with an invalid value and this property depends on Property Foo, and Property Foo does not have a value set, then the component
+         * will still be valid, because the value of this property is irrelevant.
+         * <p>
+         * If the given property is not relevant (because its dependencies are not satisfied), this property is also considered not to be valid.
+         *
+         * @param property                  the property that must be set in order for this property to become relevant
+         * @param firstDependentValue       the first value for the given property for which this Property is relevant
+         * @param additionalDependentValues any other values for the given property for which this Property is relevant
+         * @return the builder
+         */
+        public Builder dependsOn(final PropertyDescriptor property, final String firstDependentValue, final String... additionalDependentValues) {
+            final AllowableValue[] dependentValues = new AllowableValue[additionalDependentValues.length + 1];
+            dependentValues[0] = new AllowableValue(firstDependentValue);
+            int i = 1;
+            for (final String additionalDependentValue : additionalDependentValues) {
+                dependentValues[i++] = new AllowableValue(additionalDependentValue);
+            }
+
+            return dependsOn(property, dependentValues);
+        }
+
+        /**
+         * Establishes a relationship between this Property and the given property by declaring that this Property is only relevant if the given Property has a value equal to one of the given
+         * {@link DescribedValue} arguments.
+         * If this method is called multiple times, each with a different dependency, then a relationship is established such that this Property is relevant only if all dependencies are satisfied.
+         * <p>
+         * In the case that this property is NOT considered to be relevant (meaning that it depends on a property whose value is not specified, or whose value does not match one of the given
+         * Described Values), the property will not be shown in the component's configuration in the User Interface. Additionally, this property's value will not be considered for
+         * validation. That is, if this property is configured with an invalid value and this property depends on Property Foo, and Property Foo does not have a value set, then the component
+         * will still be valid, because the value of this property is irrelevant.
+         * <p>
+         * If the given property is not relevant (because its dependencies are not satisfied), this property is also considered not to be valid.
+         *
+         * @param property                  the property that must be set in order for this property to become relevant
+         * @param firstDependentValue       the first value for the given property for which this Property is relevant
+         * @param additionalDependentValues any other values for the given property for which this Property is relevant
+         * @return the builder
+         */
+        public Builder dependsOn(final PropertyDescriptor property, final DescribedValue firstDependentValue, final DescribedValue... additionalDependentValues) {
+            final AllowableValue[] dependentValues = new AllowableValue[additionalDependentValues.length + 1];
+            dependentValues[0] = AllowableValue.fromDescribedValue(firstDependentValue);
+            int i = 1;
+            for (final DescribedValue additionalDependentValue : additionalDependentValues) {
+                dependentValues[i++] = AllowableValue.fromDescribedValue(additionalDependentValue);
+            }
+
+            return dependsOn(property, dependentValues);
+        }
+
+        /**
+         * Clear all Dependencies from this Property
+         *
+         * @return the builder
+         */
+        public Builder clearDependsOn() {
+            this.dependencies = new HashSet<>();
+            return this;
+        }
+
+        /**
+         * @return a PropertyDescriptor as configured
+         * @throws IllegalStateException if allowable values are configured but
+         *                               no default value is set, or the default value is not contained within
+         *                               the allowable values.
+         */
+        public PropertyDescriptor build() {
+            if (name == null) {
+                throw new IllegalStateException("Must specify a name");
+            }
+            if (!isValueAllowed(defaultValue)) {
+                throw new IllegalStateException("Default value [" + defaultValue + "] is not in the set of allowable values");
+            }
+
+            return new PropertyDescriptor(this);
+        }
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    public boolean isRequired() {
+        return required;
+    }
+
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+    public boolean isExpressionLanguageSupported() {
+        return !expressionLanguageScope.equals(ExpressionLanguageScope.NONE);
+    }
+
+    public ExpressionLanguageScope getExpressionLanguageScope() {
+        return expressionLanguageScope;
+    }
+
+    public boolean isDynamicClasspathModifier() {
+        return dynamicallyModifiesClasspath;
+    }
+
+    public Class<? extends ControllerService> getControllerServiceDefinition() {
+        return controllerServiceDefinition;
+    }
+
+    public List<Validator> getValidators() {
+        return validators;
+    }
+
+    public List<AllowableValue> getAllowableValues() {
+        return allowableValues;
+    }
+
+    public Set<PropertyDependency> getDependencies() {
+        return dependencies;
+    }
+
+    public ResourceDefinition getResourceDefinition() {
+        return resourceDefinition;
+    }
+
+    @Override
+    public boolean equals(final Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other instanceof PropertyDescriptor otherPropertyDescriptor) {
+            return this.name.equals(otherPropertyDescriptor.name);
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return 287 + this.name.hashCode() * 47;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + displayName + "]";
+    }
+
+    private static final class ConstrainedSetValidator implements Validator {
+
+        private static final String POSITIVE_EXPLANATION = "Given value found in allowed set";
+        private static final String NEGATIVE_EXPLANATION = "Given value not found in allowed set '%1$s'";
+        private static final String VALUE_DELIMITER = ", ";
+        private final String validStrings;
+        private final Collection<String> validValues;
+
+        /**
+         * Constructs a validator that will check if the given value is in the
+         * given set.
+         *
+         * @param validValues values which are acceptible
+         * @throws NullPointerException if the given validValues is null
+         */
+        private ConstrainedSetValidator(final Collection<AllowableValue> validValues) {
+            this.validValues = validValues.stream().map(AllowableValue::getValue).toList();
+            this.validStrings = String.join(VALUE_DELIMITER, this.validValues);
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            final ValidationResult.Builder builder = new ValidationResult.Builder();
+            builder.input(input);
+            builder.subject(subject);
+            if (validValues.contains(input)) {
+                builder.valid(true);
+                builder.explanation(POSITIVE_EXPLANATION);
+            } else {
+                builder.valid(false);
+                builder.explanation(String.format(NEGATIVE_EXPLANATION, validStrings));
+            }
+            return builder.build();
+        }
+    }
+
+    private static class ResourceDefinitionValidator implements Validator {
+        private final ResourceDefinition resourceDefinition;
+        private final ExpressionLanguageScope expressionLanguageScope;
+
+        public ResourceDefinitionValidator(final ResourceDefinition resourceDefinition, final ExpressionLanguageScope expressionLanguageScope) {
+            this.resourceDefinition = resourceDefinition;
+            this.expressionLanguageScope = expressionLanguageScope;
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String configuredInput, final ValidationContext context) {
+            final ValidationResult.Builder resultBuilder = new ValidationResult.Builder()
+                    .input(configuredInput)
+                    .subject(subject);
+
+            if (configuredInput == null) {
+                return resultBuilder.valid(false)
+                        .explanation("No value specified")
+                        .build();
+            }
+
+            // If Expression Language is supported and is used in the property value, we cannot perform validation against the configured
+            // input unless the Expression Language is expressly limited to only env/syst properties variables. In that case, we can evaluate
+            // it and then validate the value after evaluating the Expression Language.
+            String input = configuredInput;
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(configuredInput)) {
+                if (expressionLanguageScope != null && expressionLanguageScope == ExpressionLanguageScope.ENVIRONMENT) {
+                    input = context.newPropertyValue(configuredInput).evaluateAttributeExpressions().getValue();
+                    resultBuilder.input(input);
+                } else {
+                    return resultBuilder.valid(true)
+                            .explanation("Expression Language is present, so validation of property value cannot be performed")
+                            .build();
+                }
+            }
+
+            // If the property can be text, then there's nothing to validate. Anything that is entered may be valid.
+            // This will be improved in the future, by allowing the user to specify the type of resource that is being referenced.
+            // Until then, we will simply require that the component perform any necessary validation.
+            final boolean allowsText = resourceDefinition.getResourceTypes().contains(ResourceType.TEXT);
+            if (allowsText) {
+                return resultBuilder.valid(true)
+                        .explanation("Property allows for Resource Type of Text, so validation of property value cannot be performed")
+                        .build();
+            }
+
+            final String[] splits = input.split(",");
+            if (resourceDefinition.getCardinality() == ResourceCardinality.SINGLE && splits.length > 1) {
+                return resultBuilder.valid(false)
+                        .explanation("Property only supports a single Resource but " + splits.length + " resources were specified")
+                        .build();
+            }
+
+            final Set<ResourceType> resourceTypes = resourceDefinition.getResourceTypes();
+            final List<String> nonExistentResources = new ArrayList<>();
+
+            int count = 0;
+            for (final String split : splits) {
+                final ResourceReference resourceReference = new StandardResourceReferenceFactory().createResourceReference(split, resourceDefinition);
+                if (resourceReference == null) {
+                    continue;
+                }
+
+                count++;
+
+                final boolean accessible = resourceReference.isAccessible();
+                if (!accessible) {
+                    nonExistentResources.add(resourceReference.getLocation());
+                    continue;
+                }
+
+                if (!resourceTypes.contains(resourceReference.getResourceType())) {
+                    return resultBuilder.valid(false)
+                            .explanation("Specified Resource is a " + resourceReference.getResourceType().name() + " but this property does not allow this type of resource")
+                            .build();
+                }
+            }
+
+            if (count == 0) {
+                return resultBuilder.valid(false)
+                        .explanation("No resources were specified")
+                        .build();
+            }
+
+            if (!nonExistentResources.isEmpty()) {
+                return resultBuilder.valid(false)
+                        .explanation("The specified resource(s) do not exist or could not be accessed: " + nonExistentResources)
+                        .build();
+            }
+
+            return resultBuilder.valid(true)
+                    .build();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/PropertyValue.java b/src/main/java/org/apache/nifi/components/PropertyValue.java
new file mode 100644
index 0000000..4e52c54
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/PropertyValue.java
@@ -0,0 +1,371 @@
+/*
+ * 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.components;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.nifi.components.resource.ResourceReference;
+import org.apache.nifi.components.resource.ResourceReferences;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.expression.AttributeValueDecorator;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.registry.EnvironmentVariables;
+
+/**
+ * <p>
+ * A PropertyValue provides a mechanism whereby the currently configured value
+ * of a processor property can be obtained in different forms.
+ * </p>
+ */
+public interface PropertyValue {
+
+    /**
+     * @return the raw property value as a string
+     */
+    String getValue();
+
+    /**
+     * @return an integer representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Integer asInteger();
+
+    /**
+     * @return a Long representation of the property value, or <code>null</code>
+     * if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Long asLong();
+
+    /**
+     * @return a Boolean representation of the property value, or
+     * <code>null</code> if not set
+     */
+    Boolean asBoolean();
+
+    /**
+     * @return a Float representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Float asFloat();
+
+    /**
+     * @return a Double representation of the property value, of
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Double asDouble();
+
+    /**
+     * @param timeUnit specifies the TimeUnit to convert the time duration into
+     * @return a Long value representing the value of the configured time period
+     * in terms of the specified TimeUnit; if the property is not set, returns
+     * <code>null</code>
+     */
+    Long asTimePeriod(TimeUnit timeUnit);
+
+    /**
+     * Returns the value as a Duration
+     *
+     * @return a Duration representing the value, or <code>null</code> if the value is unset
+     */
+    Duration asDuration();
+
+    /**
+     *
+     * @param dataUnit specifies the DataUnit to convert the data size into
+     * @return a Long value representing the value of the configured data size
+     * in terms of the specified DataUnit; if hte property is not set, returns
+     * <code>null</code>
+     */
+    Double asDataSize(DataUnit dataUnit);
+
+    /**
+     * @return the ControllerService whose identifier is the raw value of
+     * <code>this</code>, or <code>null</code> if either the value is not set or
+     * the value does not identify a ControllerService
+     */
+    ControllerService asControllerService();
+
+    /**
+     * @param <T> the generic type of the controller service
+     * @param serviceType the class of the Controller Service
+     * @return the ControllerService whose identifier is the raw value of the
+     * <code>this</code>, or <code>null</code> if either the value is not set or
+     * the value does not identify a ControllerService. The object returned by
+     * this method is explicitly cast to type specified, if the type specified
+     * is valid. Otherwise, throws an IllegalArgumentException
+     *
+     * @throws IllegalArgumentException if the value of <code>this</code> points
+     * to a ControllerService but that service is not of type
+     * <code>serviceType</code> or if <code>serviceType</code> references a
+     * class that is not an interface
+     */
+    <T extends ControllerService> T asControllerService(Class<T> serviceType) throws IllegalArgumentException;
+
+    /**
+     * @return a ResourceReference for the configured property value, or <code>null</code> if no value was specified, or if the property references multiple resources.
+     * @see #asResources()
+     */
+    ResourceReference asResource();
+
+    /**
+     * @return a ResourceReferences for the configured property value. If no property value is set, a ResourceReferences will be returned that references no resources.
+     * I.e., this method will never return <code>null</code>.
+     */
+    ResourceReferences asResources();
+
+    /**
+     * Returns the property value as one of the configured allowableValues, see {@link PropertyDescriptor.Builder#allowableValues(Class)}
+     * <p>
+     * The {@link Class#getEnumConstants()} of the provided enum are searched for an entry matching the value of <code>this</code>.
+     * In case an enum value is a {@link DescribedValue}, uses the defined {@link DescribedValue#getValue()} for comparison.
+     * Otherwise, the {@link Enum#name()} is used.
+     *
+     * @param <E> the generic type of the enum used as allowableValues
+     * @param enumType the class of the enum used as allowableValues
+     * @return the matching enum entry, or <code>null</code> if the value is not set.
+     * @throws IllegalArgumentException if no enum entry matching the value of <code>this</code> is found.
+     */
+    <E extends Enum<E>> E asAllowableValue(Class<E> enumType) throws IllegalArgumentException;
+
+    /**
+     * @return <code>true</code> if the user has configured a value, or if the
+     * {@link PropertyDescriptor} for the associated property has a default
+     * value, <code>false</code> otherwise
+     */
+    boolean isSet();
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language;
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @return a PropertyValue with the new value is returned, supporting call
+     * chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions() throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language;
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param attributes a Map of attributes that the Expression can reference.
+     * These will take precedence over any underlying env/syst properties values.
+     *
+     * @return a PropertyValue with the new value
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(Map<String, String> attributes) throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language.
+     * The supplied decorator is then given a chance to decorate the value, and
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param attributes a Map of attributes that the Expression can reference.
+     * These will take precedence over any variables in any underlying variable
+     * registry.
+     * @param decorator the decorator to use in order to update the values
+     * returned after variable substitution and expression language evaluation.
+     *
+     * @return a PropertyValue with the new value
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(Map<String, String> attributes, AttributeValueDecorator decorator) throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language;
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param flowFile to evaluate attributes of. It's flow file properties and
+     * then flow file attributes will take precedence over any underlying
+     * env/syst properties.
+     * @return a PropertyValue with the new value is returned, supporting call
+     * chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(FlowFile flowFile) throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language;
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param flowFile to evaluate attributes of. It's flow file properties and
+     * then flow file attributes will take precedence over any underlying
+     * env/syst properties.
+     * @param additionalAttributes a Map of additional attributes that the
+     * Expression can reference. These attributes will take precedence over any
+     * conflicting attributes in the provided flowfile or any underlying
+     * env/syst properties.
+     *
+     * @return a PropertyValue with the new value is returned, supporting call
+     * chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(FlowFile flowFile, Map<String, String> additionalAttributes) throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language;
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param flowFile to evaluate attributes of. It's flow file properties and
+     * then flow file attributes will take precedence over any underlying
+     * env/syst properties.
+     * @param additionalAttributes a Map of additional attributes that the
+     * Expression can reference. These attributes will take precedence over any
+     * conflicting attributes in the provided flowfile or any underlying
+     * env/syst properties.
+     * @param decorator the decorator to use in order to update the values
+     * returned after variable substitution and expression language evaluation.
+     *
+     * @return a PropertyValue with the new value is returned, supporting call
+     * chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(FlowFile flowFile, Map<String, String> additionalAttributes, AttributeValueDecorator decorator) throws ProcessException;
+
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression
+     * Language; a PropertyValue with the new value is then returned, supporting
+     * call chaining.
+     * </p>
+     *
+     * @param flowFile to evaluate attributes of
+     * @param additionalAttributes a Map of additional attributes that the Expression can reference. If entries in
+     * this Map conflict with entries in the FlowFile's attributes, the entries in this Map are given a higher priority.
+     * @param decorator the decorator to use in order to update the values returned by the Expression Language
+     * @param stateValues a Map of the state values to be referenced explicitly by specific state accessing functions
+     *
+     * @return a PropertyValue with the new value is returned, supporting call
+     * chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or evaluating
+     * the Expression against the given attributes causes an Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(FlowFile flowFile, Map<String, String> additionalAttributes, AttributeValueDecorator decorator, Map<String, String> stateValues)
+            throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language.
+     * The supplied decorator is then given a chance to decorate the value, and
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param decorator the decorator to use in order to update the values
+     * returned after variable substitution and expression language evaluation.
+     * @return a PropertyValue with the new value is then returned, supporting
+     * call chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(AttributeValueDecorator decorator) throws ProcessException;
+
+    /**
+     * <p>
+     * Replaces values in the Property Value using the NiFi Expression Language.
+     * The supplied decorator is then given a chance to decorate the value, and
+     * a PropertyValue with the new value is then returned, supporting call
+     * chaining. Before executing the expression language statement any
+     * variables names found within any underlying {@link EnvironmentVariables} will
+     * be substituted with their values.
+     * </p>
+     *
+     * @param flowFile to evaluate expressions against
+     * @param decorator the decorator to use in order to update the values
+     * returned after variable substitution and expression language evaluation.
+     *
+     *
+     * @return a PropertyValue with the new value is then returned, supporting
+     * call chaining
+     *
+     * @throws ProcessException if the Expression cannot be compiled or
+     * evaluating the Expression against the given attributes causes an
+     * Exception to be thrown
+     */
+    PropertyValue evaluateAttributeExpressions(FlowFile flowFile, AttributeValueDecorator decorator) throws ProcessException;
+
+    /**
+     * <p>
+     * Indicates whether the value of the property uses Expression Language.
+     * </p>
+     *
+     * @return <code>true</code> if the property value makes use of the Expression Language, <code>false</code> otherwise.
+     */
+    boolean isExpressionLanguagePresent();
+}
diff --git a/src/main/java/org/apache/nifi/components/RequiredPermission.java b/src/main/java/org/apache/nifi/components/RequiredPermission.java
new file mode 100644
index 0000000..81c9ded
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/RequiredPermission.java
@@ -0,0 +1,57 @@
+/*
+ * 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.components;
+
+import java.util.Arrays;
+
+/**
+ * Enumeration of permission required for annotated components
+ */
+public enum RequiredPermission {
+    READ_FILESYSTEM("read-filesystem", "read filesystem"),
+    WRITE_FILESYSTEM("write-filesystem", "write filesystem"),
+    READ_DISTRIBUTED_FILESYSTEM("read-distributed-filesystem", "read distributed filesystem"),
+    WRITE_DISTRIBUTED_FILESYSTEM("write-distributed-filesystem", "write distributed filesystem"),
+    EXECUTE_CODE("execute-code", "execute code"),
+    ACCESS_KEYTAB("access-keytab", "access keytab"),
+    ACCESS_TICKET_CACHE("access-ticket-cache", "access ticket cache"),
+    ACCESS_ENVIRONMENT_CREDENTIALS("access-environment-credentials", "access environment credentials"),
+    EXPORT_NIFI_DETAILS("export-nifi-details", "export nifi details"),
+    REFERENCE_REMOTE_RESOURCES("reference-remote-resources", "reference remote resources");
+
+    private String permissionIdentifier;
+    private String permissionLabel;
+
+    RequiredPermission(String permissionIdentifier, String permissionLabel) {
+        this.permissionIdentifier = permissionIdentifier;
+        this.permissionLabel = permissionLabel;
+    }
+
+    public String getPermissionIdentifier() {
+        return permissionIdentifier;
+    }
+
+    public String getPermissionLabel() {
+        return permissionLabel;
+    }
+
+    public static RequiredPermission valueOfPermissionIdentifier(final String permissionIdentifier) {
+        return Arrays.stream(values())
+                .filter(candidate -> candidate.getPermissionIdentifier().equals(permissionIdentifier))
+                .findFirst().orElse(null);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/ValidationContext.java b/src/main/java/org/apache/nifi/components/ValidationContext.java
new file mode 100644
index 0000000..031d39a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/ValidationContext.java
@@ -0,0 +1,203 @@
+/*
+ * 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.components;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.expression.ExpressionLanguageCompiler;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+public interface ValidationContext extends PropertyContext {
+
+    /**
+     * @return the {@link ControllerServiceLookup} which can be used to obtain
+     * Controller Services
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @param controllerService to lookup the validation context of
+     * @return a ValidationContext that is appropriate for validating the given
+     * {@link ControllerService}
+     */
+    ValidationContext getControllerServiceValidationContext(ControllerService controllerService);
+
+    /**
+     * @return a new {@link ExpressionLanguageCompiler} that can be used to
+     * compile and evaluate Attribute Expressions
+     */
+    ExpressionLanguageCompiler newExpressionLanguageCompiler();
+
+    /**
+     * @param value to make a PropertyValue object for
+     * @return a PropertyValue that represents the given value
+     */
+    PropertyValue newPropertyValue(String value);
+
+    /**
+     * @return a Map of all configured Properties
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return the currently configured Annotation Data
+     */
+    String getAnnotationData();
+
+    /**
+     * There are times when the framework needs to consider a component valid,
+     * even if it references an invalid ControllerService. This method will
+     * return <code>false</code> if the component is to be considered valid even
+     * if the given Controller Service is referenced and is invalid.
+     *
+     * @param service to check if validation is required
+     * @return <code>false</code> if the component is to be considered valid
+     * even if the given Controller Service is referenced and is invalid
+     */
+    boolean isValidationRequired(ControllerService service);
+
+    /**
+     * @param value to test whether expression language is present
+     * @return <code>true</code> if the given value contains a NiFi Expression
+     * Language expression, <code>false</code> if it does not
+     */
+    boolean isExpressionLanguagePresent(String value);
+
+    /**
+     * @param propertyName to test whether expression language is supported
+     * @return <code>true</code> if the property with the given name supports
+     * the NiFi Expression Language, <code>false</code> if the property does not
+     * support the Expression Language or is not a valid property name
+     */
+    boolean isExpressionLanguageSupported(String propertyName);
+
+    /**
+     * Returns the identifier of the ProcessGroup that the component being validated lives in
+     *
+     * @return the identifier of the ProcessGroup that the component being validated lives in
+     */
+    String getProcessGroupIdentifier();
+
+    /**
+     * Returns a Collection containing the names of all Parameters that are referenced by the property with the given name
+     *
+     * @param propertyName Name of property for which to get referenced parameters
+     * @return  a Collection containing the names of all Parameters that are referenced by the property with the given name
+     */
+    Collection<String> getReferencedParameters(String propertyName);
+
+    /**
+     * @param parameterName the name of the Parameter
+     * @return <code>true</code> if a Parameter with the given name is defined in the currently selected Parameter Context
+     */
+    boolean isParameterDefined(String parameterName);
+
+    /**
+     * Returns <code>true</code> if a Parameter with the given name is defined and has a non-null value, <code>false</code> if either the Parameter
+     * is not defined or the Parameter is defined but has a value of <code>null</code>.
+     * @param parameterName the name of the parameter
+     * @return <code>true</code> if the Parameter is defined and has a non-null value, false otherwise
+     */
+    boolean isParameterSet(String parameterName);
+
+    /**
+     * Determines whether or not the dependencies of the given Property Descriptor are satisfied.
+     * If the given Property Descriptor has no dependency on another property, then the dependency is satisfied.
+     * If there is at least one dependency, then all dependencies must be satisfied.
+     * In order for a dependency to be considered satisfied, all of the following must be true:
+     * <ul>
+     *     <li>The property that is depended upon has all of its dependencies satisfied.</li>
+     *     <li>If the given Property Descriptor depends on a given AllowableValue, then the property that is depended upon has a value that falls within the given range of Allowable Values for
+     *     the dependency.</li>
+     * </ul>
+     *
+     * @param propertyDescriptor the property descriptor
+     * @param propertyDescriptorLookup a lookup for converting from a property name to the property descriptor with that name
+     * @return <code>true</code> if all dependencies of the given property descriptor are satisfied, <code>false</code> otherwise
+     */
+    default boolean isDependencySatisfied(PropertyDescriptor propertyDescriptor, Function<String, PropertyDescriptor> propertyDescriptorLookup) {
+        return isDependencySatisfied(propertyDescriptor, propertyDescriptorLookup, new HashSet<>());
+    }
+
+    private boolean isDependencySatisfied(final PropertyDescriptor propertyDescriptor, final Function<String, PropertyDescriptor> propertyDescriptorLookup, final Set<String> propertiesSeen) {
+        final Set<PropertyDependency> dependencies = propertyDescriptor.getDependencies();
+        if (dependencies.isEmpty()) {
+            return true;
+        }
+
+        final boolean added = propertiesSeen.add(propertyDescriptor.getName());
+        if (!added) {
+            return false;
+        }
+
+        try {
+            for (final PropertyDependency dependency : dependencies) {
+                final String dependencyName = dependency.getPropertyName();
+
+                // Check if the property being depended upon has its dependencies satisfied.
+                final PropertyDescriptor dependencyDescriptor = propertyDescriptorLookup.apply(dependencyName);
+                if (dependencyDescriptor == null) {
+                    return false;
+                }
+
+                final PropertyValue propertyValue = getProperty(dependencyDescriptor);
+                final String dependencyValue = propertyValue == null ? dependencyDescriptor.getDefaultValue() : propertyValue.getValue();
+                if (dependencyValue == null) {
+                    return false;
+                }
+
+                final boolean transitiveDependencySatisfied = isDependencySatisfied(dependencyDescriptor, propertyDescriptorLookup, propertiesSeen);
+                if (!transitiveDependencySatisfied) {
+                    return false;
+                }
+
+                // Check if the property being depended upon is set to one of the values that satisfies this dependency.
+                // If the dependency has no dependent values, then any non-null value satisfies the dependency.
+                // The value is already known to be non-null due to the check above.
+                final Set<String> dependentValues = dependency.getDependentValues();
+                if (dependentValues != null && !dependentValues.contains(dependencyValue)) {
+                    return false;
+                }
+            }
+
+            return true;
+        } finally {
+            propertiesSeen.remove(propertyDescriptor.getName());
+        }
+    }
+
+    /**
+     * Determines whether or not incoming and outgoing connections should be validated.
+     * If <code>true</code>, then the validation should verify that all Relationships either have one or more connections that include the Relationship,
+     * or that the Relationship is auto-terminated.
+     * Additionally, if <code>true</code>, then any Processor with an {@link InputRequirement} of {@link InputRequirement.Requirement#INPUT_REQUIRED}
+     * should be invalid unless it has an incoming (non-looping) connection, and any Processor with an {@link InputRequirement} of {@link InputRequirement.Requirement#INPUT_FORBIDDEN}
+     * should be invalid if it does have any incoming connection.
+     *
+     * @return <code>true</code> if Connections should be validated, <code>false</code> if Connections should be ignored
+     */
+    default boolean isValidateConnections() {
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/ValidationResult.java b/src/main/java/org/apache/nifi/components/ValidationResult.java
new file mode 100644
index 0000000..ca23c1d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/ValidationResult.java
@@ -0,0 +1,170 @@
+/*
+ * 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.components;
+
+import java.util.Objects;
+
+/**
+ * Immutable - thread safe
+ */
+public class ValidationResult {
+
+    private final String subject;
+    private final String input;
+    private final String explanation;
+    private final boolean valid;
+
+    protected ValidationResult(final Builder builder) {
+        this.subject = builder.subject;
+        this.input = builder.input;
+        this.explanation = builder.explanation;
+        this.valid = builder.valid;
+    }
+
+    /**
+     * @return true if current result is valid; false otherwise
+     */
+    public boolean isValid() {
+        return this.valid;
+    }
+
+    /**
+     * @return this input value that was tested for validity
+     */
+    public String getInput() {
+        return this.input;
+    }
+
+    /**
+     * @return an explanation of the validation result
+     */
+    public String getExplanation() {
+        return this.explanation;
+    }
+
+    /**
+     * @return the item being validated
+     */
+    public String getSubject() {
+        return this.subject;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 79 * hash + Objects.hashCode(this.subject);
+        hash = 79 * hash + Objects.hashCode(this.input);
+        hash = 79 * hash + Objects.hashCode(this.explanation);
+        hash = 79 * hash + (this.valid ? 1 : 0);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ValidationResult other = (ValidationResult) obj;
+        if (!Objects.equals(this.subject, other.subject)) {
+            return false;
+        }
+        if (!Objects.equals(this.input, other.input)) {
+            return false;
+        }
+        if (!Objects.equals(this.explanation, other.explanation)) {
+            return false;
+        }
+        if (this.valid != other.valid) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        if (input == null) {
+            return String.format("'%s' is %s because %s", subject, valid ? "valid" : "invalid", explanation);
+        } else {
+            return String.format("'%s' validated against '%s' is %s because %s", subject, input, valid ? "valid" : "invalid", explanation);
+        }
+    }
+
+    public static final class Builder {
+
+        private boolean valid = false;
+        private String input = null;
+        private String explanation = "";
+        private String subject = "";
+
+        /**
+         * Defaults to false
+         *
+         * @param valid true if is valid; false otherwise
+         * @return the builder
+         */
+        public Builder valid(final boolean valid) {
+            this.valid = valid;
+            return this;
+        }
+
+        /**
+         * Defaults to empty string
+         *
+         * @param input what was validated
+         * @return the builder
+         */
+        public Builder input(final String input) {
+            if (null != input) {
+                this.input = input;
+            }
+            return this;
+        }
+
+        /**
+         * Defaults to empty string
+         *
+         * @param explanation of validation result
+         * @return the builder
+         */
+        public Builder explanation(final String explanation) {
+            if (null != explanation) {
+                this.explanation = explanation;
+            }
+            return this;
+        }
+
+        /**
+         * Defaults to empty string
+         *
+         * @param subject the thing that was validated
+         * @return the builder
+         */
+        public Builder subject(final String subject) {
+            if (null != subject) {
+                this.subject = subject;
+            }
+            return this;
+        }
+
+        public ValidationResult build() {
+            return new ValidationResult(this);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/Validator.java b/src/main/java/org/apache/nifi/components/Validator.java
new file mode 100644
index 0000000..babdc41
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/Validator.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.components;
+
+public interface Validator {
+
+    /**
+     * Validator object providing validation behavior in which validation always
+     * fails
+     */
+    Validator INVALID = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            return new ValidationResult.Builder().subject(subject).explanation(String.format("'%s' is not a supported property or has no Validator associated with it", subject)).input(input).build();
+        }
+    };
+
+    /**
+     * Validator object providing validation behavior in which validation always
+     * passes
+     */
+    Validator VALID = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+        }
+    };
+
+    /**
+     * @param subject what is being validated
+     * @param input the string to be validated
+     * @param context the ValidationContext to use when validating properties
+     * @return ValidationResult
+     * @throws NullPointerException of given input is null
+     */
+    ValidationResult validate(String subject, String input, ValidationContext context);
+}
diff --git a/src/main/java/org/apache/nifi/components/VersionedComponent.java b/src/main/java/org/apache/nifi/components/VersionedComponent.java
new file mode 100644
index 0000000..53fc62c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/VersionedComponent.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.components;
+
+import java.util.Optional;
+
+public interface VersionedComponent {
+
+    /**
+     * @return the unique identifier that maps this component to a component that is versioned
+     *         in a Flow Registry or has been imported, or <code>Optional.empty</code> if this component has not
+     *         been saved to a Flow Registry or imported.
+     */
+    Optional<String> getVersionedComponentId();
+
+    /**
+     * Updates the versioned component identifier
+     *
+     * @param versionedComponentId the identifier of the versioned component
+     *
+     * @throws IllegalStateException if this component is already under version control with a different ID and
+     *             the given ID is not null
+     */
+    void setVersionedComponentId(String versionedComponentId);
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/FileResourceReference.java b/src/main/java/org/apache/nifi/components/resource/FileResourceReference.java
new file mode 100644
index 0000000..65fc167
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/FileResourceReference.java
@@ -0,0 +1,100 @@
+/*
+ * 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.components.resource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Objects;
+
+public class FileResourceReference implements ResourceReference {
+    private final File file;
+    private final ResourceType resourceType;
+
+    public FileResourceReference(final File file) {
+        this.file = Objects.requireNonNull(file);
+        this.resourceType = file.isDirectory() ? ResourceType.DIRECTORY : ResourceType.FILE;
+    }
+
+    @Override
+    public File asFile() {
+        return file;
+    }
+
+    @Override
+    public URL asURL() {
+        try {
+            return file.toURI().toURL();
+        } catch (final MalformedURLException e) {
+            throw new AssertionError("File " + file.getAbsolutePath() + " cannot be represented as a URL"); // we won't encounter this.
+        }
+    }
+
+    @Override
+    public InputStream read() throws IOException {
+        if (resourceType != ResourceType.FILE) {
+            throw new FileNotFoundException("Could not read from file with name " + file.getAbsolutePath() + " because that references a directory");
+        }
+
+        return new FileInputStream(file);
+    }
+
+    @Override
+    public boolean isAccessible() {
+        return file.exists() && file.canRead();
+    }
+
+    @Override
+    public String getLocation() {
+        return file.getAbsolutePath();
+    }
+
+    @Override
+    public ResourceType getResourceType() {
+        return resourceType;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final FileResourceReference that = (FileResourceReference) o;
+        return Objects.equals(file, that.file)
+            && resourceType == that.resourceType;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(file, resourceType);
+    }
+
+    @Override
+    public String toString() {
+        return "FileResourceReference[file=" + file + ", resourceType=" + resourceType + "]";
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceCardinality.java b/src/main/java/org/apache/nifi/components/resource/ResourceCardinality.java
new file mode 100644
index 0000000..1563df7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceCardinality.java
@@ -0,0 +1,34 @@
+/*
+ * 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.components.resource;
+
+/**
+ * Indicates the cardinality of how many resources can be referenced by a given property.
+ */
+public enum ResourceCardinality {
+
+    /**
+     * Exactly one resource must be specified
+     */
+    SINGLE,
+
+    /**
+     * One or more resources may be supplied, as a comma-separated list
+     */
+    MULTIPLE;
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceContext.java b/src/main/java/org/apache/nifi/components/resource/ResourceContext.java
new file mode 100644
index 0000000..bf02c59
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceContext.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.components.resource;
+
+import org.apache.nifi.components.PropertyDescriptor;
+
+/**
+ * Provides the context in which a Reference Reference is to be evaluated
+ */
+public interface ResourceContext {
+    /**
+     * @return a ResourceReferenceFactory that can be used to parse a property value into a {@link ResourceReference} or {@link ResourceReferences}
+     */
+    ResourceReferenceFactory getResourceReferenceFactory();
+
+    /**
+     * @return the PropertyDescriptor that describes the property whose value may be a resource
+     */
+    PropertyDescriptor getPropertyDescriptor();
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceDefinition.java b/src/main/java/org/apache/nifi/components/resource/ResourceDefinition.java
new file mode 100644
index 0000000..b91065e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceDefinition.java
@@ -0,0 +1,37 @@
+/*
+ * 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.components.resource;
+
+import java.util.Set;
+
+/**
+ * Defines the number and types of resources that allowed to be referenced by a component property
+ */
+public interface ResourceDefinition {
+
+    /**
+     * Specifies the number of resources that should be reference-able by a component property
+     * @return the cardinality for the number of resources that should be referenced
+     */
+    ResourceCardinality getCardinality();
+
+    /**
+     * @return the types of resources that are allowed to be referenced
+     */
+    Set<ResourceType> getResourceTypes();
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceReference.java b/src/main/java/org/apache/nifi/components/resource/ResourceReference.java
new file mode 100644
index 0000000..49b95d3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceReference.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.components.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * A reference to a Resource that is identified by a property value
+ */
+public interface ResourceReference {
+
+    /**
+     * @return a file representation of the resource, or <code>null</code> if the Resource cannot be represented as a File
+     */
+    File asFile();
+
+    /**
+     * @return a URL representation of the resource, or <code>null</code> if the Resource cannot be represented as a URL
+     */
+    URL asURL();
+
+    /**
+     * @return an InputStream to read the contents of the resource
+     *
+     * @throws IOException if unable to obtain an InputStream from the resource
+     */
+    InputStream read() throws IOException;
+
+    /**
+     * Indicates whether or not the resource is accessible. What it means for the resource to be accessible depends on the type of
+     * resource. A File resource, for example, might be accessible only if the file exists and is readable, while a URL resource might
+     * always be considered accessible, or might be accesssible only if the existence of the resource can be confirmed.
+     *
+     * @return <code>true</code> if the file can be accessed, <code>false</code> otherwise
+     */
+    boolean isAccessible();
+
+    /**
+     * @return a String representation of the location, or <code>null</code> for a Resource that does not have an external location.
+     * For a File or a Directory, this will be the full path name; for a URL it will be the String form of the URL
+     */
+    String getLocation();
+
+    /**
+     * @return the type of resource that is being referenced
+     */
+    ResourceType getResourceType();
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceReferenceFactory.java b/src/main/java/org/apache/nifi/components/resource/ResourceReferenceFactory.java
new file mode 100644
index 0000000..67b056e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceReferenceFactory.java
@@ -0,0 +1,24 @@
+/*
+ * 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.components.resource;
+
+public interface ResourceReferenceFactory {
+    ResourceReference createResourceReference(String value, ResourceDefinition resourceDefinition);
+
+    ResourceReferences createResourceReferences(String value, ResourceDefinition resourceDefinition);
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceReferences.java b/src/main/java/org/apache/nifi/components/resource/ResourceReferences.java
new file mode 100644
index 0000000..5addba0
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceReferences.java
@@ -0,0 +1,73 @@
+/*
+ * 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.components.resource;
+
+import java.net.URL;
+import java.util.List;
+
+/**
+ * A representation of zero or more {@link ResourceReference}s
+ */
+public interface ResourceReferences {
+
+    /**
+     * @return a List representation of all Resource References
+     */
+    List<ResourceReference> asList();
+
+    /**
+     * @return a list of all Resource References' locations
+     */
+    List<String> asLocations();
+
+    /**
+     * @return a list of all Resource References' URLs
+     */
+    List<URL> asURLs();
+
+    /**
+     * @return the number of Resource References held
+     */
+    int getCount();
+
+    /**
+     * Iterates through the Resource References and for any reference that may represent more than one
+     * resource, flattens the resource into a List of single-entity references. For example, consider that this ResourceReferences
+     * holds a single ResourceReference, of type DIRECTORY and the referenced directory contains 10 files. Calling {@link #asList()} would
+     * return a single ResourceReference. But calling <code>flatten()</code> would return a new ResourceReferences type whose {@link #asList()}
+     * method would return 10 ResourceReference objects, each with a ResourceType of FILE. The flatten operation is not recursive, meaning that if
+     * a DIRECTORY is flattened, any sub-directories will be dropped. If the contents of the subdirectories are to be retained, use {@link #flattenRecursively()}
+     * instead.
+     *
+     * @return a flattened ResourceReferences
+     */
+    ResourceReferences flatten();
+
+    /**
+     * Recursively iterates through the Resource References and for any reference that may represent more than one
+     * resource, flattens the resource into a List of single-entity references. For example, consider that this ResourceReferences
+     * holds a single ResourceReference, of type DIRECTORY and the referenced directory contains 10 files. Calling {@link #asList()} would
+     * return a single ResourceReference. But calling <code>flatten()</code> would return a new ResourceReferences type whose {@link #asList()}
+     * method would return 10 ResourceReference objects, each with a ResourceType of FILE. The flatten operation is recursive, meaning that if
+     * a DIRECTORY is encountered, its reference will be replaced with a new reference for each file, even if that file exists 100 levels deep
+     * in the directory structure.
+     *
+     * @return a flattened ResourceReferences
+     */
+    ResourceReferences flattenRecursively();
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/ResourceType.java b/src/main/java/org/apache/nifi/components/resource/ResourceType.java
new file mode 100644
index 0000000..60eff90
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/ResourceType.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.components.resource;
+
+public enum ResourceType {
+    /**
+     * Referenced Resource is a File on a local (or mounted) file system
+     */
+    FILE("file"),
+
+    /**
+     * Referenced Resource is a directory on a local (or mounted) file system
+     */
+    DIRECTORY("directory"),
+
+    /**
+     * Referenced Resource is UTF-8 text, rather than an external entity
+     */
+    TEXT("text"),
+
+    /**
+     * Referenced Resource is a URL that uses the HTTP, HTTPS, or file protocol
+     * (i.e., <code>http://...</code>, <code>https://...</code>, or <code>file:...</code>)
+     */
+    URL("URL");
+
+    private final String prettyPrintName;
+    ResourceType(final String prettyPrintName) {
+        this.prettyPrintName = prettyPrintName;
+    }
+
+    @Override
+    public String toString() {
+        return prettyPrintName;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/StandardResourceContext.java b/src/main/java/org/apache/nifi/components/resource/StandardResourceContext.java
new file mode 100644
index 0000000..4e1fa6b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/StandardResourceContext.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.components.resource;
+
+import org.apache.nifi.components.PropertyDescriptor;
+
+public class StandardResourceContext implements ResourceContext {
+    private final ResourceReferenceFactory resourceReferenceFactory;
+    private final PropertyDescriptor propertyDescriptor;
+
+    public StandardResourceContext(final ResourceReferenceFactory resourceReferenceFactory, final PropertyDescriptor propertyDescriptor) {
+        this.resourceReferenceFactory = resourceReferenceFactory;
+        this.propertyDescriptor = propertyDescriptor;
+    }
+
+    @Override
+    public ResourceReferenceFactory getResourceReferenceFactory() {
+        return resourceReferenceFactory;
+    }
+
+    @Override
+    public PropertyDescriptor getPropertyDescriptor() {
+        return propertyDescriptor;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/StandardResourceDefinition.java b/src/main/java/org/apache/nifi/components/resource/StandardResourceDefinition.java
new file mode 100644
index 0000000..125dcbf
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/StandardResourceDefinition.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.components.resource;
+
+import java.util.Set;
+
+public class StandardResourceDefinition implements ResourceDefinition {
+    private final ResourceCardinality cardinality;
+    private final Set<ResourceType> resourceTypes;
+
+    public StandardResourceDefinition(final ResourceCardinality cardinality, final Set<ResourceType> resourceTypes) {
+        this.cardinality = cardinality;
+        this.resourceTypes = resourceTypes;
+    }
+
+    @Override
+    public ResourceCardinality getCardinality() {
+        return cardinality;
+    }
+
+    @Override
+    public Set<ResourceType> getResourceTypes() {
+        return resourceTypes;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/StandardResourceReferenceFactory.java b/src/main/java/org/apache/nifi/components/resource/StandardResourceReferenceFactory.java
new file mode 100644
index 0000000..3980a9b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/StandardResourceReferenceFactory.java
@@ -0,0 +1,131 @@
+/*
+ * 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.components.resource;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class StandardResourceReferenceFactory implements ResourceReferenceFactory {
+
+    private static final ResourceReferences EMPTY_RESOURCE_REFERENCES = new StandardResourceReferences(Collections.emptyList());
+
+    public ResourceReferences createResourceReferences(final String value, final ResourceDefinition resourceDefinition) {
+        if (value == null) {
+            return EMPTY_RESOURCE_REFERENCES;
+        }
+
+        final String trimmed = value.trim();
+        if (trimmed.isEmpty()) {
+            return EMPTY_RESOURCE_REFERENCES;
+        }
+
+        if (resourceDefinition == null) {
+            return EMPTY_RESOURCE_REFERENCES;
+        }
+
+        final List<ResourceReference> references;
+        final List<String> locations = parseResourceLocations(value);
+        references = new ArrayList<>(locations.size());
+        locations.forEach(location -> references.add(createResourceReference(location, resourceDefinition)));
+
+        return new StandardResourceReferences(references);
+    }
+
+    public ResourceReference createResourceReference(final String value, final ResourceDefinition resourceDefinition) {
+        if (value == null) {
+            return null;
+        }
+
+        final String trimmed = value.trim();
+        if (trimmed.isEmpty()) {
+            return null;
+        }
+
+        if (resourceDefinition == null) {
+            return null;
+        }
+
+        final Set<ResourceType> allowedResourceTypes = resourceDefinition.getResourceTypes();
+        if (allowedResourceTypes.contains(ResourceType.URL)) {
+            try {
+                if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
+                    return new URLResourceReference(URI.create(trimmed).toURL());
+                }
+
+                if (trimmed.startsWith("file:")) {
+                    final URL url = URI.create(trimmed).toURL();
+                    final String filename = url.getFile();
+                    final File file = new File(filename);
+                    return new FileResourceReference(file);
+                }
+            } catch (MalformedURLException e) {
+                throw new IllegalArgumentException("Invalid URL: " + trimmed);
+            }
+        }
+
+        final boolean fileAllowed = allowedResourceTypes.contains(ResourceType.FILE) || allowedResourceTypes.contains(ResourceType.DIRECTORY);
+        final boolean textAllowed = allowedResourceTypes.contains(ResourceType.TEXT);
+
+        if (fileAllowed && textAllowed) {
+            // We have to make a determination whether this is a file or text. Eventually, it will be best if the user tells us explicitly.
+            // For now, we will make a determination based on a couple of simple rules.
+            final File file = new File(trimmed);
+            if (file.isAbsolute() || file.exists()) {
+                return new FileResourceReference(file);
+            }
+
+            if (trimmed.startsWith("./") || trimmed.startsWith(".\\")) {
+                return new FileResourceReference(file);
+            }
+
+            return new Utf8TextResource(value); // Use explicit value, not trimmed value, as the white space may be important for textual content.
+        }
+
+        if (fileAllowed) {
+            final File file = new File(trimmed);
+            return new FileResourceReference(file);
+        }
+
+        if (textAllowed) {
+            return new Utf8TextResource(value);
+        }
+
+        return null;
+    }
+
+    private List<String> parseResourceLocations(final String rawValue) {
+        final List<String> resourceLocations = new ArrayList<>();
+        final String[] splits = rawValue.split(",");
+        for (final String split : splits) {
+            final String trimmed = split.trim();
+            if (trimmed.isEmpty()) {
+                continue;
+            }
+
+            resourceLocations.add(trimmed);
+        }
+
+        return resourceLocations;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/StandardResourceReferences.java b/src/main/java/org/apache/nifi/components/resource/StandardResourceReferences.java
new file mode 100644
index 0000000..11ce057
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/StandardResourceReferences.java
@@ -0,0 +1,154 @@
+/*
+ * 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.components.resource;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class StandardResourceReferences implements ResourceReferences {
+    public final List<ResourceReference> resourceReferences;
+
+    public StandardResourceReferences(final List<ResourceReference> resourceReferences) {
+        this.resourceReferences = new ArrayList<>(Objects.requireNonNull(resourceReferences));
+    }
+
+    @Override
+    public List<ResourceReference> asList() {
+        return Collections.unmodifiableList(resourceReferences);
+    }
+
+    @Override
+    public List<String> asLocations() {
+        final List<String> locations = new ArrayList<>(resourceReferences.size());
+        resourceReferences.forEach(ref -> locations.add(ref.getLocation()));
+        return locations;
+    }
+
+    @Override
+    public List<URL> asURLs() {
+        final List<URL> locations = new ArrayList<>(resourceReferences.size());
+        resourceReferences.forEach(ref -> locations.add(ref.asURL()));
+        return locations;
+    }
+
+    @Override
+    public int getCount() {
+        return resourceReferences.size();
+    }
+
+    @Override
+    public ResourceReferences flatten() {
+        if (resourceReferences.isEmpty()) {
+            return this;
+        }
+
+        final List<ResourceReference> flattened = new ArrayList<>();
+        resourceReferences.forEach(reference -> {
+            if (reference.getResourceType() == ResourceType.DIRECTORY) {
+                addChildren(reference.asFile(), flattened);
+            } else {
+                flattened.add(reference);
+            }
+        });
+
+        return new StandardResourceReferences(flattened);
+    }
+
+    private void addChildren(final File file, final List<ResourceReference> flattened) {
+        if (file == null) {
+            return;
+        }
+
+        if (file.isDirectory()) {
+            final File[] children = file.listFiles();
+            if (children != null) {
+                for (final File child : children) {
+                    if (child.isFile()) {
+                        flattened.add(new FileResourceReference(child));
+                    }
+                }
+            }
+        } else {
+            flattened.add(new FileResourceReference(file));
+        }
+    }
+
+
+    @Override
+    public ResourceReferences flattenRecursively() {
+        if (resourceReferences.isEmpty()) {
+            return this;
+        }
+
+        final List<ResourceReference> flattened = new ArrayList<>();
+        resourceReferences.forEach(reference -> {
+            if (reference.getResourceType() == ResourceType.DIRECTORY) {
+                recurse(reference.asFile(), flattened);
+            } else {
+                flattened.add(reference);
+            }
+        });
+
+        return new StandardResourceReferences(flattened);
+    }
+
+    private void recurse(final File file, final List<ResourceReference> flattened) {
+        if (file == null) {
+            return;
+        }
+
+        if (file.isDirectory()) {
+            final File[] children = file.listFiles();
+            if (children != null) {
+                for (final File child : children) {
+                    recurse(child, flattened);
+                }
+            }
+        } else {
+            flattened.add(new FileResourceReference(file));
+        }
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final StandardResourceReferences that = (StandardResourceReferences) o;
+        return Objects.equals(resourceReferences, that.resourceReferences);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(resourceReferences);
+    }
+
+    @Override
+    public String toString() {
+        return "StandardResourceReferences[resources=" + resourceReferences + "]";
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/URLResourceReference.java b/src/main/java/org/apache/nifi/components/resource/URLResourceReference.java
new file mode 100644
index 0000000..8b5f7f7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/URLResourceReference.java
@@ -0,0 +1,67 @@
+/*
+ * 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.components.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+public class URLResourceReference implements ResourceReference {
+    private final URL url;
+
+    public URLResourceReference(final URL url) {
+        this.url = url;
+    }
+
+    @Override
+    public File asFile() {
+        return null;
+    }
+
+    @Override
+    public URL asURL() {
+        return url;
+    }
+
+    @Override
+    public InputStream read() throws IOException {
+        return url.openStream();
+    }
+
+    @Override
+    public boolean isAccessible() {
+        return true;
+    }
+
+    @Override
+    public String getLocation() {
+        return url.toExternalForm();
+    }
+
+    @Override
+    public ResourceType getResourceType() {
+        return ResourceType.URL;
+    }
+
+    @Override
+    public String toString() {
+        return "URLResourceReference[url=" + url + "]";
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/components/resource/Utf8TextResource.java b/src/main/java/org/apache/nifi/components/resource/Utf8TextResource.java
new file mode 100644
index 0000000..78f94e9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/resource/Utf8TextResource.java
@@ -0,0 +1,68 @@
+/*
+ * 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.components.resource;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class Utf8TextResource implements ResourceReference {
+    private final String text;
+
+    public Utf8TextResource(final String text) {
+        this.text = text;
+    }
+
+    @Override
+    public File asFile() {
+        return null;
+    }
+
+    @Override
+    public URL asURL() {
+        return null;
+    }
+
+    @Override
+    public InputStream read() throws IOException {
+        return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public boolean isAccessible() {
+        return true;
+    }
+
+    @Override
+    public String getLocation() {
+        return null;
+    }
+
+    @Override
+    public ResourceType getResourceType() {
+        return ResourceType.TEXT;
+    }
+
+    @Override
+    public String toString() {
+        return "Utf8TextResource[text=" + text.length() + " characters]";
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/state/Scope.java b/src/main/java/org/apache/nifi/components/state/Scope.java
new file mode 100644
index 0000000..dd0d0aa
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/state/Scope.java
@@ -0,0 +1,41 @@
+/*
+ * 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.components.state;
+
+/**
+ * A Scope represents how a NiFi component's state is to be stored and retrieved when running in a cluster.
+ */
+public enum Scope {
+
+    /**
+     * State is to be treated as "global" across the cluster. I.e., the same component on all nodes will
+     * have access to the same state.
+     */
+    CLUSTER,
+
+    /**
+     * State is to be treated local to the node. I.e., the same component will have different state on each
+     * node in the cluster.
+     */
+    LOCAL;
+
+    @Override
+    public String toString() {
+        return name();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/components/state/StateManager.java b/src/main/java/org/apache/nifi/components/state/StateManager.java
new file mode 100644
index 0000000..1669ea1
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/state/StateManager.java
@@ -0,0 +1,101 @@
+/*
+ * 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.components.state;
+
+import java.io.IOException;
+import java.util.Map;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.components.state.exception.StateTooLargeException;
+
+/**
+ * <p>
+ * The StateManager is responsible for providing NiFi components a mechanism for storing
+ * and retrieving state.
+ * </p>
+ *
+ * <p>
+ * When calling methods in this class, the {@link Scope} is used in order to specify whether
+ * state should be stored/retrieved from the local state or the clustered state. However, if
+ * any instance of NiFi is not clustered (or is disconnected from its cluster), the Scope is
+ * not really relevant and the local state will be used. This allows component
+ * developers to not concern themselves with whether or not a particular instance of NiFi is
+ * clustered. Instead, developers should assume that the instance is indeed clustered and write
+ * the component accordingly. If not clustered, the component will still behave in the same
+ * manner, as a standalone node could be thought of as a "cluster of 1."
+ * </p>
+ *
+ * <p>
+ * This mechanism is designed to allow developers to easily store and retrieve small amounts of state.
+ * The storage mechanism is implementation-specific, but is typically either stored on a remote system
+ * or on disk. For that reason, one should consider the cost of storing and retrieving this data, and the
+ * amount of data should be kept to the minimum required.
+ * </p>
+ *
+ * <p>
+ * Any component that wishes to use the StateManager should also use the {@link Stateful} annotation to provide
+ * a description of what state is being stored and what Scope is used. If this annotation is not present, the UI
+ * will not expose such information or allow DFMs to clear the state.
+ * </p>
+ */
+public interface StateManager {
+
+    /**
+     * Updates the value of the component's state, setting it to given value
+     *
+     * @param state the value to change the state to
+     * @param scope the scope to use when storing the state
+     *
+     * @throws StateTooLargeException if attempting to store more state than is allowed by the backing storage mechanism
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    void setState(Map<String, String> state, Scope scope) throws IOException;
+
+    /**
+     * Returns the current state for the component. This return value will never be <code>null</code>.
+     * If the state has not yet been set, the StateMap's version will be -1, and the map of values will be empty.
+     *
+     * @param scope the scope to use when fetching the state
+     * @return the current state for the component
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    StateMap getState(Scope scope) throws IOException;
+
+    /**
+     * Updates the value of the component's state to the new value if and only if the value currently
+     * is the same as the given oldValue.
+     *
+     * @param oldValue the old value to compare against
+     * @param newValue the new value to use if and only if the state's value is the same as the given oldValue
+     * @param scope the scope to use for storing the new state
+     * @return <code>true</code> if the state was updated to the new value, <code>false</code> if the state's value was not
+     *         equal to oldValue
+     *
+     * @throws StateTooLargeException if attempting to store more state than is allowed by the backing storage mechanism
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    boolean replace(StateMap oldValue, Map<String, String> newValue, Scope scope) throws IOException;
+
+    /**
+     * Clears all keys and values from the component's state
+     *
+     * @param scope the scope whose values should be cleared
+     *
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    void clear(Scope scope) throws IOException;
+}
diff --git a/src/main/java/org/apache/nifi/components/state/StateMap.java b/src/main/java/org/apache/nifi/components/state/StateMap.java
new file mode 100644
index 0000000..95829a7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/state/StateMap.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.components.state;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Provides a representation of a component's state at some point in time.
+ */
+public interface StateMap {
+
+    /**
+     * Get state version is not guaranteed to be numeric, but can be used to compare against an expected version.
+     * The default implementation uses the available version number and considers -1 as indicating an empty version
+     *
+     * @return State version or empty when not known
+     */
+    Optional<String> getStateVersion();
+
+    /**
+     * Returns the value associated with the given key
+     *n
+     * @param key the key whose value should be retrieved
+     * @return the value associated with the given key, or <code>null</code> if no value is associated
+     *         with this key.
+     */
+    String get(String key);
+
+    /**
+     * Returns an immutable Map representation of all keys and values for the state of a component.
+     *
+     * @return an immutable Map representation of all keys and values for the state of a component.
+     */
+    Map<String, String> toMap();
+}
diff --git a/src/main/java/org/apache/nifi/components/state/exception/StateTooLargeException.java b/src/main/java/org/apache/nifi/components/state/exception/StateTooLargeException.java
new file mode 100644
index 0000000..41c8b24
--- /dev/null
+++ b/src/main/java/org/apache/nifi/components/state/exception/StateTooLargeException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.components.state.exception;
+
+import java.io.IOException;
+import org.apache.nifi.components.state.StateManager;
+
+/**
+ * Thrown when attempting to store state via the {@link StateManager} but the state being
+ * stored is larger than is allowed by the backing storage mechanism.
+ */
+public class StateTooLargeException extends IOException {
+    private static final long serialVersionUID = 1L;
+
+    public StateTooLargeException(final String message) {
+        super(message);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/context/ClusterContext.java b/src/main/java/org/apache/nifi/context/ClusterContext.java
new file mode 100644
index 0000000..aba5244
--- /dev/null
+++ b/src/main/java/org/apache/nifi/context/ClusterContext.java
@@ -0,0 +1,30 @@
+/*
+ * 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.context;
+
+/**
+ * A context for retrieving information about the state of the cluster.
+ */
+public interface ClusterContext {
+
+    /**
+     * Retrieves the current state of the cluster connection of this node.
+     *
+     * @return True if this node is connected to the cluster. False otherwise.
+     */
+    boolean isConnectedToCluster();
+}
diff --git a/src/main/java/org/apache/nifi/context/PropertyContext.java b/src/main/java/org/apache/nifi/context/PropertyContext.java
new file mode 100644
index 0000000..89ffb45
--- /dev/null
+++ b/src/main/java/org/apache/nifi/context/PropertyContext.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.context;
+
+import java.util.Map;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+
+/**
+ * A context for retrieving a PropertyValue from a PropertyDescriptor.
+ */
+public interface PropertyContext {
+
+    /**
+     * Retrieves the current value set for the given descriptor, if a value is
+     * set - else uses the descriptor to determine the appropriate default value
+     *
+     * @param descriptor to lookup the value of
+     * @return the property value of the given descriptor
+     */
+    PropertyValue getProperty(PropertyDescriptor descriptor);
+
+
+    Map<String, String> getAllProperties();
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/AbstractControllerService.java b/src/main/java/org/apache/nifi/controller/AbstractControllerService.java
new file mode 100644
index 0000000..30773ee
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/AbstractControllerService.java
@@ -0,0 +1,130 @@
+/*
+ * 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.OnDisabled;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.reporting.InitializationException;
+
+public abstract class AbstractControllerService extends AbstractConfigurableComponent implements ControllerService {
+
+    private String identifier;
+    private ControllerServiceLookup serviceLookup;
+    private ComponentLog logger;
+    private StateManager stateManager;
+    private volatile ConfigurationContext configurationContext;
+    private volatile boolean enabled = false;
+    private NodeTypeProvider nodeTypeProvider;
+
+    @Override
+    public final void initialize(final ControllerServiceInitializationContext context) throws InitializationException {
+        this.identifier = context.getIdentifier();
+        serviceLookup = context.getControllerServiceLookup();
+        logger = context.getLogger();
+        stateManager = context.getStateManager();
+        nodeTypeProvider = context.getNodeTypeProvider();
+        init(context);
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the {@link ControllerServiceLookup} that was passed to the
+     * {@link #init(ControllerServiceInitializationContext)} method
+     */
+    protected final ControllerServiceLookup getControllerServiceLookup() {
+        return serviceLookup;
+    }
+
+    /**
+     * @return the {@link NodeTypeProvider} that was passed to the
+     * {@link #init(ControllerServiceInitializationContext)} method
+     */
+    protected final NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+
+    /**
+     * Provides a mechanism by which subclasses can perform initialization of
+     * the Controller Service before it is scheduled to be run
+     *
+     * @param config of initialization context
+     * @throws InitializationException if unable to init
+     */
+    protected void init(final ControllerServiceInitializationContext config) throws InitializationException {
+    }
+
+    @OnEnabled
+    public final void enabled() {
+        this.enabled = true;
+    }
+
+    @OnDisabled
+    public final void disabled() {
+        this.enabled = false;
+    }
+
+    public boolean isEnabled() {
+        return this.enabled;
+    }
+
+    /**
+     * @return the logger that has been provided to the component by the
+     * framework in its initialize method
+     */
+    protected ComponentLog getLogger() {
+        return logger;
+    }
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this Controller Service
+     */
+    protected StateManager getStateManager() {
+        return stateManager;
+    }
+
+    @OnEnabled
+    public final void abstractStoreConfigContext(final ConfigurationContext configContext) {
+        this.configurationContext = configContext;
+    }
+
+    @OnDisabled
+    public final void abstractClearConfigContext() {
+        this.configurationContext = null;
+    }
+
+    protected ConfigurationContext getConfigurationContext() {
+        final ConfigurationContext context = this.configurationContext;
+        if (context == null) {
+            throw new IllegalStateException("No Configuration Context exists");
+        }
+
+        return configurationContext;
+    }
+
+    protected PropertyValue getProperty(final PropertyDescriptor descriptor) {
+        return getConfigurationContext().getProperty(descriptor);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/ConfigurationContext.java b/src/main/java/org/apache/nifi/controller/ConfigurationContext.java
new file mode 100644
index 0000000..2886a2a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/ConfigurationContext.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.controller;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.context.PropertyContext;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This context is passed to ControllerServices and Reporting Tasks in order
+ * to expose their configuration to them.
+ */
+public interface ConfigurationContext extends PropertyContext {
+
+    /**
+     * @return an unmodifiable map of all configured properties for this
+     * {@link ControllerService}
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return the annotation data configured for the component
+     */
+    String getAnnotationData();
+
+    /**
+     * @return a String representation of the scheduling period, or <code>null</code> if
+     *         the component does not have a scheduling period (e.g., for ControllerServices)
+     */
+    String getSchedulingPeriod();
+
+    /**
+     * Returns the amount of time, in the given {@link TimeUnit} that will
+     * elapsed between the return of one execution of the
+     * component's <code>onTrigger</code> method and
+     * the time at which the method is invoked again. This method will return
+     * null if the component does not have a scheduling period (e.g., for ControllerServices)
+     *
+     * @param timeUnit unit of time for scheduling
+     * @return period of time or <code>null</code> if component does not have a scheduling
+     *         period
+     */
+    Long getSchedulingPeriod(TimeUnit timeUnit);
+
+    /**
+     * Returns the component's (ControllerService, ReportingTask, ParameterProvider, e.g.) name
+     * @return the String name of this component
+     */
+    String getName();
+}
diff --git a/src/main/java/org/apache/nifi/controller/ControllerService.java b/src/main/java/org/apache/nifi/controller/ControllerService.java
new file mode 100644
index 0000000..e8f9850
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/ControllerService.java
@@ -0,0 +1,206 @@
+/*
+ * 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.behavior.Stateful;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.migration.PropertyConfiguration;
+import org.apache.nifi.parameter.ParameterProvider;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSessionFactory;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.reporting.ReportingTask;
+
+/**
+ * <p>
+ * This interface provides a mechanism for creating services that are shared
+ * among all {@link Processor}s, {@link ReportingTask}s, {@link FlowAnalysisRule}s, {@link ParameterProvider}s and other
+ * {@code ControllerService}s.
+ * </p>
+ *
+ * <p>
+ * <code>ControllerService</code>s are discovered using Java's
+ * <code>ServiceLoader</code> mechanism. As a result, all implementations must
+ * follow these rules:
+ * </p>
+ *
+ * <ul>
+ * <li>The implementation must implement this interface.</li>
+ * <li>The implementation must have a file named
+ * org.apache.nifi.controller.ControllerService located within the jar's
+ * <code>META-INF/services</code> directory. This file contains a list of
+ * fully-qualified class names of all <code>ControllerService</code>s in the
+ * jar, one-per-line.
+ * <li>The implementation must support a default constructor.</li>
+ * </ul>
+ *
+ * <p>
+ * <b>All implementations of this interface must be thread-safe.</b>
+ * </p>
+ *
+ * <h2>Accessing Controller Services</h2>
+ * <p>
+ * A ControllerService is accessible only through its interface. The framework
+ * provides access to a ControllerService through two different mechanisms:
+ * <ul>
+ * <li>
+ * A {@link PropertyDescriptor} can be created via the
+ * {@link PropertyDescriptor.Builder} after calling the
+ * {@link PropertyDescriptor.Builder#identifiesControllerService(Class) identifiesControllerService(Class)}
+ * method and then the ControllerService is accessed via
+ * {@link PropertyValue#asControllerService(Class)} method.
+ * <p>
+ * For example:
+ * </p>
+ * <pre><code>
+ *    public static final PropertyDescriptor MY_PROPERTY = new PropertyDescriptor.Builder()
+ *     .name("My Property")
+ *     .description("Example Property")
+ *     .identifiesControllerService( MyControllerServiceInterface.class )
+ *     .build();
+ *
+ *    ...
+ *    public void onTrigger(ProcessContext context, ProcessSessionFactory sessionFactory) throws ProcessException {
+ *     // Obtain the user-selected controller service
+ *     final MyControllerServiceInterface service = context.getProperty(MY_PROPERTY).asControllerService( MyControllerServiceInterface.class );
+ *     ...
+ *    }
+ *
+ * </code></pre>
+ * <li>A Controller Service can be obtained via a
+ * {@link ControllerServiceLookup}. This lookup may be obtained, for example,
+ * from the {@link ProcessContext} that is provided to a {@link Processor}'s
+ * {@link Processor#onTrigger(ProcessContext, ProcessSessionFactory) onTrigger}
+ * method.
+ * <p>
+ * For example:
+ * </p>
+ *
+ * <pre><code>
+ *    public void onTrigger(ProcessContext context, ProcessSessionFactory sessionFactory) throws ProcessException {
+ *      final MyControllerServiceInterface service = (MyControllerServiceInterface) context.getControllerServiceLookup().getControllerService("service_identifier");
+ *    }
+ * </code></pre>
+ * </li>
+ * </ul>
+ *
+ * <h2>Defining a Controller Service</h2>
+ * <p>
+ * Note in both of the examples above, that the Controller Service was accessed
+ * only by its interface, and this interface extends ControllerService. If we
+ * have an implementation named MyServiceImpl, for example, that implements
+ * MyControllerServiceInterface, we cannot, in either case, attempt to cast the
+ * ControllerService to the desired implementation. Doing so will result in a
+ * {@link ClassCastException}. This is by design and is done for the following
+ * reasons:
+ *
+ * <ul>
+ * <li>
+ * It is a good coding practice to implement such a service as an interface in
+ * general.
+ * </li>
+ * <li>
+ * A Controller Service can be referenced from different NiFi Archives (NARs).
+ * This means that the Controller Service may be defined in one ClassLoader and
+ * referenced from another unrelated ClassLoader. In order to account for this,
+ * NiFi will change the current thread's ClassLoader as appropriate when
+ * entering the Controller Service's code and revert back to the previous
+ * ClassLoader after exiting the Controller Service's code.
+ * </li>
+ * </ul>
+ *
+ * <h2>Controller Services and NARs</h2>
+ * <p>
+ * Due to the fact that a Controller Service may be referenced from a different
+ * NAR than the one in which the implementation lives, it is crucial that both
+ * the Controller Service's interface and the code referencing the interface
+ * inherit from the same ClassLoader. This is accomplished by ensuring that the
+ * NAR that contains the Controller Service interface is the parent (or
+ * ancestor) of the NAR that references the Controller Service interface.
+ * </p>
+ *
+ * <p>
+ * Typically, this is done by creating a NAR structure as follows:
+ * <pre>
+ *   + my-services-api-nar
+ *   +--- service-X-implementation-nar
+ *   +--- service-Y-implementation-nar
+ *   +--- service-Z-implementation-nar
+ *   +--- processor-A-nar
+ *   +--- processor-B-nar
+ * </pre>
+ *
+ * <p>
+ * In this case, the {@code MyControllerServiceInterface} interface, and any
+ * other Controller Service interfaces, will be defined in the
+ * {@code my-services-api-nar} NAR. Implementations are then encapsulated within
+ * the {@code service-X-implementation-nar},
+ * {@code service-Y-implementation-nar}, and
+ * {@code service-Z-implementation-nar} NARs. All Controller Services and all
+ * Processors defined in these NARs are able to reference any other Controller
+ * Services whose interfaces are provided in the {@code my-services-api-nar}
+ * NAR.
+ * </p>
+ *
+ * <p>
+ * For more information on NARs, see the NiFi Developer Guide.
+ * </p>
+ */
+public interface ControllerService extends ConfigurableComponent {
+
+    /**
+     * Provides the Controller Service with access to objects that may be of use
+     * throughout the life of the service. This method will be called before any
+     * properties are set
+     *
+     * @param context of initialization
+     * @throws org.apache.nifi.reporting.InitializationException if unable to init
+     */
+    void initialize(ControllerServiceInitializationContext context) throws InitializationException;
+
+    /**
+     * Indicates whether this controller service, configured with the given {@link ConfigurationContext}, stores state.
+     * @param context provides access to convenience methods for obtaining property values
+     * @return True if this controller service stores state
+     */
+    default boolean isStateful(ConfigurationContext context) {
+        return this.getClass().isAnnotationPresent(Stateful.class);
+    }
+
+    /**
+     * <p>
+     * Allows for the migration of an old property configuration to a new configuration. This allows the Controller Service to evolve over time,
+     * as it allows properties to be renamed, removed, or reconfigured.
+     * </p>
+     *
+     * <p>
+     * This method is called only when a Controller Service is restored from a previous configuration. For example, when NiFi is restarted and the
+     * flow is restored from disk, when a previously configured flow is imported (e.g., from a JSON file that was exported or a NiFi Registry),
+     * or when a node joins a cluster and inherits a flow that has a new Controller Service. Once called, the method will not be invoked again for this
+     * Controller Service until NiFi is restarted.
+     * </p>
+     *
+     * @param config the current property configuration
+     */
+    default void migrateProperties(PropertyConfiguration config) {
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/ControllerServiceInitializationContext.java b/src/main/java/org/apache/nifi/controller/ControllerServiceInitializationContext.java
new file mode 100644
index 0000000..c778869
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/ControllerServiceInitializationContext.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.controller;
+
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+
+public interface ControllerServiceInitializationContext extends KerberosContext {
+
+    /**
+     * @return the identifier associated with the {@link ControllerService} with
+     * which this context is associated
+     */
+    String getIdentifier();
+
+    /**
+     * @return the {@link ControllerServiceLookup} which can be used to obtain
+     * Controller Services
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @return a logger that can be used to log important events in a standard
+     * way and generate bulletins when appropriate
+     */
+    ComponentLog getLogger();
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this component
+     */
+    StateManager getStateManager();
+
+    /**
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     */
+    NodeTypeProvider getNodeTypeProvider();
+}
diff --git a/src/main/java/org/apache/nifi/controller/ControllerServiceLookup.java b/src/main/java/org/apache/nifi/controller/ControllerServiceLookup.java
new file mode 100644
index 0000000..0144438
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/ControllerServiceLookup.java
@@ -0,0 +1,72 @@
+/*
+ * 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 java.util.Set;
+
+public interface ControllerServiceLookup {
+
+    /**
+     * @param serviceIdentifier of controller service
+     * @return the ControllerService that is registered with the given
+     * identifier
+     */
+    ControllerService getControllerService(String serviceIdentifier);
+
+    /**
+     * @param serviceIdentifier identifier of service to check
+     * @return <code>true</code> if the Controller Service with the given
+     * identifier is enabled, <code>false</code> otherwise. If the given
+     * identifier is not known by this ControllerServiceLookup, returns
+     * <code>false</code>
+     */
+    boolean isControllerServiceEnabled(String serviceIdentifier);
+
+    /**
+     * @param serviceIdentifier identifier of service to check
+     * @return <code>true</code> if the Controller Service with the given
+     * identifier has been enabled but is still in the transitioning state,
+     * otherwise returns <code>false</code>. If the given identifier is not
+     * known by this ControllerServiceLookup, returns <code>false</code>
+     */
+    boolean isControllerServiceEnabling(String serviceIdentifier);
+
+    /**
+     * @param service service to check
+     * @return <code>true</code> if the given Controller Service is enabled,
+     * <code>false</code> otherwise. If the given Controller Service is not
+     * known by this ControllerServiceLookup, returns <code>false</code>
+     */
+    boolean isControllerServiceEnabled(ControllerService service);
+
+    /**
+     *
+     * @param serviceType type of service to get identifiers for
+     *
+     * @return the set of all Controller Service Identifiers whose Controller
+     *         Service is of the given type.
+     * @throws IllegalArgumentException if the given class is not an interface
+     */
+    Set<String> getControllerServiceIdentifiers(Class<? extends ControllerService> serviceType) throws IllegalArgumentException;
+
+    /**
+     * @param serviceIdentifier identifier to look up
+     * @return the name of the Controller service with the given identifier. If
+     * no service can be found with this identifier, returns {@code null}
+     */
+    String getControllerServiceName(String serviceIdentifier);
+}
diff --git a/src/main/java/org/apache/nifi/controller/NodeTypeProvider.java b/src/main/java/org/apache/nifi/controller/NodeTypeProvider.java
new file mode 100644
index 0000000..1552282
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/NodeTypeProvider.java
@@ -0,0 +1,78 @@
+/*
+ * 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 java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * This interface provides a set of methods for checking NiFi node type.
+ */
+public interface NodeTypeProvider {
+
+    /**
+     * @return true if this instance is clustered, false otherwise.MockProcessContext
+     * Clustered means that a node is either connected or trying to connect to the cluster.
+     */
+    boolean isClustered();
+
+    /**
+     * @return true if the expected state of clustering is true, false otherwise. Contrary to {{@link #isClustered()}}
+     * this does not dynamically change with the state of this node.
+     */
+    default boolean isConfiguredForClustering() {
+        return false;
+    }
+
+    /**
+     * @return true if this instances is clustered and connected to the cluster.
+     */
+    default boolean isConnected() {
+        return false;
+    }
+
+    /**
+     * @return true if this instance is the primary node in the cluster; false otherwise
+     */
+    boolean isPrimary();
+
+    /**
+     * @return Returns with the hostname of the current node, if clustered. For For a standalone
+     * NiFi this returns an empty instead.
+     */
+    default Optional<String> getCurrentNode() {
+        if (isClustered()) {
+            throw new IllegalStateException("Clustered environment is not handled!");
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * @return Names/IP addresses of all expected hosts in the cluster (including the current one). For a standalone
+     * NiFi this returns an empty set instead.
+     */
+    default Set<String> getClusterMembers() {
+        if (isClustered()) {
+            throw new IllegalStateException("Clustered environment is not handled!");
+        } else {
+            return Collections.emptySet();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/ScheduledState.java b/src/main/java/org/apache/nifi/controller/ScheduledState.java
new file mode 100644
index 0000000..f457a9c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/ScheduledState.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.controller;
+
+/**
+ * Indicates the valid values for the state of a <code>Triggerable</code> entity
+ * with respect to scheduling the entity to run.
+ */
+public enum ScheduledState {
+
+    /**
+     * Entity cannot be scheduled to run
+     */
+    DISABLED,
+    /**
+     * Entity can be scheduled to run but currently is not
+     */
+    STOPPED,
+    /**
+     * Entity is currently scheduled to run
+     */
+    RUNNING,
+
+    STARTING,
+
+    STOPPING,
+
+    RUN_ONCE;
+}
diff --git a/src/main/java/org/apache/nifi/controller/VerifiableControllerService.java b/src/main/java/org/apache/nifi/controller/VerifiableControllerService.java
new file mode 100644
index 0000000..c9978f8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/VerifiableControllerService.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.controller;
+
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.components.ConfigVerificationResult;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ *     Any Controller Service that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Controller Service. This allows the Controller Service to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Controller Service is not valid, it cannot be started. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when starting a component).
+ * </p>
+ *
+ * <p>
+ *     Verification is allowed to be run only when a Controller Service is fully disabled.
+ *     Therefore, any initialization logic that may need to be performed
+ *     before the Controller Service is triggered may also be required for verification. However, the framework is not responsible
+ *     for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ConfigurationContext, ComponentLog, Map)} itself.
+ *     The {@link #verify(ConfigurationContext, ComponentLog, Map)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableControllerService {
+
+    /**
+     * Verifies that the configuration defined by the given ConfigurationContext is valid.
+     * @param context the ProcessContext that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     * @param variables a Map of key/value pairs that can be used to resolve variables referenced in property values via Expression Language
+     *
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ConfigurationContext context, ComponentLog verificationLogger, Map<String, String> variables);
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java b/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java
new file mode 100644
index 0000000..899bcbc
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/VersionedControllerServiceLookup.java
@@ -0,0 +1,29 @@
+/*
+ * 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.flow.VersionedControllerService;
+
+public interface VersionedControllerServiceLookup {
+
+    /**
+     * @param serviceIdentifier of controller service
+     * @return the VersionedControllerService with the given
+     * identifier
+     */
+    VersionedControllerService getVersionedControllerService(String serviceIdentifier);
+}
diff --git a/src/main/java/org/apache/nifi/controller/queue/QueueSize.java b/src/main/java/org/apache/nifi/controller/queue/QueueSize.java
new file mode 100644
index 0000000..ef6a145
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/queue/QueueSize.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+import java.text.NumberFormat;
+
+public class QueueSize {
+    private final int objectCount;
+    private final long totalSizeBytes;
+    private final int hashCode;
+
+    public QueueSize(final int numberObjects, final long totalSizeBytes) {
+        if (numberObjects < 0 || totalSizeBytes < 0) {
+            throw new IllegalArgumentException();
+        }
+        objectCount = numberObjects;
+        this.totalSizeBytes = totalSizeBytes;
+        hashCode = (int) (41 + 47 * objectCount + 51 * totalSizeBytes);
+    }
+
+    /**
+     * @return number of objects present on the queue
+     */
+    public int getObjectCount() {
+        return objectCount;
+    }
+
+    /**
+     * @return total size in bytes of the content for the data on the queue
+     */
+    public long getByteCount() {
+        return totalSizeBytes;
+    }
+
+    /**
+     * Returns a new QueueSize that is the sum of this QueueSize and the provided QueueSize
+     *
+     * @param other the other QueueSize to add to this QueueSize
+     * @return a new QueueSize that is the sum of this QueueSize and the provided QueueSize
+     */
+    public QueueSize add(final QueueSize other) {
+        if (other == null) {
+            return new QueueSize(objectCount, totalSizeBytes);
+        }
+
+        return new QueueSize(objectCount + other.getObjectCount(), totalSizeBytes + other.getByteCount());
+    }
+
+    public QueueSize add(final int count, final long bytes) {
+        return new QueueSize(objectCount + count, totalSizeBytes + bytes);
+    }
+
+    @Override
+    public String toString() {
+        return "QueueSize[FlowFiles=" + objectCount + ", ContentSize=" + NumberFormat.getNumberInstance().format(totalSizeBytes) + " Bytes]";
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj == null) {
+            return false;
+        }
+
+        if (!(obj instanceof QueueSize)) {
+            return false;
+        }
+
+        final QueueSize other = (QueueSize) obj;
+        return getObjectCount() == other.getObjectCount() && getByteCount() == other.getByteCount();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/ConnectionStatus.java b/src/main/java/org/apache/nifi/controller/status/ConnectionStatus.java
new file mode 100644
index 0000000..bd2cd29
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/ConnectionStatus.java
@@ -0,0 +1,313 @@
+/*
+ * 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;
+
+import org.apache.nifi.controller.status.analytics.ConnectionStatusPredictions;
+import org.apache.nifi.processor.DataUnit;
+
+public class ConnectionStatus implements Cloneable {
+
+    private String id;
+    private String groupId;
+    private String name;
+    private String sourceId;
+    private String sourceName;
+    private String destinationId;
+    private String destinationName;
+    private String backPressureDataSizeThreshold;
+    private ConnectionStatusPredictions predictions;
+    private long backPressureBytesThreshold;
+    private long backPressureObjectThreshold;
+    private int inputCount;
+    private long inputBytes;
+    private int queuedCount;
+    private long queuedBytes;
+    private int outputCount;
+    private long outputBytes;
+    private int maxQueuedCount;
+    private long maxQueuedBytes;
+    private long totalQueuedDuration;
+    private long maxQueuedDuration;
+    private FlowFileAvailability flowFileAvailability;
+    private LoadBalanceStatus loadBalanceStatus;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(final String groupId) {
+        this.groupId = groupId;
+    }
+
+    public LoadBalanceStatus getLoadBalanceStatus() {
+        return loadBalanceStatus;
+    }
+
+    public void setLoadBalanceStatus(final LoadBalanceStatus loadBalanceStatus) {
+        this.loadBalanceStatus = loadBalanceStatus;
+    }
+
+    public int getQueuedCount() {
+        return queuedCount;
+    }
+
+    public void setQueuedCount(final int queuedCount) {
+        this.queuedCount = queuedCount;
+    }
+
+    public long getQueuedBytes() {
+        return queuedBytes;
+    }
+
+    public void setQueuedBytes(final long queuedBytes) {
+        this.queuedBytes = queuedBytes;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getSourceId() {
+        return sourceId;
+    }
+
+    public void setSourceId(String sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    public void setSourceName(String sourceName) {
+        this.sourceName = sourceName;
+    }
+
+    public String getDestinationId() {
+        return destinationId;
+    }
+
+    public void setDestinationId(String destinationId) {
+        this.destinationId = destinationId;
+    }
+
+    public String getDestinationName() {
+        return destinationName;
+    }
+
+    public void setDestinationName(String destinationName) {
+        this.destinationName = destinationName;
+    }
+
+    public String getBackPressureDataSizeThreshold() {
+        return backPressureDataSizeThreshold;
+    }
+
+    public void setBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) {
+        this.backPressureDataSizeThreshold = backPressureDataSizeThreshold;
+        setBackPressureBytesThreshold(DataUnit.parseDataSize(backPressureDataSizeThreshold, DataUnit.B).longValue());
+    }
+
+    public ConnectionStatusPredictions getPredictions() {
+        return predictions;
+    }
+
+    public void setPredictions(ConnectionStatusPredictions predictions) {
+        this.predictions = predictions;
+    }
+
+    public long getBackPressureObjectThreshold() {
+        return backPressureObjectThreshold;
+    }
+
+    public void setBackPressureObjectThreshold(long backPressureObjectThreshold) {
+        this.backPressureObjectThreshold = backPressureObjectThreshold;
+    }
+
+    public long getInputBytes() {
+        return inputBytes;
+    }
+
+    public void setInputBytes(long inputBytes) {
+        this.inputBytes = inputBytes;
+    }
+
+    public int getInputCount() {
+        return inputCount;
+    }
+
+    public void setInputCount(int inputCount) {
+        this.inputCount = inputCount;
+    }
+
+    public long getOutputBytes() {
+        return outputBytes;
+    }
+
+    public void setOutputBytes(long outputBytes) {
+        this.outputBytes = outputBytes;
+    }
+
+    public int getOutputCount() {
+        return outputCount;
+    }
+
+    public void setOutputCount(int outputCount) {
+        this.outputCount = outputCount;
+    }
+
+    public int getMaxQueuedCount() {
+        return maxQueuedCount;
+    }
+
+    public void setMaxQueuedCount(int maxQueuedCount) {
+        this.maxQueuedCount = maxQueuedCount;
+    }
+
+    public long getMaxQueuedBytes() {
+        return maxQueuedBytes;
+    }
+
+    public void setMaxQueuedBytes(long maxQueueBytes) {
+        this.maxQueuedBytes = maxQueueBytes;
+    }
+
+    public long getBackPressureBytesThreshold() {
+        return backPressureBytesThreshold;
+    }
+
+    public void setBackPressureBytesThreshold(long backPressureBytesThreshold) {
+        this.backPressureBytesThreshold = backPressureBytesThreshold;
+    }
+
+    public long getTotalQueuedDuration() {
+        return totalQueuedDuration;
+    }
+
+    public void setTotalQueuedDuration(long totalQueuedDuration) {
+        this.totalQueuedDuration = totalQueuedDuration;
+    }
+
+    public long getMaxQueuedDuration() {
+        return maxQueuedDuration;
+    }
+
+    public void setMaxQueuedDuration(long maxQueuedDuration) {
+        this.maxQueuedDuration = maxQueuedDuration;
+    }
+
+    public FlowFileAvailability getFlowFileAvailability() {
+        return flowFileAvailability;
+    }
+
+    public void setFlowFileAvailability(final FlowFileAvailability availability) {
+        this.flowFileAvailability = availability;
+    }
+
+    @Override
+    public ConnectionStatus clone() {
+        final ConnectionStatus clonedObj = new ConnectionStatus();
+        clonedObj.groupId = groupId;
+        clonedObj.id = id;
+        clonedObj.loadBalanceStatus = loadBalanceStatus;
+        clonedObj.inputBytes = inputBytes;
+        clonedObj.inputCount = inputCount;
+        clonedObj.name = name;
+        clonedObj.outputBytes = outputBytes;
+        clonedObj.outputCount = outputCount;
+        clonedObj.queuedBytes = queuedBytes;
+        clonedObj.queuedCount = queuedCount;
+        clonedObj.sourceId = sourceId;
+        clonedObj.sourceName = sourceName;
+        clonedObj.destinationId = destinationId;
+        clonedObj.destinationName = destinationName;
+        clonedObj.flowFileAvailability = flowFileAvailability;
+
+        if (predictions != null) {
+            clonedObj.setPredictions(predictions.clone());
+        }
+
+        clonedObj.backPressureDataSizeThreshold = backPressureDataSizeThreshold;
+        clonedObj.backPressureObjectThreshold = backPressureObjectThreshold;
+        clonedObj.maxQueuedBytes = maxQueuedBytes;
+        clonedObj.maxQueuedCount = maxQueuedCount;
+        clonedObj.totalQueuedDuration = totalQueuedDuration;
+        clonedObj.maxQueuedDuration = maxQueuedDuration;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("ConnectionStatus [id=");
+        builder.append(id);
+        builder.append(", groupId=");
+        builder.append(groupId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", sourceId=");
+        builder.append(sourceId);
+        builder.append(", sourceName=");
+        builder.append(sourceName);
+        builder.append(", destinationId=");
+        builder.append(destinationId);
+        builder.append(", destinationName=");
+        builder.append(destinationName);
+        builder.append(", backPressureDataSizeThreshold=");
+        builder.append(backPressureDataSizeThreshold);
+        builder.append(", backPressureObjectThreshold=");
+        builder.append(backPressureObjectThreshold);
+        builder.append(", flowFileAvailability=");
+        builder.append(flowFileAvailability);
+        builder.append(", inputCount=");
+        builder.append(inputCount);
+        builder.append(", inputBytes=");
+        builder.append(inputBytes);
+        builder.append(", queuedCount=");
+        builder.append(queuedCount);
+        builder.append(", queuedBytes=");
+        builder.append(queuedBytes);
+        builder.append(", outputCount=");
+        builder.append(outputCount);
+        builder.append(", outputBytes=");
+        builder.append(outputBytes);
+        builder.append(", maxQueuedCount=");
+        builder.append(maxQueuedCount);
+        builder.append(", maxQueueBytes=");
+        builder.append(maxQueuedBytes);
+        builder.append(", totalActiveQueuedDuration=");
+        builder.append(totalQueuedDuration);
+        builder.append(", maxActiveQueuedDuration=");
+        builder.append(maxQueuedDuration);
+        builder.append(", loadBalanceStatus=");
+        builder.append(loadBalanceStatus);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/FlowFileAvailability.java b/src/main/java/org/apache/nifi/controller/status/FlowFileAvailability.java
new file mode 100644
index 0000000..d805312
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/FlowFileAvailability.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public enum FlowFileAvailability {
+    ACTIVE_QUEUE_EMPTY,
+
+    HEAD_OF_QUEUE_PENALIZED,
+
+    FLOWFILE_AVAILABLE;
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/LoadBalanceStatus.java b/src/main/java/org/apache/nifi/controller/status/LoadBalanceStatus.java
new file mode 100644
index 0000000..bd3bed4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/LoadBalanceStatus.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public enum LoadBalanceStatus {
+    LOAD_BALANCE_NOT_CONFIGURED,
+
+    LOAD_BALANCE_ACTIVE,
+
+    LOAD_BALANCE_INACTIVE;
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/NodeStatus.java b/src/main/java/org/apache/nifi/controller/status/NodeStatus.java
new file mode 100644
index 0000000..0414ee2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/NodeStatus.java
@@ -0,0 +1,207 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The status of a NiFi node.
+ */
+public class NodeStatus implements Cloneable {
+    private long createdAtInMs;
+
+    private long freeHeap;
+    private long usedHeap;
+    private long heapUtilization;
+    private long freeNonHeap;
+    private long usedNonHeap;
+
+    private long openFileHandlers;
+    private double processorLoadAverage;
+
+    private long totalThreads;
+    private long timerDrivenThreads;
+
+    private long flowFileRepositoryFreeSpace;
+    private long flowFileRepositoryUsedSpace;
+
+    private List<StorageStatus> contentRepositories = new ArrayList<>();
+    private List<StorageStatus> provenanceRepositories = new ArrayList<>();
+
+    public long getCreatedAtInMs() {
+        return createdAtInMs;
+    }
+
+    public void setCreatedAtInMs(final long createdAtInMs) {
+        this.createdAtInMs = createdAtInMs;
+    }
+
+    public long getFreeHeap() {
+        return freeHeap;
+    }
+
+    public void setFreeHeap(final long freeHeap) {
+        this.freeHeap = freeHeap;
+    }
+
+    public long getUsedHeap() {
+        return usedHeap;
+    }
+
+    public void setUsedHeap(final long usedHeap) {
+        this.usedHeap = usedHeap;
+    }
+
+    public long getHeapUtilization() {
+        return heapUtilization;
+    }
+
+    public void setHeapUtilization(final long heapUtilization) {
+        this.heapUtilization = heapUtilization;
+    }
+
+    public long getFreeNonHeap() {
+        return freeNonHeap;
+    }
+
+    public void setFreeNonHeap(final long freeNonHeap) {
+        this.freeNonHeap = freeNonHeap;
+    }
+
+    public long getUsedNonHeap() {
+        return usedNonHeap;
+    }
+
+    public void setUsedNonHeap(final long usedNonHeap) {
+        this.usedNonHeap = usedNonHeap;
+    }
+
+    public long getOpenFileHandlers() {
+        return openFileHandlers;
+    }
+
+    public void setOpenFileHandlers(final long openFileHandlers) {
+        this.openFileHandlers = openFileHandlers;
+    }
+
+    public double getProcessorLoadAverage() {
+        return processorLoadAverage;
+    }
+
+    public void setProcessorLoadAverage(final double processorLoadAverage) {
+        this.processorLoadAverage = processorLoadAverage;
+    }
+
+    public long getTotalThreads() {
+        return totalThreads;
+    }
+
+    public void setTotalThreads(final long totalThreads) {
+        this.totalThreads = totalThreads;
+    }
+
+    public long getTimerDrivenThreads() {
+        return timerDrivenThreads;
+    }
+
+    public void setTimerDrivenThreads(final long timerDrivenThreads) {
+        this.timerDrivenThreads = timerDrivenThreads;
+    }
+
+    public long getFlowFileRepositoryFreeSpace() {
+        return flowFileRepositoryFreeSpace;
+    }
+
+    public void setFlowFileRepositoryFreeSpace(final long flowFileRepositoryFreeSpace) {
+        this.flowFileRepositoryFreeSpace = flowFileRepositoryFreeSpace;
+    }
+
+    public long getFlowFileRepositoryUsedSpace() {
+        return flowFileRepositoryUsedSpace;
+    }
+
+    public void setFlowFileRepositoryUsedSpace(final long flowFileRepositoryUsedSpace) {
+        this.flowFileRepositoryUsedSpace = flowFileRepositoryUsedSpace;
+    }
+
+    public List<StorageStatus> getContentRepositories() {
+        return contentRepositories;
+    }
+
+    public void setContentRepositories(final List<StorageStatus> contentRepositories) {
+        this.contentRepositories = new ArrayList<>();
+        this.contentRepositories.addAll(contentRepositories);
+    }
+
+    public List<StorageStatus> getProvenanceRepositories() {
+        return provenanceRepositories;
+    }
+
+    public void setProvenanceRepositories(final List<StorageStatus> provenanceRepositories) {
+        this.provenanceRepositories = new ArrayList<>();
+        this.provenanceRepositories.addAll(provenanceRepositories);
+    }
+
+    @Override
+    protected NodeStatus clone() {
+        final NodeStatus clonedObj = new NodeStatus();
+        clonedObj.createdAtInMs = createdAtInMs;
+        clonedObj.freeHeap = freeHeap;
+        clonedObj.usedHeap = usedHeap;
+        clonedObj.heapUtilization = heapUtilization;
+        clonedObj.freeNonHeap = freeNonHeap;
+        clonedObj.usedNonHeap = usedNonHeap;
+        clonedObj.openFileHandlers = openFileHandlers;
+        clonedObj.processorLoadAverage = processorLoadAverage;
+        clonedObj.totalThreads = totalThreads;
+        clonedObj.timerDrivenThreads = timerDrivenThreads;
+        clonedObj.flowFileRepositoryFreeSpace = flowFileRepositoryFreeSpace;
+        clonedObj.flowFileRepositoryUsedSpace = flowFileRepositoryUsedSpace;
+
+        final List<StorageStatus> clonedContentRepositories = new ArrayList<>();
+        contentRepositories.stream().map(r -> r.clone()).forEach(r -> clonedContentRepositories.add(r));
+        clonedObj.contentRepositories = clonedContentRepositories;
+
+        final List<StorageStatus> clonedProvenanceRepositories = new ArrayList<>();
+        provenanceRepositories.stream().map(r -> r.clone()).forEach(r -> clonedProvenanceRepositories.add(r));
+        clonedObj.provenanceRepositories = clonedProvenanceRepositories;
+
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("NodeStatus{");
+        sb.append("createdAtInMs=").append(createdAtInMs);
+        sb.append(", freeHeap=").append(freeHeap);
+        sb.append(", usedHeap=").append(usedHeap);
+        sb.append(", heapUtilization=").append(heapUtilization);
+        sb.append(", freeNonHeap=").append(freeNonHeap);
+        sb.append(", usedNonHeap=").append(usedNonHeap);
+        sb.append(", openFileHandlers=").append(openFileHandlers);
+        sb.append(", processorLoadAverage=").append(processorLoadAverage);
+        sb.append(", totalThreads=").append(totalThreads);
+        sb.append(", timerDrivenThreads=").append(timerDrivenThreads);
+        sb.append(", flowFileRepositoryFreeSpace=").append(flowFileRepositoryFreeSpace);
+        sb.append(", flowFileRepositoryUsedSpace=").append(flowFileRepositoryUsedSpace);
+        sb.append(", contentRepositories=").append(contentRepositories);
+        sb.append(", provenanceRepositories=").append(provenanceRepositories);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/PortStatus.java b/src/main/java/org/apache/nifi/controller/status/PortStatus.java
new file mode 100644
index 0000000..0d248cd
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/PortStatus.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.controller.status;
+
+/**
+ * The status of a port.
+ */
+public class PortStatus implements Cloneable {
+
+    private String id;
+    private String groupId;
+    private String name;
+    private Integer activeThreadCount;
+    private int inputCount;
+    private long inputBytes;
+    private int outputCount;
+    private long outputBytes;
+    private int flowFilesReceived;
+    private long bytesReceived;
+    private int flowFilesSent;
+    private long bytesSent;
+    private Boolean transmitting;
+    private RunStatus runStatus;
+
+    public Boolean isTransmitting() {
+        return transmitting;
+    }
+
+    public void setTransmitting(Boolean transmitting) {
+        this.transmitting = transmitting;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getActiveThreadCount() {
+        return activeThreadCount;
+    }
+
+    public void setActiveThreadCount(Integer activeThreadCount) {
+        this.activeThreadCount = activeThreadCount;
+    }
+
+    public int getInputCount() {
+        return inputCount;
+    }
+
+    public void setInputCount(int inputCount) {
+        this.inputCount = inputCount;
+    }
+
+    public long getInputBytes() {
+        return inputBytes;
+    }
+
+    public void setInputBytes(long inputBytes) {
+        this.inputBytes = inputBytes;
+    }
+
+    public int getOutputCount() {
+        return outputCount;
+    }
+
+    public void setOutputCount(final int outputCount) {
+        this.outputCount = outputCount;
+    }
+
+    public long getOutputBytes() {
+        return outputBytes;
+    }
+
+    public void setOutputBytes(final long outputBytes) {
+        this.outputBytes = outputBytes;
+    }
+
+    public RunStatus getRunStatus() {
+        return runStatus;
+    }
+
+    public void setRunStatus(RunStatus runStatus) {
+        this.runStatus = runStatus;
+    }
+
+    public int getFlowFilesReceived() {
+        return flowFilesReceived;
+    }
+
+    public void setFlowFilesReceived(int flowFilesReceived) {
+        this.flowFilesReceived = flowFilesReceived;
+    }
+
+    public long getBytesReceived() {
+        return bytesReceived;
+    }
+
+    public void setBytesReceived(long bytesReceived) {
+        this.bytesReceived = bytesReceived;
+    }
+
+    public int getFlowFilesSent() {
+        return flowFilesSent;
+    }
+
+    public void setFlowFilesSent(int flowFilesSent) {
+        this.flowFilesSent = flowFilesSent;
+    }
+
+    public long getBytesSent() {
+        return bytesSent;
+    }
+
+    public void setBytesSent(long bytesSent) {
+        this.bytesSent = bytesSent;
+    }
+
+    public Boolean getTransmitting() {
+        return transmitting;
+    }
+
+    @Override
+    public PortStatus clone() {
+        final PortStatus clonedObj = new PortStatus();
+        clonedObj.id = id;
+        clonedObj.groupId = groupId;
+        clonedObj.name = name;
+        clonedObj.activeThreadCount = activeThreadCount;
+        clonedObj.inputBytes = inputBytes;
+        clonedObj.inputCount = inputCount;
+        clonedObj.outputBytes = outputBytes;
+        clonedObj.outputCount = outputCount;
+        clonedObj.flowFilesReceived = flowFilesReceived;
+        clonedObj.bytesReceived = bytesReceived;
+        clonedObj.flowFilesSent = flowFilesSent;
+        clonedObj.bytesSent = bytesSent;
+        clonedObj.transmitting = transmitting;
+        clonedObj.runStatus = runStatus;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("PortStatus [id=");
+        builder.append(id);
+        builder.append(", groupId=");
+        builder.append(groupId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", activeThreadCount=");
+        builder.append(activeThreadCount);
+        builder.append(", transmitting=");
+        builder.append(transmitting);
+        builder.append(", inputCount=");
+        builder.append(inputCount);
+        builder.append(", inputBytes=");
+        builder.append(inputBytes);
+        builder.append(", outputCount=");
+        builder.append(outputCount);
+        builder.append(", outputBytes=");
+        builder.append(outputBytes);
+        builder.append(", runStatus=");
+        builder.append(runStatus);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java b/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
new file mode 100644
index 0000000..7aaf6e7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
@@ -0,0 +1,694 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.flow.VersionedFlowState;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ProcessGroupStatus implements Cloneable {
+
+    private String id;
+    private String name;
+    private VersionedFlowState versionedFlowState;
+    private Integer inputCount;
+    private Long inputContentSize;
+    private Integer outputCount;
+    private Long outputContentSize;
+    private Integer activeThreadCount;
+    private Integer statelessActiveThreadCount;
+    private Integer terminatedThreadCount;
+    private Integer queuedCount;
+    private Long queuedContentSize;
+    private Long bytesRead;
+    private Long bytesWritten;
+    private int flowFilesReceived;
+    private long bytesReceived;
+    private int flowFilesSent;
+    private long bytesSent;
+    private int flowFilesTransferred;
+    private long bytesTransferred;
+    private long processingNanos;
+
+    private Collection<ConnectionStatus> connectionStatus = new ArrayList<>();
+    private Collection<ProcessorStatus> processorStatus = new ArrayList<>();
+    private Collection<ProcessGroupStatus> processGroupStatus = new ArrayList<>();
+    private Collection<RemoteProcessGroupStatus> remoteProcessGroupStatus = new ArrayList<>();
+    private Collection<PortStatus> inputPortStatus = new ArrayList<>();
+    private Collection<PortStatus> outputPortStatus = new ArrayList<>();
+
+    private ProcessingPerformanceStatus processingPerformanceStatus;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public VersionedFlowState getVersionedFlowState() {
+        return versionedFlowState;
+    }
+
+    public void setVersionedFlowState(VersionedFlowState versionedFlowState) {
+        this.versionedFlowState = versionedFlowState;
+    }
+
+    public Integer getInputCount() {
+        return inputCount;
+    }
+
+    public void setInputCount(final Integer inputCount) {
+        this.inputCount = inputCount;
+    }
+
+    public Long getInputContentSize() {
+        return inputContentSize;
+    }
+
+    public void setInputContentSize(final Long inputContentSize) {
+        this.inputContentSize = inputContentSize;
+    }
+
+    public Integer getOutputCount() {
+        return outputCount;
+    }
+
+    public void setOutputCount(final Integer outputCount) {
+        this.outputCount = outputCount;
+    }
+
+    public Long getOutputContentSize() {
+        return outputContentSize;
+    }
+
+    public void setOutputContentSize(final Long outputContentSize) {
+        this.outputContentSize = outputContentSize;
+    }
+
+    public Long getBytesRead() {
+        return bytesRead;
+    }
+
+    public void setBytesRead(final Long bytesRead) {
+        this.bytesRead = bytesRead;
+    }
+
+    public Long getBytesWritten() {
+        return bytesWritten;
+    }
+
+    public void setBytesWritten(final Long bytesWritten) {
+        this.bytesWritten = bytesWritten;
+    }
+
+    public Integer getQueuedCount() {
+        return this.queuedCount;
+    }
+
+    public void setQueuedCount(final Integer queuedCount) {
+        this.queuedCount = queuedCount;
+    }
+
+    public Long getQueuedContentSize() {
+        return queuedContentSize;
+    }
+
+    public void setQueuedContentSize(final Long queuedContentSize) {
+        this.queuedContentSize = queuedContentSize;
+    }
+
+    public Integer getActiveThreadCount() {
+        return activeThreadCount;
+    }
+
+    public void setActiveThreadCount(final Integer activeThreadCount) {
+        this.activeThreadCount = activeThreadCount;
+    }
+
+    public Integer getStatelessActiveThreadCount() {
+        return statelessActiveThreadCount;
+    }
+
+    public void setStatelessActiveThreadCount(final Integer statelessActiveThreadCount) {
+        this.statelessActiveThreadCount = statelessActiveThreadCount;
+    }
+
+    public Integer getTerminatedThreadCount() {
+        return terminatedThreadCount;
+    }
+
+    public void setTerminatedThreadCount(Integer terminatedThreadCount) {
+        this.terminatedThreadCount = terminatedThreadCount;
+    }
+
+    public Collection<ConnectionStatus> getConnectionStatus() {
+        return connectionStatus;
+    }
+
+    public void setConnectionStatus(final Collection<ConnectionStatus> connectionStatus) {
+        this.connectionStatus = connectionStatus;
+    }
+
+    public Collection<ProcessorStatus> getProcessorStatus() {
+        return processorStatus;
+    }
+
+    public void setProcessorStatus(final Collection<ProcessorStatus> processorStatus) {
+        this.processorStatus = processorStatus;
+    }
+
+    public Collection<ProcessGroupStatus> getProcessGroupStatus() {
+        return processGroupStatus;
+    }
+
+    public void setProcessGroupStatus(final Collection<ProcessGroupStatus> processGroupStatus) {
+        this.processGroupStatus = processGroupStatus;
+    }
+
+    public Collection<PortStatus> getInputPortStatus() {
+        return inputPortStatus;
+    }
+
+    public void setInputPortStatus(Collection<PortStatus> inputPortStatus) {
+        this.inputPortStatus = inputPortStatus;
+    }
+
+    public Collection<PortStatus> getOutputPortStatus() {
+        return outputPortStatus;
+    }
+
+    public void setOutputPortStatus(Collection<PortStatus> outputPortStatus) {
+        this.outputPortStatus = outputPortStatus;
+    }
+
+    public Collection<RemoteProcessGroupStatus> getRemoteProcessGroupStatus() {
+        return remoteProcessGroupStatus;
+    }
+
+    public void setRemoteProcessGroupStatus(final Collection<RemoteProcessGroupStatus> remoteProcessGroupStatus) {
+        this.remoteProcessGroupStatus = remoteProcessGroupStatus;
+    }
+
+    public int getFlowFilesReceived() {
+        return flowFilesReceived;
+    }
+
+    public void setFlowFilesReceived(final int flowFilesReceived) {
+        this.flowFilesReceived = flowFilesReceived;
+    }
+
+    public long getBytesReceived() {
+        return bytesReceived;
+    }
+
+    public void setBytesReceived(final long bytesReceived) {
+        this.bytesReceived = bytesReceived;
+    }
+
+    public int getFlowFilesSent() {
+        return flowFilesSent;
+    }
+
+    public void setFlowFilesSent(final int flowFilesSent) {
+        this.flowFilesSent = flowFilesSent;
+    }
+
+    public long getBytesSent() {
+        return bytesSent;
+    }
+
+    public void setBytesSent(final long bytesSent) {
+        this.bytesSent = bytesSent;
+    }
+
+    public int getFlowFilesTransferred() {
+        return flowFilesTransferred;
+    }
+
+    public void setFlowFilesTransferred(int flowFilesTransferred) {
+        this.flowFilesTransferred = flowFilesTransferred;
+    }
+
+    public long getBytesTransferred() {
+        return bytesTransferred;
+    }
+
+    public void setBytesTransferred(long bytesTransferred) {
+        this.bytesTransferred = bytesTransferred;
+    }
+
+    public long getProcessingNanos() {
+        return processingNanos;
+    }
+
+    public void setProcessingNanos(long processingNanos) {
+        this.processingNanos = processingNanos;
+    }
+
+    public ProcessingPerformanceStatus getProcessingPerformanceStatus() {
+        return processingPerformanceStatus;
+    }
+
+    public void setProcessingPerformanceStatus(ProcessingPerformanceStatus processingPerformanceStatus) {
+        this.processingPerformanceStatus = processingPerformanceStatus;
+    }
+
+    @Override
+    public ProcessGroupStatus clone() {
+        final ProcessGroupStatus clonedObj = new ProcessGroupStatus();
+
+        clonedObj.id = id;
+        clonedObj.name = name;
+        clonedObj.outputContentSize = outputContentSize;
+        clonedObj.outputCount = outputCount;
+        clonedObj.inputContentSize = inputContentSize;
+        clonedObj.inputCount = inputCount;
+        clonedObj.activeThreadCount = activeThreadCount;
+        clonedObj.terminatedThreadCount = terminatedThreadCount;
+        clonedObj.queuedContentSize = queuedContentSize;
+        clonedObj.queuedCount = queuedCount;
+        clonedObj.bytesRead = bytesRead;
+        clonedObj.bytesWritten = bytesWritten;
+        clonedObj.flowFilesReceived = flowFilesReceived;
+        clonedObj.bytesReceived = bytesReceived;
+        clonedObj.flowFilesSent = flowFilesSent;
+        clonedObj.bytesSent = bytesSent;
+        clonedObj.flowFilesTransferred = flowFilesTransferred;
+        clonedObj.bytesTransferred = bytesTransferred;
+        clonedObj.processingNanos = processingNanos;
+        clonedObj.processingPerformanceStatus = processingPerformanceStatus;
+
+        if (connectionStatus != null) {
+            final Collection<ConnectionStatus> statusList = new ArrayList<>();
+            clonedObj.setConnectionStatus(statusList);
+            for (final ConnectionStatus status : connectionStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        if (processorStatus != null) {
+            final Collection<ProcessorStatus> statusList = new ArrayList<>();
+            clonedObj.setProcessorStatus(statusList);
+            for (final ProcessorStatus status : processorStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        if (inputPortStatus != null) {
+            final Collection<PortStatus> statusList = new ArrayList<>();
+            clonedObj.setInputPortStatus(statusList);
+            for (final PortStatus status : inputPortStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        if (outputPortStatus != null) {
+            final Collection<PortStatus> statusList = new ArrayList<>();
+            clonedObj.setOutputPortStatus(statusList);
+            for (final PortStatus status : outputPortStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        if (processGroupStatus != null) {
+            final Collection<ProcessGroupStatus> statusList = new ArrayList<>();
+            clonedObj.setProcessGroupStatus(statusList);
+            for (final ProcessGroupStatus status : processGroupStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        if (remoteProcessGroupStatus != null) {
+            final Collection<RemoteProcessGroupStatus> statusList = new ArrayList<>();
+            clonedObj.setRemoteProcessGroupStatus(statusList);
+            for (final RemoteProcessGroupStatus status : remoteProcessGroupStatus) {
+                statusList.add(status.clone());
+            }
+        }
+
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("ProcessGroupStatus [id=");
+        builder.append(id);
+        builder.append(", inputCount=");
+        builder.append(inputCount);
+        builder.append(", inputBytes=");
+        builder.append(inputContentSize);
+        builder.append(", outputCount=");
+        builder.append(outputCount);
+        builder.append(", outputBytes=");
+        builder.append(outputContentSize);
+        builder.append(", activeThreadCount=");
+        builder.append(activeThreadCount);
+        builder.append(", terminatedThreadCount=");
+        builder.append(terminatedThreadCount);
+        builder.append(", flowFilesTransferred=");
+        builder.append(flowFilesTransferred);
+        builder.append(", bytesTransferred=");
+        builder.append(bytesTransferred);
+        builder.append(", flowFilesReceived=");
+        builder.append(flowFilesReceived);
+        builder.append(", bytesReceived=");
+        builder.append(bytesReceived);
+        builder.append(", flowFilesSent=");
+        builder.append(flowFilesSent);
+        builder.append(", bytesSent=");
+        builder.append(bytesSent);
+        builder.append(", processingNanos=");
+        builder.append(processingNanos);
+        builder.append(",\n\tconnectionStatus=");
+
+        for (final ConnectionStatus status : connectionStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(",\n\tprocessorStatus=");
+
+        for (final ProcessorStatus status : processorStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(",\n\tprocessGroupStatus=");
+
+        for (final ProcessGroupStatus status : processGroupStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(",\n\tremoteProcessGroupStatus=");
+        for (final RemoteProcessGroupStatus status : remoteProcessGroupStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(",\n\tinputPortStatus=");
+        for (final PortStatus status : inputPortStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(",\n\toutputPortStatus=");
+        for (final PortStatus status : outputPortStatus) {
+            builder.append("\n\t\t");
+            builder.append(status);
+        }
+
+        builder.append(", processingPerformanceStatus=");
+        builder.append(processingPerformanceStatus);
+
+        builder.append("]");
+        return builder.toString();
+    }
+
+    public static void merge(final ProcessGroupStatus target, final ProcessGroupStatus toMerge) {
+        if (target == null || toMerge == null) {
+            return;
+        }
+
+        target.setInputCount(target.getInputCount() + toMerge.getInputCount());
+        target.setInputContentSize(target.getInputContentSize() + toMerge.getInputContentSize());
+        target.setOutputCount(target.getOutputCount() + toMerge.getOutputCount());
+        target.setOutputContentSize(target.getOutputContentSize() + toMerge.getOutputContentSize());
+        target.setQueuedCount(target.getQueuedCount() + toMerge.getQueuedCount());
+        target.setQueuedContentSize(target.getQueuedContentSize() + toMerge.getQueuedContentSize());
+        target.setBytesRead(target.getBytesRead() + toMerge.getBytesRead());
+        target.setBytesWritten(target.getBytesWritten() + toMerge.getBytesWritten());
+        target.setActiveThreadCount(target.getActiveThreadCount() + toMerge.getActiveThreadCount());
+        target.setStatelessActiveThreadCount(target.getStatelessActiveThreadCount() + toMerge.getStatelessActiveThreadCount());
+        target.setTerminatedThreadCount(target.getTerminatedThreadCount() + toMerge.getTerminatedThreadCount());
+        target.setFlowFilesTransferred(target.getFlowFilesTransferred() + toMerge.getFlowFilesTransferred());
+        target.setBytesTransferred(target.getBytesTransferred() + toMerge.getBytesTransferred());
+        target.setFlowFilesReceived(target.getFlowFilesReceived() + toMerge.getFlowFilesReceived());
+        target.setBytesReceived(target.getBytesReceived() + toMerge.getBytesReceived());
+        target.setFlowFilesSent(target.getFlowFilesSent() + toMerge.getFlowFilesSent());
+        target.setBytesSent(target.getBytesSent() + toMerge.getBytesSent());
+        target.setProcessingNanos(target.getProcessingNanos() + toMerge.getProcessingNanos());
+
+        // if the versioned flow state to merge is sync failure allow it to take precedence.
+        if (VersionedFlowState.SYNC_FAILURE.equals(toMerge.getVersionedFlowState())) {
+            target.setVersionedFlowState(VersionedFlowState.SYNC_FAILURE);
+        }
+
+        // connection status
+        // sort by id
+        final Map<String, ConnectionStatus> mergedConnectionMap = new HashMap<>();
+        for (final ConnectionStatus status : target.getConnectionStatus()) {
+            mergedConnectionMap.put(status.getId(), status);
+        }
+
+        for (final ConnectionStatus statusToMerge : toMerge.getConnectionStatus()) {
+            ConnectionStatus merged = mergedConnectionMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedConnectionMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            merged.setQueuedCount(merged.getQueuedCount() + statusToMerge.getQueuedCount());
+            merged.setQueuedBytes(merged.getQueuedBytes() + statusToMerge.getQueuedBytes());
+            merged.setInputCount(merged.getInputCount() + statusToMerge.getInputCount());
+            merged.setInputBytes(merged.getInputBytes() + statusToMerge.getInputBytes());
+            merged.setOutputCount(merged.getOutputCount() + statusToMerge.getOutputCount());
+            merged.setOutputBytes(merged.getOutputBytes() + statusToMerge.getOutputBytes());
+            merged.setFlowFileAvailability(mergeFlowFileAvailability(merged.getFlowFileAvailability(), statusToMerge.getFlowFileAvailability()));
+            merged.setLoadBalanceStatus(mergeLoadBalanceStatus(merged.getLoadBalanceStatus(), statusToMerge.getLoadBalanceStatus()));
+        }
+        target.setConnectionStatus(mergedConnectionMap.values());
+
+        // processor status
+        final Map<String, ProcessorStatus> mergedProcessorMap = new HashMap<>();
+        for (final ProcessorStatus status : target.getProcessorStatus()) {
+            mergedProcessorMap.put(status.getId(), status);
+        }
+
+        for (final ProcessorStatus statusToMerge : toMerge.getProcessorStatus()) {
+            ProcessorStatus merged = mergedProcessorMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedProcessorMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            merged.setActiveThreadCount(merged.getActiveThreadCount() + statusToMerge.getActiveThreadCount());
+            merged.setTerminatedThreadCount(merged.getTerminatedThreadCount() + statusToMerge.getTerminatedThreadCount());
+            merged.setBytesRead(merged.getBytesRead() + statusToMerge.getBytesRead());
+            merged.setBytesWritten(merged.getBytesWritten() + statusToMerge.getBytesWritten());
+            merged.setInputBytes(merged.getInputBytes() + statusToMerge.getInputBytes());
+            merged.setInputCount(merged.getInputCount() + statusToMerge.getInputCount());
+            merged.setInvocations(merged.getInvocations() + statusToMerge.getInvocations());
+            merged.setOutputBytes(merged.getOutputBytes() + statusToMerge.getOutputBytes());
+            merged.setOutputCount(merged.getOutputCount() + statusToMerge.getOutputCount());
+            merged.setProcessingNanos(merged.getProcessingNanos() + statusToMerge.getProcessingNanos());
+            merged.setFlowFilesRemoved(merged.getFlowFilesRemoved() + statusToMerge.getFlowFilesRemoved());
+
+            // if the status to merge is invalid allow it to take precedence. whether the
+            // processor run status is disabled/stopped/running is part of the flow configuration
+            // and should not differ amongst nodes. however, whether a processor is invalid
+            // can be driven by environmental conditions. this check allows any of those to
+            // take precedence over the configured run status.
+            if (RunStatus.Validating.equals(statusToMerge.getRunStatus())) {
+                merged.setRunStatus(RunStatus.Validating);
+            } else if (RunStatus.Invalid.equals(statusToMerge.getRunStatus())) {
+                merged.setRunStatus(RunStatus.Invalid);
+            }
+        }
+        target.setProcessorStatus(mergedProcessorMap.values());
+
+        // input ports
+        final Map<String, PortStatus> mergedInputPortMap = new HashMap<>();
+        for (final PortStatus status : target.getInputPortStatus()) {
+            mergedInputPortMap.put(status.getId(), status);
+        }
+
+        for (final PortStatus statusToMerge : toMerge.getInputPortStatus()) {
+            PortStatus merged = mergedInputPortMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedInputPortMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            merged.setInputBytes(merged.getInputBytes() + statusToMerge.getInputBytes());
+            merged.setInputCount(merged.getInputCount() + statusToMerge.getInputCount());
+            merged.setOutputBytes(merged.getOutputBytes() + statusToMerge.getOutputBytes());
+            merged.setOutputCount(merged.getOutputCount() + statusToMerge.getOutputCount());
+            merged.setActiveThreadCount(merged.getActiveThreadCount() + statusToMerge.getActiveThreadCount());
+            if (statusToMerge.isTransmitting() != null && statusToMerge.isTransmitting()) {
+                merged.setTransmitting(true);
+            }
+
+            // should be unnecessary here since ports run status should not be affected by
+            // environmental conditions but doing so in case that changes
+            if (RunStatus.Invalid.equals(statusToMerge.getRunStatus())) {
+                merged.setRunStatus(RunStatus.Invalid);
+            }
+        }
+        target.setInputPortStatus(mergedInputPortMap.values());
+
+        // output ports
+        final Map<String, PortStatus> mergedOutputPortMap = new HashMap<>();
+        for (final PortStatus status : target.getOutputPortStatus()) {
+            mergedOutputPortMap.put(status.getId(), status);
+        }
+
+        for (final PortStatus statusToMerge : toMerge.getOutputPortStatus()) {
+            PortStatus merged = mergedOutputPortMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedOutputPortMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            merged.setInputBytes(merged.getInputBytes() + statusToMerge.getInputBytes());
+            merged.setInputCount(merged.getInputCount() + statusToMerge.getInputCount());
+            merged.setOutputBytes(merged.getOutputBytes() + statusToMerge.getOutputBytes());
+            merged.setOutputCount(merged.getOutputCount() + statusToMerge.getOutputCount());
+            merged.setActiveThreadCount(merged.getActiveThreadCount() + statusToMerge.getActiveThreadCount());
+            if (statusToMerge.isTransmitting() != null && statusToMerge.isTransmitting()) {
+                merged.setTransmitting(true);
+            }
+
+            // should be unnecessary here since ports run status not should be affected by
+            // environmental conditions but doing so in case that changes
+            if (RunStatus.Invalid.equals(statusToMerge.getRunStatus())) {
+                merged.setRunStatus(RunStatus.Invalid);
+            }
+        }
+        target.setOutputPortStatus(mergedOutputPortMap.values());
+
+        // child groups
+        final Map<String, ProcessGroupStatus> mergedGroupMap = new HashMap<>();
+        for (final ProcessGroupStatus status : target.getProcessGroupStatus()) {
+            mergedGroupMap.put(status.getId(), status);
+        }
+
+        for (final ProcessGroupStatus statusToMerge : toMerge.getProcessGroupStatus()) {
+            ProcessGroupStatus merged = mergedGroupMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedGroupMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            merge(merged, statusToMerge);
+        }
+        target.setOutputPortStatus(mergedOutputPortMap.values());
+
+        // remote groups
+        final Map<String, RemoteProcessGroupStatus> mergedRemoteGroupMap = new HashMap<>();
+        for (final RemoteProcessGroupStatus status : target.getRemoteProcessGroupStatus()) {
+            mergedRemoteGroupMap.put(status.getId(), status);
+        }
+
+        for (final RemoteProcessGroupStatus statusToMerge : toMerge.getRemoteProcessGroupStatus()) {
+            RemoteProcessGroupStatus merged = mergedRemoteGroupMap.get(statusToMerge.getId());
+            if (merged == null) {
+                mergedRemoteGroupMap.put(statusToMerge.getId(), statusToMerge.clone());
+                continue;
+            }
+
+            // NOTE - active/inactive port counts are not merged since that state is considered part of the flow (like runStatus)
+            merged.setReceivedContentSize(merged.getReceivedContentSize() + statusToMerge.getReceivedContentSize());
+            merged.setReceivedCount(merged.getReceivedCount() + statusToMerge.getReceivedCount());
+            merged.setSentContentSize(merged.getSentContentSize() + statusToMerge.getSentContentSize());
+            merged.setSentCount(merged.getSentCount() + statusToMerge.getSentCount());
+            merged.setActiveThreadCount(merged.getActiveThreadCount() + statusToMerge.getActiveThreadCount());
+
+            // Take the earliest last refresh time
+            final Date mergedLastRefreshTime = merged.getLastRefreshTime();
+            final Date toMergeLastRefreshTime = statusToMerge.getLastRefreshTime();
+            if (mergedLastRefreshTime == null || (toMergeLastRefreshTime != null && toMergeLastRefreshTime.before(mergedLastRefreshTime))) {
+                merged.setLastRefreshTime(toMergeLastRefreshTime);
+            }
+        }
+
+        target.setRemoteProcessGroupStatus(mergedRemoteGroupMap.values());
+
+        final ProcessingPerformanceStatus targetPerformanceStatus = target.getProcessingPerformanceStatus();
+        final ProcessingPerformanceStatus toMergePerformanceStatus = toMerge.getProcessingPerformanceStatus();
+
+        if (targetPerformanceStatus != null && toMergePerformanceStatus != null) {
+            targetPerformanceStatus.setIdentifier(toMergePerformanceStatus.getIdentifier());
+            targetPerformanceStatus.setCpuDuration(targetPerformanceStatus.getCpuDuration() + toMergePerformanceStatus.getCpuDuration());
+            targetPerformanceStatus.setContentReadDuration(targetPerformanceStatus.getContentReadDuration() + toMergePerformanceStatus.getContentReadDuration());
+            targetPerformanceStatus.setContentWriteDuration(targetPerformanceStatus.getContentWriteDuration() + toMergePerformanceStatus.getContentWriteDuration());
+            targetPerformanceStatus.setSessionCommitDuration(targetPerformanceStatus.getSessionCommitDuration() + toMergePerformanceStatus.getSessionCommitDuration());
+            targetPerformanceStatus.setGarbageCollectionDuration(targetPerformanceStatus.getGarbageCollectionDuration() + toMergePerformanceStatus.getGarbageCollectionDuration());
+        } else {
+            target.setProcessingPerformanceStatus(targetPerformanceStatus);
+        }
+    }
+
+    public static FlowFileAvailability mergeFlowFileAvailability(final FlowFileAvailability availabilityA, final FlowFileAvailability availabilityB) {
+        if (availabilityA == availabilityB) {
+            return availabilityA;
+        }
+        if (availabilityA == null) {
+            return availabilityB;
+        }
+        if (availabilityB == null) {
+            return availabilityA;
+        }
+
+        if (availabilityA == FlowFileAvailability.FLOWFILE_AVAILABLE || availabilityB == FlowFileAvailability.FLOWFILE_AVAILABLE) {
+            return FlowFileAvailability.FLOWFILE_AVAILABLE;
+        }
+
+        if (availabilityA == FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED || availabilityB == FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED) {
+            return FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED;
+        }
+
+        return FlowFileAvailability.FLOWFILE_AVAILABLE;
+    }
+
+    public static LoadBalanceStatus mergeLoadBalanceStatus(final LoadBalanceStatus statusA, final LoadBalanceStatus statusB) {
+        if (statusA == statusB) {
+            return statusA;
+        }
+        if (statusA == null) {
+            return statusB;
+        }
+        if (statusB == null) {
+            return statusA;
+        }
+
+        if (statusA == LoadBalanceStatus.LOAD_BALANCE_ACTIVE || statusB == LoadBalanceStatus.LOAD_BALANCE_ACTIVE) {
+            return LoadBalanceStatus.LOAD_BALANCE_ACTIVE;
+        }
+
+        if (statusA == LoadBalanceStatus.LOAD_BALANCE_INACTIVE || statusB == LoadBalanceStatus.LOAD_BALANCE_INACTIVE) {
+            return LoadBalanceStatus.LOAD_BALANCE_INACTIVE;
+        }
+
+        return LoadBalanceStatus.LOAD_BALANCE_NOT_CONFIGURED;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/ProcessingPerformanceStatus.java b/src/main/java/org/apache/nifi/controller/status/ProcessingPerformanceStatus.java
new file mode 100644
index 0000000..e87894c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/ProcessingPerformanceStatus.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+public class ProcessingPerformanceStatus implements Cloneable {
+
+    private String identifier;
+    private long cpuDuration;
+    private long contentReadDuration;
+    private long contentWriteDuration;
+    private long sessionCommitDuration;
+    private long garbageCollectionDuration;
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public long getCpuDuration() {
+        return cpuDuration;
+    }
+
+    public void setCpuDuration(long cpuDuration) {
+        this.cpuDuration = cpuDuration;
+    }
+
+    public long getContentReadDuration() {
+        return contentReadDuration;
+    }
+
+    public void setContentReadDuration(long contentReadDuration) {
+        this.contentReadDuration = contentReadDuration;
+    }
+
+    public long getContentWriteDuration() {
+        return contentWriteDuration;
+    }
+
+    public void setContentWriteDuration(long contentWriteDuration) {
+        this.contentWriteDuration = contentWriteDuration;
+    }
+
+    public long getSessionCommitDuration() {
+        return sessionCommitDuration;
+    }
+
+    public void setSessionCommitDuration(long sessionCommitDuration) {
+        this.sessionCommitDuration = sessionCommitDuration;
+    }
+
+    public long getGarbageCollectionDuration() {
+        return garbageCollectionDuration;
+    }
+
+    public void setGarbageCollectionDuration(long garbageCollectionDuration) {
+        this.garbageCollectionDuration = garbageCollectionDuration;
+    }
+
+    @Override
+    public ProcessingPerformanceStatus clone() {
+        final ProcessingPerformanceStatus clonedObj = new ProcessingPerformanceStatus();
+
+        clonedObj.identifier = identifier;
+        clonedObj.cpuDuration = cpuDuration;
+        clonedObj.contentReadDuration = contentReadDuration;
+        clonedObj.contentWriteDuration = contentWriteDuration;
+        clonedObj.sessionCommitDuration = sessionCommitDuration;
+        clonedObj.garbageCollectionDuration = garbageCollectionDuration;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("ProcessorPerformanceStatus [Group ID= ");
+        builder.append(identifier);
+        builder.append(", cpuDuration= ");
+        builder.append(cpuDuration);
+        builder.append(", contentReadDuration= ");
+        builder.append(contentReadDuration);
+        builder.append(", contentWriteDuration= ");
+        builder.append(contentWriteDuration);
+        builder.append(", sessionCommitDuration= ");
+        builder.append(sessionCommitDuration);
+        builder.append(", garbageCollectionDuration= ");
+        builder.append(garbageCollectionDuration);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/ProcessorStatus.java b/src/main/java/org/apache/nifi/controller/status/ProcessorStatus.java
new file mode 100644
index 0000000..24f74fe
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/ProcessorStatus.java
@@ -0,0 +1,323 @@
+/*
+ * 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;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.nifi.scheduling.ExecutionNode;
+
+public class ProcessorStatus implements Cloneable {
+
+    private String id;
+    private String groupId;
+    private String name;
+    private String type;
+    private RunStatus runStatus;
+    private ExecutionNode executionNode;
+    private int inputCount;
+    private long inputBytes;
+    private int outputCount;
+    private long outputBytes;
+    private long bytesRead;
+    private long bytesWritten;
+    private int invocations;
+    private long processingNanos;
+    private int flowFilesRemoved;
+    private long averageLineageDuration;
+    private int activeThreadCount;
+    private int terminatedThreadCount;
+    private int flowFilesReceived;
+    private long bytesReceived;
+    private int flowFilesSent;
+    private long bytesSent;
+    private Map<String, Long> counters;
+    private ProcessingPerformanceStatus processingPerformanceStatus;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(final String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    public int getInputCount() {
+        return inputCount;
+    }
+
+    public RunStatus getRunStatus() {
+        return runStatus;
+    }
+
+    public void setRunStatus(RunStatus runStatus) {
+        this.runStatus = runStatus;
+    }
+
+    public ExecutionNode getExecutionNode() {
+        return executionNode;
+    }
+
+    public void setExecutionNode(ExecutionNode executionNode) {
+        this.executionNode = executionNode;
+    }
+
+    public void setInputCount(final int inputCount) {
+        this.inputCount = inputCount;
+    }
+
+    public long getInputBytes() {
+        return inputBytes;
+    }
+
+    public void setInputBytes(final long inputBytes) {
+        this.inputBytes = inputBytes;
+    }
+
+    public int getOutputCount() {
+        return outputCount;
+    }
+
+    public void setOutputCount(final int outputCount) {
+        this.outputCount = outputCount;
+    }
+
+    public long getOutputBytes() {
+        return outputBytes;
+    }
+
+    public void setOutputBytes(final long outputBytes) {
+        this.outputBytes = outputBytes;
+    }
+
+    public long getBytesRead() {
+        return bytesRead;
+    }
+
+    public void setBytesRead(final long bytesRead) {
+        this.bytesRead = bytesRead;
+    }
+
+    public long getBytesWritten() {
+        return bytesWritten;
+    }
+
+    public void setBytesWritten(final long bytesWritten) {
+        this.bytesWritten = bytesWritten;
+    }
+
+    public int getInvocations() {
+        return invocations;
+    }
+
+    public void setInvocations(final int invocations) {
+        this.invocations = invocations;
+    }
+
+    public long getProcessingNanos() {
+        return processingNanos;
+    }
+
+    public void setProcessingNanos(final long processingNanos) {
+        this.processingNanos = processingNanos;
+    }
+
+    public long getAverageLineageDuration(final TimeUnit timeUnit) {
+        return TimeUnit.MILLISECONDS.convert(averageLineageDuration, timeUnit);
+    }
+
+    public void setAverageLineageDuration(final long duration, final TimeUnit timeUnit) {
+        this.averageLineageDuration = timeUnit.toMillis(duration);
+    }
+
+    public long getAverageLineageDuration() {
+        return averageLineageDuration;
+    }
+
+    public void setAverageLineageDuration(final long millis) {
+        this.averageLineageDuration = millis;
+    }
+
+    public int getFlowFilesRemoved() {
+        return flowFilesRemoved;
+    }
+
+    public void setFlowFilesRemoved(int flowFilesRemoved) {
+        this.flowFilesRemoved = flowFilesRemoved;
+    }
+
+    public int getActiveThreadCount() {
+        return activeThreadCount;
+    }
+
+    public void setActiveThreadCount(final int activeThreadCount) {
+        this.activeThreadCount = activeThreadCount;
+    }
+
+    public int getTerminatedThreadCount() {
+        return terminatedThreadCount;
+    }
+
+    public void setTerminatedThreadCount(int terminatedThreadCount) {
+        this.terminatedThreadCount = terminatedThreadCount;
+    }
+
+    public int getFlowFilesReceived() {
+        return flowFilesReceived;
+    }
+
+    public void setFlowFilesReceived(int flowFilesReceived) {
+        this.flowFilesReceived = flowFilesReceived;
+    }
+
+    public long getBytesReceived() {
+        return bytesReceived;
+    }
+
+    public void setBytesReceived(long bytesReceived) {
+        this.bytesReceived = bytesReceived;
+    }
+
+    public int getFlowFilesSent() {
+        return flowFilesSent;
+    }
+
+    public void setFlowFilesSent(int flowFilesSent) {
+        this.flowFilesSent = flowFilesSent;
+    }
+
+    public long getBytesSent() {
+        return bytesSent;
+    }
+
+    public void setBytesSent(long bytesSent) {
+        this.bytesSent = bytesSent;
+    }
+
+    public Map<String, Long> getCounters() {
+        return counters;
+    }
+
+    public void setCounters(final Map<String, Long> counters) {
+        this.counters = counters;
+    }
+
+    public ProcessingPerformanceStatus getProcessingPerformanceStatus() {
+        return processingPerformanceStatus;
+    }
+
+    public void setProcessingPerformanceStatus(ProcessingPerformanceStatus processingPerformanceStatus) {
+        this.processingPerformanceStatus = processingPerformanceStatus;
+    }
+
+    @Override
+    public ProcessorStatus clone() {
+        final ProcessorStatus clonedObj = new ProcessorStatus();
+        clonedObj.activeThreadCount = activeThreadCount;
+        clonedObj.terminatedThreadCount = terminatedThreadCount;
+        clonedObj.bytesRead = bytesRead;
+        clonedObj.bytesWritten = bytesWritten;
+        clonedObj.flowFilesReceived = flowFilesReceived;
+        clonedObj.bytesReceived = bytesReceived;
+        clonedObj.flowFilesSent = flowFilesSent;
+        clonedObj.bytesSent = bytesSent;
+        clonedObj.groupId = groupId;
+        clonedObj.id = id;
+        clonedObj.inputBytes = inputBytes;
+        clonedObj.inputCount = inputCount;
+        clonedObj.invocations = invocations;
+        clonedObj.name = name;
+        clonedObj.outputBytes = outputBytes;
+        clonedObj.outputCount = outputCount;
+        clonedObj.processingNanos = processingNanos;
+        clonedObj.averageLineageDuration = averageLineageDuration;
+        clonedObj.flowFilesRemoved = flowFilesRemoved;
+        clonedObj.runStatus = runStatus;
+        clonedObj.executionNode = executionNode;
+        clonedObj.type = type;
+        clonedObj.counters = counters == null ? null : new HashMap<>(counters);
+        clonedObj.processingPerformanceStatus = processingPerformanceStatus;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("ProcessorStatus [id=");
+        builder.append(id);
+        builder.append(", groupId=");
+        builder.append(groupId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", type=");
+        builder.append(type);
+        builder.append(", runStatus=");
+        builder.append(runStatus);
+        builder.append(", executionNode=");
+        builder.append(executionNode);
+        builder.append(", inputCount=");
+        builder.append(inputCount);
+        builder.append(", inputBytes=");
+        builder.append(inputBytes);
+        builder.append(", outputCount=");
+        builder.append(outputCount);
+        builder.append(", outputBytes=");
+        builder.append(outputBytes);
+        builder.append(", bytesRead=");
+        builder.append(bytesRead);
+        builder.append(", bytesWritten=");
+        builder.append(bytesWritten);
+        builder.append(", invocations=");
+        builder.append(invocations);
+        builder.append(", processingNanos=");
+        builder.append(processingNanos);
+        builder.append(", activeThreadCount=");
+        builder.append(activeThreadCount);
+        builder.append(", terminatedThreadCount=");
+        builder.append(terminatedThreadCount);
+        builder.append(", counters=");
+        builder.append(counters);
+        builder.append(", processingPerformanceStatus=");
+        builder.append(processingPerformanceStatus);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/RemoteProcessGroupStatus.java b/src/main/java/org/apache/nifi/controller/status/RemoteProcessGroupStatus.java
new file mode 100644
index 0000000..b0ea196
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/RemoteProcessGroupStatus.java
@@ -0,0 +1,236 @@
+/*
+ * 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;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+public class RemoteProcessGroupStatus implements Cloneable {
+
+    private String id;
+    private String groupId;
+    private TransmissionStatus transmissionStatus;
+    private String uri;
+    private String name;
+    private String comments;
+    private String authorizationIssue;
+    private Date lastRefreshTime;
+    private Integer activeThreadCount;
+    private int sentCount;
+    private long sentContentSize;
+    private int receivedCount;
+    private long receivedContentSize;
+    private Integer activeRemotePortCount;
+    private Integer inactiveRemotePortCount;
+
+    private long averageLineageDuration;
+
+    public String getTargetUri() {
+        return uri;
+    }
+
+    public void setTargetUri(String uri) {
+        this.uri = uri;
+    }
+
+    public TransmissionStatus getTransmissionStatus() {
+        return transmissionStatus;
+    }
+
+    public void setTransmissionStatus(TransmissionStatus transmissionStatus) {
+        this.transmissionStatus = transmissionStatus;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    public String getAuthorizationIssue() {
+        return authorizationIssue;
+    }
+
+    public void setAuthorizationIssue(String authorizationIssue) {
+        this.authorizationIssue = authorizationIssue;
+    }
+
+    public Date getLastRefreshTime() {
+        return lastRefreshTime;
+    }
+
+    public void setLastRefreshTime(Date lastRefreshTime) {
+        this.lastRefreshTime = lastRefreshTime;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public Integer getActiveThreadCount() {
+        return activeThreadCount;
+    }
+
+    public void setActiveThreadCount(Integer activeThreadCount) {
+        this.activeThreadCount = activeThreadCount;
+    }
+
+    public Integer getSentCount() {
+        return sentCount;
+    }
+
+    public void setSentCount(Integer sentCount) {
+        this.sentCount = sentCount;
+    }
+
+    public Long getSentContentSize() {
+        return sentContentSize;
+    }
+
+    public void setSentContentSize(Long sentContentSize) {
+        this.sentContentSize = sentContentSize;
+    }
+
+    public Integer getReceivedCount() {
+        return receivedCount;
+    }
+
+    public void setReceivedCount(Integer receivedCount) {
+        this.receivedCount = receivedCount;
+    }
+
+    public Long getReceivedContentSize() {
+        return receivedContentSize;
+    }
+
+    public void setReceivedContentSize(Long receivedContentSize) {
+        this.receivedContentSize = receivedContentSize;
+    }
+
+    public Integer getActiveRemotePortCount() {
+        return activeRemotePortCount;
+    }
+
+    public void setActiveRemotePortCount(Integer activeRemotePortCount) {
+        this.activeRemotePortCount = activeRemotePortCount;
+    }
+
+    public Integer getInactiveRemotePortCount() {
+        return inactiveRemotePortCount;
+    }
+
+    public void setInactiveRemotePortCount(Integer inactiveRemotePortCount) {
+        this.inactiveRemotePortCount = inactiveRemotePortCount;
+    }
+
+    public long getAverageLineageDuration() {
+        return averageLineageDuration;
+    }
+
+    public void setAverageLineageDuration(final long millis) {
+        this.averageLineageDuration = millis;
+    }
+
+    public long getAverageLineageDuration(final TimeUnit timeUnit) {
+        return TimeUnit.MILLISECONDS.convert(averageLineageDuration, timeUnit);
+    }
+
+    public void setAverageLineageDuration(final long duration, final TimeUnit timeUnit) {
+        this.averageLineageDuration = timeUnit.toMillis(duration);
+    }
+
+    @Override
+    public RemoteProcessGroupStatus clone() {
+        final RemoteProcessGroupStatus clonedObj = new RemoteProcessGroupStatus();
+        clonedObj.id = id;
+        clonedObj.groupId = groupId;
+        clonedObj.name = name;
+        clonedObj.comments = comments;
+        clonedObj.authorizationIssue = authorizationIssue;
+        clonedObj.lastRefreshTime = lastRefreshTime;
+        clonedObj.uri = uri;
+        clonedObj.activeThreadCount = activeThreadCount;
+        clonedObj.transmissionStatus = transmissionStatus;
+        clonedObj.sentCount = sentCount;
+        clonedObj.sentContentSize = sentContentSize;
+        clonedObj.receivedCount = receivedCount;
+        clonedObj.receivedContentSize = receivedContentSize;
+        clonedObj.activeRemotePortCount = activeRemotePortCount;
+        clonedObj.inactiveRemotePortCount = inactiveRemotePortCount;
+        clonedObj.averageLineageDuration = averageLineageDuration;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("RemoteProcessGroupStatus [id=");
+        builder.append(id);
+        builder.append(", groupId=");
+        builder.append(groupId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", comments=");
+        builder.append(comments);
+        builder.append(", authorizationIssue=");
+        builder.append(authorizationIssue);
+        builder.append(", lastRefreshTime=");
+        builder.append(lastRefreshTime);
+        builder.append(", uri=");
+        builder.append(uri);
+        builder.append(", activeThreadCount=");
+        builder.append(activeThreadCount);
+        builder.append(", transmissionStatus=");
+        builder.append(transmissionStatus);
+        builder.append(", sentCount=");
+        builder.append(sentCount);
+        builder.append(", sentContentSize=");
+        builder.append(sentContentSize);
+        builder.append(", receivedCount=");
+        builder.append(receivedCount);
+        builder.append(", receivedContentSize=");
+        builder.append(receivedContentSize);
+        builder.append(", activeRemotePortCount=");
+        builder.append(activeRemotePortCount);
+        builder.append(", inactiveRemotePortCount=");
+        builder.append(inactiveRemotePortCount);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/RunStatus.java b/src/main/java/org/apache/nifi/controller/status/RunStatus.java
new file mode 100644
index 0000000..aad749f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/RunStatus.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public enum RunStatus {
+
+    Running,
+    Stopped,
+    Validating,
+    Invalid,
+    Disabled;
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/StorageStatus.java b/src/main/java/org/apache/nifi/controller/status/StorageStatus.java
new file mode 100644
index 0000000..2b75789
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/StorageStatus.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.controller.status;
+
+/**
+ * The status of a storage repository.
+ */
+public class StorageStatus implements Cloneable {
+    private String name;
+    private long freeSpace;
+    private long usedSpace;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public long getFreeSpace() {
+        return freeSpace;
+    }
+
+    public void setFreeSpace(final long freeSpace) {
+        this.freeSpace = freeSpace;
+    }
+
+    public long getUsedSpace() {
+        return usedSpace;
+    }
+
+    public void setUsedSpace(final long usedSpace) {
+        this.usedSpace = usedSpace;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder("StorageStatus{");
+        builder.append("name='").append(name).append('\'');
+        builder.append(", freeSpace=").append(freeSpace);
+        builder.append(", usedSpace=").append(usedSpace);
+        builder.append('}');
+        return builder.toString();
+    }
+
+    @Override
+    public StorageStatus clone() {
+        final StorageStatus clonedObj = new StorageStatus();
+        clonedObj.name = name;
+        clonedObj.freeSpace = freeSpace;
+        clonedObj.usedSpace = usedSpace;
+        return clonedObj;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/TransmissionStatus.java b/src/main/java/org/apache/nifi/controller/status/TransmissionStatus.java
new file mode 100644
index 0000000..6d7eb12
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/TransmissionStatus.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public enum TransmissionStatus {
+
+    Transmitting,
+    NotTransmitting;
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/analytics/ConnectionStatusPredictions.java b/src/main/java/org/apache/nifi/controller/status/analytics/ConnectionStatusPredictions.java
new file mode 100644
index 0000000..73cfa99
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/analytics/ConnectionStatusPredictions.java
@@ -0,0 +1,115 @@
+/*
+ * 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.analytics;
+
+public class ConnectionStatusPredictions implements Cloneable {
+    private long predictionIntervalMillis;
+    private int nextPredictedQueuedCount;
+    private long nextPredictedQueuedBytes;
+    private long predictedTimeToCountBackpressureMillis;
+    private long predictedTimeToBytesBackpressureMillis;
+    private int predictedPercentCount = 0;
+    private int predictedPercentBytes = 0;
+
+    public long getPredictionIntervalMillis() {
+        return predictionIntervalMillis;
+    }
+
+    public void setPredictionIntervalMillis(long predictionIntervalMillis) {
+        this.predictionIntervalMillis = predictionIntervalMillis;
+    }
+
+    public int getNextPredictedQueuedCount() {
+        return nextPredictedQueuedCount;
+    }
+
+    public void setNextPredictedQueuedCount(int nextPredictedQueuedCount) {
+        this.nextPredictedQueuedCount = nextPredictedQueuedCount;
+    }
+
+    public long getNextPredictedQueuedBytes() {
+        return nextPredictedQueuedBytes;
+    }
+
+    public void setNextPredictedQueuedBytes(long nextPredictedQueuedBytes) {
+        this.nextPredictedQueuedBytes = nextPredictedQueuedBytes;
+    }
+
+    public long getPredictedTimeToCountBackpressureMillis() {
+        return predictedTimeToCountBackpressureMillis;
+    }
+
+    public void setPredictedTimeToCountBackpressureMillis(long predictedTimeToCountBackpressureMillis) {
+        this.predictedTimeToCountBackpressureMillis = predictedTimeToCountBackpressureMillis;
+    }
+
+    public long getPredictedTimeToBytesBackpressureMillis() {
+        return predictedTimeToBytesBackpressureMillis;
+    }
+
+    public void setPredictedTimeToBytesBackpressureMillis(long predictedTimeToBytesBackpressureMillis) {
+        this.predictedTimeToBytesBackpressureMillis = predictedTimeToBytesBackpressureMillis;
+    }
+
+    public int getPredictedPercentCount() {
+        return predictedPercentCount;
+    }
+
+    public void setPredictedPercentCount(int predictedPercentCount) {
+        this.predictedPercentCount = predictedPercentCount;
+    }
+
+    public int getPredictedPercentBytes() {
+        return predictedPercentBytes;
+    }
+
+    public void setPredictedPercentBytes(int predictedPercentBytes) {
+        this.predictedPercentBytes = predictedPercentBytes;
+    }
+
+    @Override
+    public ConnectionStatusPredictions clone() {
+        final ConnectionStatusPredictions clonedObj = new ConnectionStatusPredictions();
+        clonedObj.nextPredictedQueuedBytes = nextPredictedQueuedBytes;
+        clonedObj.nextPredictedQueuedCount = nextPredictedQueuedCount;
+        clonedObj.predictedTimeToBytesBackpressureMillis = predictedTimeToBytesBackpressureMillis;
+        clonedObj.predictedTimeToCountBackpressureMillis = predictedTimeToCountBackpressureMillis;
+        clonedObj.predictedPercentCount = predictedPercentCount;
+        clonedObj.predictedPercentBytes = predictedPercentBytes;
+        return clonedObj;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("ConnectionStatusPredictions [id=");
+        builder.append(", nextPredictedQueuedBytes=");
+        builder.append(nextPredictedQueuedBytes);
+        builder.append(", nextPredictedQueuedCount=");
+        builder.append(nextPredictedQueuedCount);
+        builder.append(", predictedTimeToBytesBackpressureMillis=");
+        builder.append(predictedTimeToBytesBackpressureMillis);
+        builder.append(", predictedTimeToCountBackpressureMillis=");
+        builder.append(predictedTimeToCountBackpressureMillis);
+        builder.append(", predictedPercentCount=");
+        builder.append(predictedPercentCount);
+        builder.append(", predictedPercentBytes=");
+        builder.append(predictedPercentBytes);
+        builder.append("]");
+        return builder.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/analytics/QueryWindow.java b/src/main/java/org/apache/nifi/controller/status/analytics/QueryWindow.java
new file mode 100644
index 0000000..798e6cf
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/analytics/QueryWindow.java
@@ -0,0 +1,62 @@
+/*
+ * 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.analytics;
+
+import java.util.Date;
+
+/**
+ * QueryWindow is used to track time intervals for querying for observations
+ */
+public class QueryWindow {
+
+    private long startTimeMillis;
+    private long endTimeMillis;
+
+    public QueryWindow(long startTimeMillis, long endTimeMillis) {
+        this.startTimeMillis = startTimeMillis;
+        this.endTimeMillis = endTimeMillis;
+    }
+
+    public long getStartTimeMillis() {
+        return startTimeMillis;
+    }
+
+    public void setStartTimeMillis(long startTimeMillis) {
+        this.startTimeMillis = startTimeMillis;
+    }
+
+    public long getEndTimeMillis() {
+        return endTimeMillis;
+    }
+
+    public void setEndTimeMillis(long endTimeMillis) {
+        this.endTimeMillis = endTimeMillis;
+    }
+
+    public Date getStartDateTime() {
+        return new Date(startTimeMillis);
+    }
+
+    public Date getEndDateTime() {
+        return new Date(endTimeMillis);
+    }
+
+    public long getTimeDifferenceMillis() {
+        return endTimeMillis - startTimeMillis;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalytics.java b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalytics.java
new file mode 100644
index 0000000..63e26e9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalytics.java
@@ -0,0 +1,46 @@
+/*
+ * 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.analytics;
+
+import java.util.Map;
+
+/**
+ * The StatusAnalytics interface offers methods for accessing predicted and other values for a single component (Connection instance, e.g.).
+ */
+public interface StatusAnalytics {
+
+    /**
+     * Get the Query Window used by the analytics instance
+     * @return queryWindow
+     */
+    QueryWindow getQueryWindow();
+
+    /**
+     * Get available predictions where the key (String) in the map is the name of the prediction and value (Long)
+     * is the value for the prediction
+     * @return map
+     */
+    Map<String, Long> getPredictions();
+
+    /**
+     * Return if analytics object supports online learning
+     * @return boolean
+     */
+    boolean supportsOnlineLearning();
+
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsEngine.java b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsEngine.java
new file mode 100644
index 0000000..57c13e6
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsEngine.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.controller.status.analytics;
+
+public interface StatusAnalyticsEngine {
+
+    /**
+     * Retrieve status analytics object for given component
+     * @param componentId identifier for component
+     * @return componenet specific status analytics object
+     */
+    StatusAnalytics getStatusAnalytics(String componentId);
+
+}
diff --git a/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsModel.java b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsModel.java
new file mode 100644
index 0000000..96ea529
--- /dev/null
+++ b/src/main/java/org/apache/nifi/controller/status/analytics/StatusAnalyticsModel.java
@@ -0,0 +1,64 @@
+/*
+ * 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.analytics;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+public interface StatusAnalyticsModel {
+
+    /**
+     * Train model with provided observations (features, labels/targets)
+     * @param features Stream of feature observation values
+     * @param labels target observation values
+     */
+    void learn(Stream<Double[]> features, Stream<Double> labels);
+
+    /**
+     * Return a prediction given observation values
+     * @param feature feature observation values values
+     * @return prediction of target/label
+     */
+    Double predict(Double[] feature);
+
+    /**
+     * Predict a feature given a known target and known predictor values (if multiple predictors are included with model)
+     * @param predictVariableIndex index of feature that we would like to predict (index should align with order provided in model learn method)
+     * @param knownVariablesWithIndex a map of known predictor values with their indexes if available
+     * @param label known target value
+     * @return prediction for variable
+     */
+    Double predictVariable(Integer predictVariableIndex, Map<Integer, Double> knownVariablesWithIndex, Double label);
+
+    /**
+     * Indicate if model supports online learning (e.g. can learn new observation samples to create a model)
+     * @return boolean indicating online learning support
+     */
+    Boolean supportsOnlineLearning();
+
+    /**
+     * Returns a map of scores relevant to model (e.g. rSquared, Confidence Intervals, etc.)
+     * @return Map of score names with values
+     */
+    Map<String, Double> getScores();
+
+    /**
+     * Resets a model by clearing observations and other calculations
+     */
+    void clear();
+
+}
diff --git a/src/main/java/org/apache/nifi/diagnostics/StorageUsage.java b/src/main/java/org/apache/nifi/diagnostics/StorageUsage.java
new file mode 100644
index 0000000..9854765
--- /dev/null
+++ b/src/main/java/org/apache/nifi/diagnostics/StorageUsage.java
@@ -0,0 +1,71 @@
+/*
+ * 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.diagnostics;
+
+/**
+ * Disk usage for a file system directory.
+ *
+ */
+public class StorageUsage implements Cloneable {
+
+    private String identifier;
+
+    private long freeSpace;
+    private long totalSpace;
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(final String identifier) {
+        this.identifier = identifier;
+    }
+
+    public void setFreeSpace(final long freeSpace) {
+        this.freeSpace = freeSpace;
+    }
+
+    public long getFreeSpace() {
+        return freeSpace;
+    }
+
+    public long getTotalSpace() {
+        return totalSpace;
+    }
+
+    public void setTotalSpace(final long totalSpace) {
+        this.totalSpace = totalSpace;
+    }
+
+    public long getUsedSpace() {
+        return totalSpace - freeSpace;
+    }
+
+    public int getDiskUtilization() {
+        return Math.round((float) getUsedSpace() / totalSpace * 100);
+    }
+
+    @Override
+    public StorageUsage clone() {
+        final StorageUsage clonedObj = new StorageUsage();
+        clonedObj.identifier = identifier;
+        clonedObj.freeSpace = freeSpace;
+        clonedObj.totalSpace = totalSpace;
+        return clonedObj;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java
new file mode 100644
index 0000000..727b097
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/AbstractDocumentationWriter.java
@@ -0,0 +1,366 @@
+/*
+ * 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.PrimaryNodeOnly;
+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.SideEffectFree;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.behavior.SupportsSensitiveDynamicProperties;
+import org.apache.nifi.annotation.behavior.SystemResourceConsideration;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.behavior.TriggerWhenAnyDestinationAvailable;
+import org.apache.nifi.annotation.behavior.TriggerWhenEmpty;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.configuration.DefaultSchedule;
+import org.apache.nifi.annotation.configuration.DefaultSettings;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.annotation.documentation.MultiProcessorUseCase;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.documentation.UseCase;
+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.DocumentationFlowAnalysisRuleInitializationContext;
+import org.apache.nifi.documentation.init.DocumentationParameterProviderInitializationContext;
+import org.apache.nifi.documentation.init.DocumentationProcessorInitializationContext;
+import org.apache.nifi.documentation.init.DocumentationReportingInitializationContext;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
+import org.apache.nifi.parameter.ParameterProvider;
+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.Map;
+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
+ * <b>NOTE WELL:</b> 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 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);
+            } else if (component instanceof FlowAnalysisRule) {
+                initialize((FlowAnalysisRule) component);
+            } else if (component instanceof ParameterProvider) {
+                initialize((ParameterProvider) 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 initialize(final FlowAnalysisRule flowAnalysisRule) throws InitializationException {
+        flowAnalysisRule.initialize(new DocumentationFlowAnalysisRuleInitializationContext());
+    }
+
+    protected void initialize(final ParameterProvider parameterProvider) throws InitializationException {
+        parameterProvider.initialize(new DocumentationParameterProviderInitializationContext());
+    }
+
+    @Override
+    public final void write(final ConfigurableComponent component) throws IOException {
+        write(component, Collections.emptyList(), Collections.emptyMap());
+    }
+
+    @Override
+    public final void write(final ConfigurableComponent component, final Collection<ServiceAPI> providedServices, Map<String, ServiceAPI> propertyServices) throws IOException {
+        writeHeader(component);
+        writeBody(component, propertyServices);
+
+        if (providedServices != null && component instanceof ControllerService) {
+            writeProvidedServices(providedServices);
+        }
+
+        writeFooter(component);
+    }
+
+    protected void writeBody(final ConfigurableComponent component, Map<String, ServiceAPI> propertyServices) throws IOException {
+        writeExtensionName(component.getClass().getName());
+        writeExtensionType(getExtensionType(component));
+        writeDeprecationNotice(component.getClass().getAnnotation(DeprecationNotice.class));
+        writeDescription(getDescription(component));
+        writeTags(getTags(component));
+        writeProperties(component.getPropertyDescriptors(), propertyServices);
+        writeDynamicProperties(getDynamicProperties(component));
+        writeSupportsSensitiveDynamicProperties(component.getClass().getAnnotation(SupportsSensitiveDynamicProperties.class));
+
+        if (component instanceof Processor) {
+            final Processor processor = (Processor) component;
+
+            writeRelationships(processor.getRelationships());
+            writeDynamicRelationship(getDynamicRelationship(processor));
+            writeReadsAttributes(getReadsAttributes(processor));
+            writeWritesAttributes(getWritesAttributes(processor));
+
+            writeTriggerSerially(processor.getClass().getAnnotation(TriggerSerially.class));
+            writeTriggerWhenEmpty(processor.getClass().getAnnotation(TriggerWhenEmpty.class));
+            writeTriggerWhenAnyDestinationAvailable(processor.getClass().getAnnotation(TriggerWhenAnyDestinationAvailable.class));
+            writeSupportsBatching(processor.getClass().getAnnotation(SupportsBatching.class));
+            writePrimaryNodeOnly(processor.getClass().getAnnotation(PrimaryNodeOnly.class));
+            writeSideEffectFree(processor.getClass().getAnnotation(SideEffectFree.class));
+            writeDefaultSettings(processor.getClass().getAnnotation(DefaultSettings.class));
+        }
+
+        writeStatefulInfo(component.getClass().getAnnotation(Stateful.class));
+        writeRestrictedInfo(component.getClass().getAnnotation(Restricted.class));
+        writeInputRequirementInfo(getInputRequirement(component));
+        writeSystemResourceConsiderationInfo(getSystemResourceConsiderations(component));
+        writeUseCases(getUseCases(component));
+        writeMultiProcessorUseCases(getMultiProcessorUseCases(component));
+        writeSeeAlso(component.getClass().getAnnotation(SeeAlso.class));
+        writeDefaultSchedule(component.getClass().getAnnotation(DefaultSchedule.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<String> 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<DynamicProperty> getDynamicProperties(ConfigurableComponent configurableComponent) {
+        final List<DynamicProperty> 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<ReadsAttribute> getReadsAttributes(final Processor processor) {
+        final List<ReadsAttribute> 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<WritesAttribute> getWritesAttributes(Processor processor) {
+        List<WritesAttribute> 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<SystemResourceConsideration> getSystemResourceConsiderations(final ConfigurableComponent component) {
+        SystemResourceConsideration[] systemResourceConsiderations = component.getClass().getAnnotationsByType(SystemResourceConsideration.class);
+        if (systemResourceConsiderations.length == 0) {
+            return Collections.emptyList();
+        }
+
+        return Arrays.asList(systemResourceConsiderations);
+    }
+
+    private List<UseCase> getUseCases(final ConfigurableComponent component) {
+        UseCase[] useCases = component.getClass().getAnnotationsByType(UseCase.class);
+        if (useCases.length == 0) {
+            return Collections.emptyList();
+        }
+
+        return Arrays.asList(useCases);
+    }
+
+    private List<MultiProcessorUseCase> getMultiProcessorUseCases(final ConfigurableComponent component) {
+        MultiProcessorUseCase[] useCases = component.getClass().getAnnotationsByType(MultiProcessorUseCase.class);
+        if (useCases.length == 0) {
+            return Collections.emptyList();
+        }
+
+        return Arrays.asList(useCases);
+    }
+
+    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;
+        }
+        if (component instanceof FlowAnalysisRule) {
+            return ExtensionType.FLOW_ANALYSIS_RULE;
+        }
+        if (component instanceof ParameterProvider) {
+            return ExtensionType.PARAMETER_PROVIDER;
+        }
+        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<String> tags) throws IOException;
+
+    protected abstract void writeProperties(List<PropertyDescriptor> properties, Map<String, ServiceAPI> propertyServices) throws IOException;
+
+    protected abstract void writeDynamicProperties(List<DynamicProperty> 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<SystemResourceConsideration> considerations) throws IOException;
+
+    protected abstract void writeSeeAlso(SeeAlso seeAlso) throws IOException;
+
+    protected abstract void writeUseCases(List<UseCase> useCases) throws IOException;
+
+    protected abstract void writeMultiProcessorUseCases(List<MultiProcessorUseCase> useCases) throws IOException;
+
+    protected abstract void writeDefaultSchedule(DefaultSchedule defaultSchedule) throws IOException;
+
+    // Processor-specific methods
+    protected abstract void writeRelationships(Set<Relationship> relationships) throws IOException;
+
+    protected abstract void writeDynamicRelationship(DynamicRelationship dynamicRelationship) throws IOException;
+
+    protected abstract void writeReadsAttributes(List<ReadsAttribute> attributes) throws IOException;
+
+    protected abstract void writeWritesAttributes(List<WritesAttribute> attributes) throws IOException;
+
+    protected abstract void writeTriggerSerially(TriggerSerially triggerSerially) throws IOException;
+
+    protected abstract void writeTriggerWhenEmpty(TriggerWhenEmpty triggerWhenEmpty) throws IOException;
+
+    protected abstract void writeTriggerWhenAnyDestinationAvailable(TriggerWhenAnyDestinationAvailable triggerWhenAnyDestinationAvailable) throws IOException;
+
+    protected abstract void writeSupportsBatching(SupportsBatching supportsBatching) throws IOException;
+
+    protected abstract void writeSupportsSensitiveDynamicProperties(SupportsSensitiveDynamicProperties supportsSensitiveDynamicProperties) throws IOException;
+
+    protected abstract void writePrimaryNodeOnly(PrimaryNodeOnly primaryNodeOnly) throws IOException;
+
+    protected abstract void writeSideEffectFree(SideEffectFree sideEffectFree) throws IOException;
+
+    protected abstract void writeDefaultSettings(DefaultSettings defaultSettings) throws IOException;
+
+    // ControllerService-specific methods
+    protected abstract void writeProvidedServices(Collection<ServiceAPI> providedServices) throws IOException;
+
+    protected abstract void writeFooter(ConfigurableComponent component) throws IOException;
+
+}
diff --git a/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.java
new file mode 100644
index 0000000..3fa9266
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/ExtensionDocumentationWriter.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.documentation;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import org.apache.nifi.components.ConfigurableComponent;
+
+/**
+ * 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
+ * <b>NOTE WELL:</b> 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 {
+
+    /**
+     * Calls initialize on the component. Must be called before calling any write methods.
+     *
+     * @param component the component to initialize
+     */
+    void initialize(final ConfigurableComponent component);
+
+    /**
+     * Write the documentation for the given component.
+     *
+     * @param component the component to document
+     * @throws IOException if an error occurs writing the documentation
+     */
+    void write(ConfigurableComponent component) throws IOException;
+
+    /**
+     * Writes the documentation for the given component.
+     *
+     * @param component the component to document
+     * @param provideServices the service APIs implemented by the component
+     * @param propertyServiceAPIs the service APIs required by the property descriptors of the component
+     * @throws IOException if an error occurs writing the documentation
+     */
+    void write(ConfigurableComponent component, Collection<ServiceAPI> provideServices, Map<String, ServiceAPI> propertyServiceAPIs) throws IOException;
+
+}
diff --git a/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/src/main/java/org/apache/nifi/documentation/ExtensionType.java
new file mode 100644
index 0000000..00d7058
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/ExtensionType.java
@@ -0,0 +1,29 @@
+/*
+ * 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,
+
+    FLOW_ANALYSIS_RULE,
+
+    PARAMETER_PROVIDER;
+}
diff --git a/src/main/java/org/apache/nifi/documentation/ServiceAPI.java b/src/main/java/org/apache/nifi/documentation/ServiceAPI.java
new file mode 100644
index 0000000..6a4a63b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/ServiceAPI.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
+ * <b>NOTE WELL:</b> 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 ServiceAPI {
+    /**
+     * @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/src/main/java/org/apache/nifi/documentation/StandardServiceAPI.java b/src/main/java/org/apache/nifi/documentation/StandardServiceAPI.java
new file mode 100644
index 0000000..585f8e4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/StandardServiceAPI.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 StandardServiceAPI implements ServiceAPI {
+    private final String className;
+    private final String groupId;
+    private final String artifactId;
+    private final String version;
+
+    public StandardServiceAPI(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/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java
new file mode 100644
index 0000000..cb8a0f2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationControllerServiceInitializationContext.java
@@ -0,0 +1,71 @@
+/*
+ * 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 java.io.File;
+import java.util.UUID;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.controller.ControllerServiceInitializationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+
+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 NodeTypeProvider getNodeTypeProvider() {
+        return null;
+    }
+
+    @Override
+    public String getKerberosServicePrincipal() {
+        return null;
+    }
+
+    @Override
+    public File getKerberosServiceKeytab() {
+        return null;
+    }
+
+    @Override
+    public File getKerberosConfigurationFile() {
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java
new file mode 100644
index 0000000..9582243
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationFlowAnalysisRuleInitializationContext.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+import org.apache.nifi.flowanalysis.FlowAnalysisRuleInitializationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.io.File;
+import java.util.UUID;
+
+public class DocumentationFlowAnalysisRuleInitializationContext implements FlowAnalysisRuleInitializationContext {
+    private final String id = UUID.randomUUID().toString();
+    private final ComponentLog componentLog = new NopComponentLog();
+    private final NodeTypeProvider nodeTypeProvider = new StandaloneNodeTypeProvider();
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @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/src/main/java/org/apache/nifi/documentation/init/DocumentationParameterProviderInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationParameterProviderInitializationContext.java
new file mode 100644
index 0000000..03583fc
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationParameterProviderInitializationContext.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.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.parameter.ParameterProviderInitializationContext;
+
+import java.io.File;
+import java.util.UUID;
+
+public class DocumentationParameterProviderInitializationContext implements ParameterProviderInitializationContext {
+    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 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/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java
new file mode 100644
index 0000000..a48dcb6
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationProcessorInitializationContext.java
@@ -0,0 +1,64 @@
+/*
+ * 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 java.io.File;
+import java.util.UUID;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.ProcessorInitializationContext;
+
+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/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java
new file mode 100644
index 0000000..bcf216e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationReportingInitializationContext.java
@@ -0,0 +1,88 @@
+/*
+ * 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 java.io.File;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+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;
+
+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/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java b/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.java
new file mode 100644
index 0000000..5cda2af
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/EmptyControllerServiceLookup.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.documentation.init;
+
+import java.util.Set;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.ControllerServiceLookup;
+
+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<String> getControllerServiceIdentifiers(final Class<? extends ControllerService> serviceType) throws IllegalArgumentException {
+        return null;
+    }
+
+    @Override
+    public String getControllerServiceName(final String serviceIdentifier) {
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java b/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java
new file mode 100644
index 0000000..dd30eb4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/NopComponentLog.java
@@ -0,0 +1,174 @@
+/*
+ * 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;
+import org.apache.nifi.logging.LogMessage;
+
+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) {
+
+    }
+
+    @Override
+    public void warn(LogMessage logMessage) {
+
+    }
+
+    @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(LogMessage logMessage) {
+
+    }
+
+    @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(LogMessage message) {
+
+    }
+
+    @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(LogMessage message) {
+
+    }
+
+    @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) {
+
+    }
+
+    @Override
+    public void debug(LogMessage message) {
+
+    }
+
+    @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(LogMessage message) {
+
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java b/src/main/java/org/apache/nifi/documentation/init/NopStateManager.java
new file mode 100644
index 0000000..4b4e21e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/init/NopStateManager.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.init;
+
+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;
+
+public class NopStateManager implements StateManager {
+    @Override
+    public void setState(final Map<String, String> state, final Scope scope) {
+    }
+
+    @Override
+    public StateMap getState(final Scope scope) {
+        return null;
+    }
+
+    @Override
+    public boolean replace(final StateMap oldValue, final Map<String, String> newValue, final Scope scope) {
+        return false;
+    }
+
+    @Override
+    public void clear(final Scope scope) {
+    }
+}
diff --git a/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java b/src/main/java/org/apache/nifi/documentation/init/StandaloneNodeTypeProvider.java
new file mode 100644
index 0000000..a0c5be6
--- /dev/null
+++ b/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/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
new file mode 100644
index 0000000..e0b7a54
--- /dev/null
+++ b/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
@@ -0,0 +1,670 @@
+/*
+ * 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 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.Map;
+import java.util.Set;
+import java.util.function.Function;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+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.PrimaryNodeOnly;
+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.SideEffectFree;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.behavior.SupportsSensitiveDynamicProperties;
+import org.apache.nifi.annotation.behavior.SystemResourceConsideration;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.behavior.TriggerWhenAnyDestinationAvailable;
+import org.apache.nifi.annotation.behavior.TriggerWhenEmpty;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.configuration.DefaultSchedule;
+import org.apache.nifi.annotation.configuration.DefaultSettings;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.annotation.documentation.MultiProcessorUseCase;
+import org.apache.nifi.annotation.documentation.ProcessorConfiguration;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.UseCase;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.PropertyDependency;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.RequiredPermission;
+import org.apache.nifi.components.resource.ResourceDefinition;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.documentation.AbstractDocumentationWriter;
+import org.apache.nifi.documentation.ExtensionType;
+import org.apache.nifi.documentation.ServiceAPI;
+import org.apache.nifi.processor.Relationship;
+
+/**
+ * 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
+ * <b>NOTE WELL:</b> 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) {
+            return;
+        }
+
+        final Class[] classes = deprecationNotice.alternatives();
+        final String[] classNames = deprecationNotice.classNames();
+
+        final Set<String> 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<String> alternatives) throws IOException {
+        writeStartElement("deprecationNotice");
+
+        writeTextElement("reason", reason);
+        writeTextArray("alternatives", "alternative", alternatives);
+
+        writeEndElement();
+    }
+
+
+    @Override
+    protected void writeDescription(final String description) throws IOException {
+        if (description == null) {
+            return;
+        }
+        writeTextElement("description", description);
+    }
+
+    @Override
+    protected void writeTags(final List<String> tags) throws IOException {
+        if (tags == null) {
+            return;
+        }
+        writeTextArray("tags", "tag", tags);
+    }
+
+    @Override
+    protected void writeProperties(final List<PropertyDescriptor> properties, Map<String, ServiceAPI> propertyServices) throws IOException {
+        if (properties == null || properties.isEmpty()) {
+            return;
+        }
+
+        writeStartElement("properties");
+        for (final PropertyDescriptor property : properties) {
+            writeProperty(property, propertyServices);
+        }
+        writeEndElement();
+    }
+
+    private void writeProperty(final PropertyDescriptor property, Map<String, ServiceAPI> propertyServices) throws IOException {
+        writeStartElement("property");
+
+        writeTextElement("name", property.getName());
+        writeTextElement("displayName", property.getDisplayName());
+        writeTextElement("description", property.getDescription());
+        if (property.getDefaultValue() != null) {
+            writeTextElement("defaultValue", property.getDefaultValue());
+        }
+
+        if (property.getControllerServiceDefinition() != null) {
+            writeStartElement("controllerServiceDefinition");
+
+            final ServiceAPI serviceAPI = propertyServices.get(property.getName());
+            if (serviceAPI != null) {
+                writeTextElement("className", serviceAPI.getClassName());
+                writeTextElement("groupId", serviceAPI.getGroupId());
+                writeTextElement("artifactId", serviceAPI.getArtifactId());
+                writeTextElement("version", serviceAPI.getVersion());
+            } else {
+                writeTextElement("className", property.getControllerServiceDefinition().getName());
+                writeTextElement("groupId", "unknown");
+                writeTextElement("artifactId", "unknown");
+                writeTextElement("version", "unknown");
+            }
+
+            writeEndElement();
+        }
+
+        if (property.getAllowableValues() != null && !property.getAllowableValues().isEmpty()) {
+            writeArray("allowableValues", property.getAllowableValues(), this::writeAllowableValue);
+        }
+
+        writeBooleanElement("required", property.isRequired());
+        writeBooleanElement("sensitive", property.isSensitive());
+        writeBooleanElement("expressionLanguageSupported", property.isExpressionLanguageSupported());
+        if (property.getExpressionLanguageScope() != null) {
+            writeTextElement("expressionLanguageScope", property.getExpressionLanguageScope().name());
+        }
+        writeBooleanElement("dynamicallyModifiesClasspath", property.isDynamicClasspathModifier());
+        writeBooleanElement("dynamic", property.isDynamic());
+        writeResourceDefinition(property.getResourceDefinition());
+        writeDependencies(property);
+
+        writeEndElement();
+    }
+
+    private void writeResourceDefinition(final ResourceDefinition resourceDefinition) throws IOException {
+        if (resourceDefinition == null) {
+            return;
+        }
+
+        writeStartElement("resourceDefinition");
+        writeTextElement("cardinality", resourceDefinition.getCardinality().name());
+        writeArray("resourceTypes", resourceDefinition.getResourceTypes(), this::writeResourceType);
+        writeEndElement();
+    }
+
+    private void writeResourceType(final ResourceType resourceType) throws IOException {
+        writeTextElement("resourceType", resourceType.name());
+    }
+
+    private void writeAllowableValue(final AllowableValue allowableValue) throws IOException {
+        writeStartElement("allowableValue");
+        writeTextElement("displayName", allowableValue.getDisplayName());
+        writeTextElement("value", allowableValue.getValue());
+        writeTextElement("description", allowableValue.getDescription());
+        writeEndElement();
+    }
+
+    private void writeDependencies(final PropertyDescriptor propertyDescriptor) throws IOException {
+        final Set<PropertyDependency> dependencies = propertyDescriptor.getDependencies();
+        if (dependencies == null || dependencies.isEmpty()) {
+            return;
+        }
+
+        writeStartElement("dependencies");
+
+        for (final PropertyDependency dependency : dependencies) {
+            writeStartElement("dependency");
+            writeTextElement("propertyName", dependency.getPropertyName());
+            writeTextElement("propertyDisplayName", dependency.getPropertyDisplayName());
+
+            final Set<String> dependentValues = dependency.getDependentValues();
+            if (dependentValues != null) {
+                writeStartElement("dependentValues");
+                for (final String dependentValue : dependentValues) {
+                    writeTextElement("dependentValue", dependentValue);
+                }
+                writeEndElement();
+            }
+
+            writeEndElement();
+        }
+
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeDynamicProperties(final List<DynamicProperty> dynamicProperties) throws IOException {
+        if (dynamicProperties == null || dynamicProperties.isEmpty()) {
+            return;
+        }
+        writeArray("dynamicProperties", 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());
+        writeTextElement("expressionLanguageScope", property.expressionLanguageScope() == null ? null : property.expressionLanguageScope().name());
+
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeStatefulInfo(final Stateful stateful) throws IOException {
+        if (stateful == null) {
+            return;
+        }
+
+        writeStartElement("stateful");
+        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 {
+        if (restricted == null) {
+            return;
+        }
+
+        writeStartElement("restricted");
+
+        if (restricted.value() != null && !restricted.value().isEmpty()) {
+            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 {
+        if (requirement == null) {
+            return;
+        }
+        writeTextElement("inputRequirement", requirement.name());
+    }
+
+    @Override
+    protected void writeSystemResourceConsiderationInfo(final List<SystemResourceConsideration> considerations) throws IOException {
+        if (considerations == null || considerations.isEmpty()) {
+            return;
+        }
+        writeArray("systemResourceConsiderations", considerations, this::writeSystemResourceConsideration);
+    }
+
+    private void writeSystemResourceConsideration(final SystemResourceConsideration consideration) throws IOException {
+        writeStartElement("systemResourceConsideration");
+
+        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) {
+            return;
+        }
+
+        final Class[] classes = seeAlso.value();
+        final String[] classNames = seeAlso.classNames();
+
+        final Set<String> 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 writeUseCases(final List<UseCase> useCases) throws IOException {
+        if (useCases.isEmpty()) {
+            return;
+        }
+
+        writeArray("useCases", useCases, this::writeUseCase);
+    }
+
+    private void writeUseCase(final UseCase useCase) throws IOException {
+        writeStartElement("useCase");
+
+        writeTextElement("description", useCase.description());
+        writeTextElement("notes", useCase.notes());
+        writeTextArray("keywords", "keyword", Arrays.asList(useCase.keywords()));
+        writeTextElement("inputRequirement", useCase.inputRequirement().name());
+        writeTextElement("configuration", useCase.configuration());
+
+        writeEndElement();
+    }
+
+    protected void writeMultiProcessorUseCases(final List<MultiProcessorUseCase> multiProcessorUseCases) throws IOException {
+        if (multiProcessorUseCases.isEmpty()) {
+            return;
+        }
+
+        writeArray("multiProcessorUseCases", multiProcessorUseCases, this::writeMultiProcessorUseCase);
+    }
+
+    private void writeMultiProcessorUseCase(final MultiProcessorUseCase useCase) throws IOException {
+        writeStartElement("multiProcessorUseCase");
+
+        writeTextElement("description", useCase.description());
+        writeTextElement("notes", useCase.notes());
+        writeTextArray("keywords", "keyword", Arrays.asList(useCase.keywords()));
+
+        writeArray("processorConfigurations", Arrays.asList(useCase.configurations()), this::writeUseCaseComponent);
+
+        writeEndElement();
+    }
+
+    private void writeUseCaseComponent(final ProcessorConfiguration processorConfig) throws IOException {
+        writeStartElement("processorConfiguration");
+
+        String processorClassName = processorConfig.processorClassName();
+        if (processorClassName.isEmpty()) {
+            processorClassName = processorConfig.processorClass().getName();
+        }
+
+        writeTextElement("processorClassName", processorClassName);
+        writeTextElement("configuration", processorConfig.configuration());
+
+        writeEndElement();
+    }
+
+
+    @Override
+    protected void writeRelationships(final Set<Relationship> relationships) throws IOException {
+        if (relationships == null || relationships.isEmpty()) {
+            return;
+        }
+
+        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 {
+        if (dynamicRelationship == null) {
+            return;
+        }
+
+        writeStartElement("dynamicRelationship");
+        writeTextElement("name", dynamicRelationship.name());
+        writeTextElement("description", dynamicRelationship.description());
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeReadsAttributes(final List<ReadsAttribute> attributes) throws IOException {
+        if (attributes == null || attributes.isEmpty()) {
+            return;
+        }
+        writeArray("readsAttributes", attributes, this::writeReadsAttribute);
+    }
+
+    private void writeReadsAttribute(final ReadsAttribute attribute) throws IOException {
+        writeStartElement("readsAttribute");
+        writeTextElement("name", attribute.attribute());
+        writeTextElement("description", attribute.description());
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeWritesAttributes(final List<WritesAttribute> attributes) throws IOException {
+        if (attributes == null) {
+            return;
+        }
+        writeArray("writesAttributes", attributes, this::writeWritesAttribute);
+    }
+
+    private void writeWritesAttribute(final WritesAttribute attribute) throws IOException {
+        writeStartElement("writesAttribute");
+        writeTextElement("name", attribute.attribute());
+        writeTextElement("description", attribute.description());
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeTriggerSerially(TriggerSerially triggerSerially) throws IOException {
+        if (triggerSerially == null) {
+            return;
+        }
+        writeBooleanElement("triggerSerially", true);
+    }
+
+    @Override
+    protected void writeTriggerWhenEmpty(TriggerWhenEmpty triggerWhenEmpty) throws IOException {
+        if (triggerWhenEmpty == null) {
+            return;
+        }
+        writeBooleanElement("triggerWhenEmpty", true);
+    }
+
+    @Override
+    protected void writeTriggerWhenAnyDestinationAvailable(TriggerWhenAnyDestinationAvailable triggerWhenAnyDestinationAvailable) throws IOException {
+        if (triggerWhenAnyDestinationAvailable == null) {
+            return;
+        }
+        writeBooleanElement("triggerWhenAnyDestinationAvailable", true);
+    }
+
+    @Override
+    protected void writeSupportsBatching(SupportsBatching supportsBatching) throws IOException {
+        if (supportsBatching == null) {
+            return;
+        }
+        writeBooleanElement("supportsBatching", true);
+    }
+
+    @Override
+    protected void writeSupportsSensitiveDynamicProperties(final SupportsSensitiveDynamicProperties supportsSensitiveDynamicProperties) throws IOException {
+        if (supportsSensitiveDynamicProperties == null) {
+            return;
+        }
+        writeBooleanElement("supportsSensitiveDynamicProperties", true);
+    }
+
+    @Override
+    protected void writePrimaryNodeOnly(PrimaryNodeOnly primaryNodeOnly) throws IOException {
+        if (primaryNodeOnly == null) {
+            return;
+        }
+        writeBooleanElement("primaryNodeOnly", true);
+    }
+
+    @Override
+    protected void writeSideEffectFree(SideEffectFree sideEffectFree) throws IOException {
+        if (sideEffectFree == null) {
+            return;
+        }
+        writeBooleanElement("sideEffectFree", true);
+    }
+
+    @Override
+    protected void writeDefaultSchedule(DefaultSchedule defaultSchedule) throws IOException {
+        if (defaultSchedule == null) {
+            return;
+        }
+
+        writeStartElement("defaultSchedule");
+        writeTextElement("strategy", defaultSchedule.strategy().name());
+        writeTextElement("period", defaultSchedule.period());
+        writeTextElement("concurrentTasks", String.valueOf(defaultSchedule.concurrentTasks()));
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeDefaultSettings(DefaultSettings defaultSettings) throws IOException {
+        if (defaultSettings == null) {
+            return;
+        }
+
+        writeStartElement("defaultSettings");
+        writeTextElement("yieldDuration", defaultSettings.yieldDuration());
+        writeTextElement("penaltyDuration", defaultSettings.penaltyDuration());
+        writeTextElement("bulletinLevel", defaultSettings.bulletinLevel().name());
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeFooter(final ConfigurableComponent component) throws IOException {
+        writeEndElement();
+    }
+
+    @Override
+    protected void writeProvidedServices(final Collection<ServiceAPI> providedServices) throws IOException {
+        if (providedServices == null || providedServices.isEmpty()) {
+            return;
+        }
+        writeArray("providedServiceAPIs", providedServices, this::writeProvidedService);
+    }
+
+    private void writeProvidedService(final ServiceAPI service) throws IOException {
+        writeStartElement("providedServiceAPI");
+
+        writeTextElement("className", service.getClassName());
+        writeTextElement("groupId", service.getGroupId());
+        writeTextElement("artifactId", service.getArtifactId());
+        writeTextElement("version", service.getVersion());
+
+        writeEndElement();
+    }
+
+    private <T> void writeArray(final String tagName, final Collection<T> values, final ElementWriter<T> 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<String> values) throws IOException {
+        writeTextArray(outerTagName, elementTagName, values, String::toString);
+    }
+
+    private <T> void writeTextArray(final String outerTagName, final String elementTagName, final Collection<T> values, final Function<T, String> 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 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<T> {
+        void write(T value) throws IOException;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/expression/AttributeExpression.java b/src/main/java/org/apache/nifi/expression/AttributeExpression.java
new file mode 100644
index 0000000..e3b8751
--- /dev/null
+++ b/src/main/java/org/apache/nifi/expression/AttributeExpression.java
@@ -0,0 +1,81 @@
+/*
+ * 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.expression;
+
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.registry.EnvironmentVariables;
+
+/**
+ * Defines a type of expression language statement that can be applied
+ * parametrized by various attributes and properties as specified in each of
+ * the method calls. AttributeExpression evaluations may be also backed by a
+ * {@link EnvironmentVariables} used to substitute attributes and variables found in
+ * the expression for which the registry has a value.
+ */
+public interface AttributeExpression {
+
+    /**
+     * @return Evaluates the expression without any additional attributes.
+     * @throws ProcessException if unable to evaluate
+     */
+    String evaluate() throws ProcessException;
+
+    /**
+     * Evaluates the expression without additional attributes but enables the
+     * expression result to be decorated before returning.
+     *
+     * @param decorator to execute on the resulting expression value
+     * @return evaluated value
+     * @throws ProcessException if failure in evaluation
+     */
+    String evaluate(AttributeValueDecorator decorator) throws ProcessException;
+
+    /**
+     * Evaluates the expression providing access to additional variables
+     * including the flow file properties such as file size, identifier, etc..
+     * and also all of the flow file attributes.
+     *
+     * @param flowFile to evaluate
+     * @return evaluated value
+     * @throws ProcessException if failure evaluating
+     */
+    String evaluate(FlowFile flowFile) throws ProcessException;
+
+    /**
+     * Evaluates the expression providing access to additional variables
+     * including the flow file properties such as file size, identifier, etc..
+     * and also all of the flow file attributes. The resulting value after
+     * executing any variable substitution and expression evaluation is run
+     * through the given decorator and returned.
+     *
+     * @param flowFile to evaluate
+     * @param decorator for evaluation
+     * @return evaluated value
+     * @throws ProcessException if failed to evaluate
+     */
+    String evaluate(FlowFile flowFile, AttributeValueDecorator decorator) throws ProcessException;
+
+    /**
+     * @return the type that is returned by the Expression
+     */
+    ResultType getResultType();
+
+    enum ResultType {
+        STRING, BOOLEAN, WHOLE_NUMBER, DATE, INSTANT, DECIMAL, NUMBER;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/expression/AttributeValueDecorator.java b/src/main/java/org/apache/nifi/expression/AttributeValueDecorator.java
new file mode 100644
index 0000000..b2500dd
--- /dev/null
+++ b/src/main/java/org/apache/nifi/expression/AttributeValueDecorator.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.expression;
+
+@FunctionalInterface
+public interface AttributeValueDecorator {
+
+    /**
+     * Decorates the given value
+     * @param attributeValue to decorate
+     * @return decorated value
+     */
+    String decorate(String attributeValue);
+}
diff --git a/src/main/java/org/apache/nifi/expression/ExpressionLanguageCompiler.java b/src/main/java/org/apache/nifi/expression/ExpressionLanguageCompiler.java
new file mode 100644
index 0000000..9383d27
--- /dev/null
+++ b/src/main/java/org/apache/nifi/expression/ExpressionLanguageCompiler.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.expression;
+
+import org.apache.nifi.expression.AttributeExpression.ResultType;
+
+public interface ExpressionLanguageCompiler {
+
+    /**
+     * Compiles the given Attribute Expression string into an
+     * AttributeExpression that can be evaluated
+     *
+     * @param expression the Attribute Expression to be compiled
+     * @return expression that can be evaluated
+     * @throws IllegalArgumentException if the given expression is not valid
+     */
+    AttributeExpression compile(String expression) throws IllegalArgumentException;
+
+    /**
+     * Indicates whether or not the given string is a valid Attribute
+     * Expression.
+     *
+     * @param expression to validate
+     * @return if is value or not
+     */
+    boolean isValidExpression(String expression);
+
+    /**
+     * Attempts to validate the given expression and returns <code>null</code>
+     * if the expression is syntactically valid or a String indicating why the
+     * expression is invalid otherwise.
+     *
+     * @param expression to validate
+     * @param allowSurroundingCharacters if <code>true</code> allows characters
+     * to surround the Expression, otherwise the expression must be exactly
+     * equal to a valid Expression. E.g., <code>/${path}</code> is valid if and
+     * only if <code>allowSurroundingCharacters</code> is true
+     *
+     * @return a String indicating the reason that the expression is not
+     * syntactically correct, or <code>null</code> if the expression is
+     * syntactically correct
+     */
+    String validateExpression(String expression, boolean allowSurroundingCharacters);
+
+    /**
+     * Returns the ResultType that will be returned by the given Expression
+     *
+     * @param expression the Expression to evaluate
+     * @return result type for the given expression
+     * @throws IllegalArgumentException if the given Expression is not a valid
+     * Expression Language Expression; the message of this Exception will
+     * indicate the problem if the expression is not syntactically valid.
+     */
+    ResultType getResultType(String expression) throws IllegalArgumentException;
+}
diff --git a/src/main/java/org/apache/nifi/expression/ExpressionLanguageScope.java b/src/main/java/org/apache/nifi/expression/ExpressionLanguageScope.java
new file mode 100644
index 0000000..db52405
--- /dev/null
+++ b/src/main/java/org/apache/nifi/expression/ExpressionLanguageScope.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.expression;
+
+/**
+ * Indicates the scope of expression language on a property descriptor.
+ *
+ * Scope of the expression language is hierarchical.
+ *      NONE -> ENVIRONMENT -> FLOWFILE_ATTRIBUTES
+ *
+ * When scope is set to FlowFiles attributes, variables are evaluated
+ * against attributes of each incoming flow file. If no matching attribute
+ * is found, env/syst properties will be checked.
+ *
+ * NONE - expression language is not supported
+ *
+ * ENVIRONMENT - Environment variables defined at JVM level and system properties.
+ *
+ * FLOWFILE_ATTRIBUTES - will check attributes of each individual flow file
+ *
+ */
+public enum ExpressionLanguageScope {
+
+    /**
+     * Expression language is disabled
+     */
+    NONE("Not Supported"),
+
+    /**
+     * Expression language is evaluated against variables in registry
+     */
+    ENVIRONMENT("Environment variables defined at JVM level and system properties"),
+
+    /**
+     * Expression language is evaluated per flow file using attributes
+     */
+    FLOWFILE_ATTRIBUTES("Environment variables and FlowFile Attributes");
+
+    private String description;
+
+    ExpressionLanguageScope(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return this.description;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/BatchSize.java b/src/main/java/org/apache/nifi/flow/BatchSize.java
new file mode 100644
index 0000000..05ba824
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/BatchSize.java
@@ -0,0 +1,76 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.Objects;
+
+
+public class BatchSize {
+    private Integer count;
+    private String size;
+    private String duration;
+
+    @Schema(description = "Preferred number of flow files to include in a transaction.")
+    public Integer getCount() {
+        return count;
+    }
+
+    public void setCount(Integer count) {
+        this.count = count;
+    }
+
+    @Schema(description = "Preferred number of bytes to include in a transaction.")
+    public String getSize() {
+        return size;
+    }
+
+    public void setSize(String size) {
+        this.size = size;
+    }
+
+    @Schema(description = "Preferred amount of time that a transaction should span.")
+    public String getDuration() {
+        return duration;
+    }
+
+    public void setDuration(String duration) {
+        this.duration = duration;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(count, duration, size);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BatchSize other = (BatchSize) obj;
+        return Objects.equals(count, other.count) && Objects.equals(size, other.size) && Objects.equals(duration, other.duration);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/Bundle.java b/src/main/java/org/apache/nifi/flow/Bundle.java
new file mode 100644
index 0000000..ddfd567
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/Bundle.java
@@ -0,0 +1,92 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Objects;
+
+public class Bundle {
+    private String group;
+    private String artifact;
+    private String version;
+
+    public Bundle() {
+    }
+
+    public Bundle(final String group, final String artifact, final String version) {
+        this.group = group;
+        this.artifact = artifact;
+        this.version = version;
+    }
+
+    @Schema(description = "The group of the bundle")
+    public String getGroup() {
+        return group;
+    }
+
+    public void setGroup(String group) {
+        this.group = group;
+    }
+
+    @Schema(description = "The artifact of the bundle")
+    public String getArtifact() {
+        return artifact;
+    }
+
+    public void setArtifact(String artifact) {
+        this.artifact = artifact;
+    }
+
+    @Schema(description = "The version of the bundle")
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final Bundle other = (Bundle) obj;
+        return Objects.equals(group, other.group) && Objects.equals(artifact, other.artifact) && Objects.equals(version, other.version);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(group, artifact, version);
+    }
+
+    @Override
+    public String toString() {
+        return "Bundle[" +
+            "group='" + group + "'" +
+            ", artifact='" + artifact + "'" +
+            ", version='" + version + "'" +
+            "]";
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/ComponentType.java b/src/main/java/org/apache/nifi/flow/ComponentType.java
new file mode 100644
index 0000000..23b2ef3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ComponentType.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.flow;
+
+public enum ComponentType {
+
+    CONNECTION("Connection"),
+    PROCESSOR("Processor"),
+    PROCESS_GROUP("Process Group"),
+    REMOTE_PROCESS_GROUP("Remote Process Group"),
+    INPUT_PORT("Input Port"),
+    OUTPUT_PORT("Output Port"),
+    REMOTE_INPUT_PORT("Remote Input Port"),
+    REMOTE_OUTPUT_PORT("Remote Output Port"),
+    FUNNEL("Funnel"),
+    LABEL("Label"),
+    CONTROLLER_SERVICE("Controller Service"),
+    REPORTING_TASK("Reporting Task"),
+    FLOW_ANALYSIS_RULE("Flow Analysis Rule"),
+    PARAMETER_CONTEXT("Parameter Context"),
+    PARAMETER_PROVIDER("Parameter Provider"),
+    FLOW_REGISTRY_CLIENT("Flow Registry Client");
+
+
+    private final String typeName;
+
+    ComponentType(final String typeName) {
+        this.typeName = typeName;
+    }
+
+    public String getTypeName() {
+        return typeName;
+    }
+
+    @Override
+    public String toString() {
+        return typeName;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/ConnectableComponent.java b/src/main/java/org/apache/nifi/flow/ConnectableComponent.java
new file mode 100644
index 0000000..c4264db
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ConnectableComponent.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.Objects;
+
+
+public class ConnectableComponent {
+    private String id;
+    private String instanceId;
+    private ConnectableComponentType type;
+    private String groupId;
+    private String name;
+    private String comments;
+
+    @Schema(description = "The id of the connectable component.")
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    @Schema(description = "The instance ID of an existing component that is described by this VersionedComponent, or null if this is not mapped to an instantiated component")
+    public String getInstanceIdentifier() {
+        return instanceId;
+    }
+
+    public void setInstanceIdentifier(String instanceIdentifier) {
+        this.instanceId = instanceIdentifier;
+    }
+
+    @Schema(description = "The type of component the connectable is.")
+    public ConnectableComponentType getType() {
+        return type;
+    }
+
+    public void setType(ConnectableComponentType type) {
+        this.type = type;
+    }
+
+    @Schema(description = "The id of the group that the connectable component resides in")
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    @Schema(description = "The name of the connectable component")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Schema(description = "The comments for the connectable component.")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, groupId, name, type, comments);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof ConnectableComponent)) {
+            return false;
+        }
+        final ConnectableComponent other = (ConnectableComponent) obj;
+        return Objects.equals(id, other.id);
+    }
+
+    @Override
+    public String toString() {
+        return "ConnectableComponent{" +
+            "id='" + id + '\'' +
+            ", instanceId='" + instanceId + '\'' +
+            ", type=" + type +
+            ", groupId='" + groupId + '\'' +
+            ", name='" + name + '\'' +
+            '}';
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/ConnectableComponentType.java b/src/main/java/org/apache/nifi/flow/ConnectableComponentType.java
new file mode 100644
index 0000000..2ad41f8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ConnectableComponentType.java
@@ -0,0 +1,27 @@
+/*
+ * 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.flow;
+
+public enum ConnectableComponentType {
+    PROCESSOR,
+    REMOTE_INPUT_PORT,
+    REMOTE_OUTPUT_PORT,
+    INPUT_PORT,
+    OUTPUT_PORT,
+    FUNNEL;
+}
diff --git a/src/main/java/org/apache/nifi/flow/ControllerServiceAPI.java b/src/main/java/org/apache/nifi/flow/ControllerServiceAPI.java
new file mode 100644
index 0000000..ab28fc3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ControllerServiceAPI.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.flow;
+
+import java.util.Objects;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class ControllerServiceAPI {
+    private String type;
+    private Bundle bundle;
+
+    @Schema(description = "The fully qualified name of the service interface.")
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    @Schema(description = "The details of the artifact that bundled this service interface.")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final ControllerServiceAPI other = (ControllerServiceAPI) o;
+        return Objects.equals(type, other.type) && Objects.equals(bundle, other.bundle);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, bundle);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/ExecutionEngine.java b/src/main/java/org/apache/nifi/flow/ExecutionEngine.java
new file mode 100644
index 0000000..9c5412a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ExecutionEngine.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.flow;
+
+public enum ExecutionEngine {
+    /**
+     * Run using the standard NiFi engine
+     */
+    STANDARD,
+
+    /**
+     * Run using the Stateless engine
+     */
+    STATELESS,
+
+    /**
+     * Use the Execution Engine that is configured for the parent Process Group. If there is no parent Process Group, default to the standard engine.
+     */
+    INHERITED;
+}
diff --git a/src/main/java/org/apache/nifi/flow/ExternalControllerServiceReference.java b/src/main/java/org/apache/nifi/flow/ExternalControllerServiceReference.java
new file mode 100644
index 0000000..f7d023e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ExternalControllerServiceReference.java
@@ -0,0 +1,44 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema
+public class ExternalControllerServiceReference {
+
+    private String identifier;
+    private String name;
+
+    @Schema(description = "The identifier of the controller service")
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @Schema(description = "The name of the controller service")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/ParameterProviderReference.java b/src/main/java/org/apache/nifi/flow/ParameterProviderReference.java
new file mode 100644
index 0000000..e8c8256
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ParameterProviderReference.java
@@ -0,0 +1,64 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema
+public class ParameterProviderReference {
+
+    private String identifier;
+    private String name;
+    private String type;
+    private Bundle bundle;
+
+    @Schema(description = "The fully qualified name of the parameter provider class.")
+    public String getType() {
+        return type;
+    }
+
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    @Schema(description = "The details of the artifact that bundled this parameter provider.")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    public void setBundle(final Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @Schema(description = "The identifier of the parameter provider")
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(final String identifier) {
+        this.identifier = identifier;
+    }
+
+    @Schema(description = "The name of the parameter provider")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/PortType.java b/src/main/java/org/apache/nifi/flow/PortType.java
new file mode 100644
index 0000000..86a994e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/PortType.java
@@ -0,0 +1,23 @@
+/*
+ * 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.flow;
+
+public enum PortType {
+    INPUT_PORT,
+    OUTPUT_PORT;
+}
diff --git a/src/main/java/org/apache/nifi/flow/Position.java b/src/main/java/org/apache/nifi/flow/Position.java
new file mode 100644
index 0000000..076e713
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/Position.java
@@ -0,0 +1,76 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Objects;
+
+@Schema(description = "The position of a component on the graph")
+public class Position {
+    private double x;
+    private double y;
+
+    public Position() {
+    }
+
+    public Position(double x, double y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    @Schema(description = "The x coordinate.")
+    public double getX() {
+        return x;
+    }
+
+    public void setX(double x) {
+        this.x = x;
+    }
+
+    @Schema(description = "The y coordinate.")
+    public double getY() {
+        return y;
+    }
+
+    public void setY(double y) {
+        this.y = y;
+    }
+
+    @Override
+    public String toString() {
+        return "[x=" + x + ", y=" + y + "]";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Position position = (Position) o;
+        return Double.compare(position.x, x) == 0 && Double.compare(position.y, y) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(x, y);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/ScheduledState.java b/src/main/java/org/apache/nifi/flow/ScheduledState.java
new file mode 100644
index 0000000..d53ede9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/ScheduledState.java
@@ -0,0 +1,23 @@
+/*
+ * 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.flow;
+
+public enum ScheduledState {
+    ENABLED,
+    DISABLED,
+    RUNNING;
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedAsset.java b/src/main/java/org/apache/nifi/flow/VersionedAsset.java
new file mode 100644
index 0000000..2f1acf6
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedAsset.java
@@ -0,0 +1,44 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedAsset {
+    private String identifier;
+    private String name;
+
+    @Schema(description = "The identifier of the asset")
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(final String identifier) {
+        this.identifier = identifier;
+    }
+
+    @Schema(description = "The name of the asset")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedComponent.java b/src/main/java/org/apache/nifi/flow/VersionedComponent.java
new file mode 100644
index 0000000..d645069
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedComponent.java
@@ -0,0 +1,113 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Objects;
+
+
+public abstract class VersionedComponent {
+
+    private String identifier;
+    private String instanceIdentifier;
+    private String groupId;
+    private String name;
+    private String comments;
+    private Position position;
+
+    @Schema(description = "The component's unique identifier")
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @Schema(description = "The instance ID of an existing component that is described by this VersionedComponent, or null if this is not mapped to an instantiated component")
+    public String getInstanceIdentifier() {
+        return instanceIdentifier;
+    }
+
+    public void setInstanceIdentifier(String instanceIdentifier) {
+        this.instanceIdentifier = instanceIdentifier;
+    }
+
+    @Schema(description = "The ID of the Process Group that this component belongs to")
+    public String getGroupIdentifier() {
+        return groupId;
+    }
+
+    public void setGroupIdentifier(String groupId) {
+        this.groupId = groupId;
+    }
+
+    @Schema(description = "The component's name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Schema(description = "The component's position on the graph")
+    public Position getPosition() {
+        return position;
+    }
+
+    public void setPosition(Position position) {
+        this.position = position;
+    }
+
+    @Schema(description = "The user-supplied comments for the component")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    public abstract ComponentType getComponentType();
+
+    public void setComponentType(ComponentType type) {
+        // purposely do nothing here, this just to allow unmarshalling
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(identifier);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof VersionedComponent)) {
+            return false;
+        }
+        final VersionedComponent other = (VersionedComponent) obj;
+        return Objects.equals(identifier, other.identifier);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java b/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
new file mode 100644
index 0000000..918a2b2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedConfigurableComponent.java
@@ -0,0 +1,34 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+
+/**
+ * A component that has property descriptors and can be configured with values for those properties.
+ */
+public interface VersionedConfigurableComponent {
+
+    Map<String, VersionedPropertyDescriptor> getPropertyDescriptors();
+
+    void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors);
+
+    Map<String, String> getProperties();
+
+    void setProperties(Map<String, String> properties);
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedConfigurableExtension.java b/src/main/java/org/apache/nifi/flow/VersionedConfigurableExtension.java
new file mode 100644
index 0000000..d21059d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedConfigurableExtension.java
@@ -0,0 +1,77 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Map;
+
+public abstract class VersionedConfigurableExtension extends VersionedComponent
+        implements VersionedExtensionComponent, VersionedConfigurableComponent {
+
+    private String type;
+    private Bundle bundle;
+
+    private Map<String, String> properties;
+    private Map<String, VersionedPropertyDescriptor> propertyDescriptors;
+
+    @Override
+    @Schema(description = "The type of the extension component")
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    @Override
+    @Schema(description = "Information about the bundle from which the component came")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    @Override
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+
+    @Override
+    @Schema(description = "The properties for the component. Properties whose value is not set will only contain the property name.")
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    @Schema(description = "The property descriptors for the component.")
+    public Map<String, VersionedPropertyDescriptor> getPropertyDescriptors() {
+        return propertyDescriptors;
+    }
+
+    @Override
+    public void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors) {
+        this.propertyDescriptors = propertyDescriptors;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedConnection.java b/src/main/java/org/apache/nifi/flow/VersionedConnection.java
new file mode 100644
index 0000000..c8e36df
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedConnection.java
@@ -0,0 +1,174 @@
+/*
+ * 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.flow;
+
+import java.util.List;
+import java.util.Set;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedConnection extends VersionedComponent {
+    private ConnectableComponent source;
+    private ConnectableComponent destination;
+    private Integer labelIndex;
+    private Long zIndex;
+    private Set<String> selectedRelationships;
+
+    private Long backPressureObjectThreshold;
+    private String backPressureDataSizeThreshold;
+    private String flowFileExpiration;
+    private List<String> prioritizers;
+    private List<Position> bends;
+
+    private String loadBalanceStrategy;
+    private String partitioningAttribute;
+    private String loadBalanceCompression;
+
+    @Schema(description = "The source of the connection.")
+    public ConnectableComponent getSource() {
+        return source;
+    }
+
+    public void setSource(ConnectableComponent source) {
+        this.source = source;
+    }
+
+    @Schema(description = "The destination of the connection.")
+    public ConnectableComponent getDestination() {
+        return destination;
+    }
+
+    public void setDestination(ConnectableComponent destination) {
+        this.destination = destination;
+    }
+
+    @Schema(description = "The bend points on the connection.")
+    public List<Position> getBends() {
+        return bends;
+    }
+
+    public void setBends(List<Position> bends) {
+        this.bends = bends;
+    }
+
+    @Schema(description = "The index of the bend point where to place the connection label.")
+    public Integer getLabelIndex() {
+        return labelIndex;
+    }
+
+    public void setLabelIndex(Integer labelIndex) {
+        this.labelIndex = labelIndex;
+    }
+
+    @Schema(
+            description = "The z index of the connection.",
+            name = "zIndex")  // Jackson maps this method name to JSON key "zIndex", but Swagger does not by default
+    public Long getzIndex() {
+        return zIndex;
+    }
+
+    public void setzIndex(Long zIndex) {
+        this.zIndex = zIndex;
+    }
+
+    @Schema(description = "The selected relationship that comprise the connection.")
+    public Set<String> getSelectedRelationships() {
+        return selectedRelationships;
+    }
+
+    public void setSelectedRelationships(Set<String> relationships) {
+        this.selectedRelationships = relationships;
+    }
+
+    @Schema(description = "The object count threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing files "
+        + "over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.")
+    public Long getBackPressureObjectThreshold() {
+        return backPressureObjectThreshold;
+    }
+
+    public void setBackPressureObjectThreshold(Long backPressureObjectThreshold) {
+        this.backPressureObjectThreshold = backPressureObjectThreshold;
+    }
+
+    @Schema(description = "The object data size threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing "
+        + "files over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.")
+    public String getBackPressureDataSizeThreshold() {
+        return backPressureDataSizeThreshold;
+    }
+
+    public void setBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) {
+        this.backPressureDataSizeThreshold = backPressureDataSizeThreshold;
+    }
+
+
+    @Schema(description = "The amount of time a flow file may be in the flow before it will be automatically aged out of the flow. Once a flow file reaches this age it will be terminated from "
+        + "the flow the next time a processor attempts to start work on it.")
+    public String getFlowFileExpiration() {
+        return flowFileExpiration;
+    }
+
+    public void setFlowFileExpiration(String flowFileExpiration) {
+        this.flowFileExpiration = flowFileExpiration;
+    }
+
+
+    @Schema(description = "The comparators used to prioritize the queue.")
+    public List<String> getPrioritizers() {
+        return prioritizers;
+    }
+
+    public void setPrioritizers(List<String> prioritizers) {
+        this.prioritizers = prioritizers;
+    }
+
+    @Schema(description = "The Strategy to use for load balancing data across the cluster, or null, if no Load Balance Strategy has been specified.",
+            allowableValues = "DO_NOT_LOAD_BALANCE, PARTITION_BY_ATTRIBUTE, ROUND_ROBIN, SINGLE_NODE")
+    public String getLoadBalanceStrategy() {
+        return loadBalanceStrategy;
+    }
+
+    public void setLoadBalanceStrategy(String loadBalanceStrategy) {
+        this.loadBalanceStrategy = loadBalanceStrategy;
+    }
+
+    @Schema(description = "The attribute to use for partitioning data as it is load balanced across the cluster. If the Load Balance Strategy is configured to use PARTITION_BY_ATTRIBUTE, the value " +
+            "returned by this method is the name of the FlowFile Attribute that will be used to determine which node in the cluster should receive a given FlowFile. If the Load Balance Strategy is " +
+            "unset or is set to any other value, the Partitioning Attribute has no effect.")
+    public String getPartitioningAttribute() {
+        return partitioningAttribute;
+    }
+
+    public void setPartitioningAttribute(final String partitioningAttribute) {
+        this.partitioningAttribute = partitioningAttribute;
+    }
+
+    @Schema(description = "Whether or not compression should be used when transferring FlowFiles between nodes",
+            allowableValues = "DO_NOT_COMPRESS, COMPRESS_ATTRIBUTES_ONLY, COMPRESS_ATTRIBUTES_AND_CONTENT")
+    public String getLoadBalanceCompression() {
+        return loadBalanceCompression;
+    }
+
+    public void setLoadBalanceCompression(final String compression) {
+        this.loadBalanceCompression = compression;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.CONNECTION;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedControllerService.java b/src/main/java/org/apache/nifi/flow/VersionedControllerService.java
new file mode 100644
index 0000000..25b857b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedControllerService.java
@@ -0,0 +1,72 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+
+public class VersionedControllerService extends VersionedConfigurableExtension {
+
+    private List<ControllerServiceAPI> controllerServiceApis;
+
+    private String annotationData;
+    private ScheduledState scheduledState;
+    private String bulletinLevel;
+
+    @Schema(description = "Lists the APIs this Controller Service implements.")
+    public List<ControllerServiceAPI> getControllerServiceApis() {
+        return controllerServiceApis;
+    }
+
+    public void setControllerServiceApis(List<ControllerServiceAPI> controllerServiceApis) {
+        this.controllerServiceApis = controllerServiceApis;
+    }
+
+    @Schema(description = "The annotation for the controller service. This is how the custom UI relays configuration to the controller service.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.CONTROLLER_SERVICE;
+    }
+
+    @Schema(description = "The ScheduledState denoting whether the Controller Service is ENABLED or DISABLED")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(final ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+
+    @Schema(description = "The level at which the controller service will report bulletins.")
+    public String getBulletinLevel() {
+        return bulletinLevel;
+    }
+
+    public void setBulletinLevel(String bulletinLevel) {
+        this.bulletinLevel = bulletinLevel;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedExtensionComponent.java b/src/main/java/org/apache/nifi/flow/VersionedExtensionComponent.java
new file mode 100644
index 0000000..f2094ba
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedExtensionComponent.java
@@ -0,0 +1,32 @@
+/*
+ * 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.flow;
+
+/**
+ * A component that is an extension and has a type and bundle.
+ */
+public interface VersionedExtensionComponent {
+
+    Bundle getBundle();
+
+    void setBundle(Bundle bundle);
+
+    String getType();
+
+    void setType(String type);
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedExternalFlow.java b/src/main/java/org/apache/nifi/flow/VersionedExternalFlow.java
new file mode 100644
index 0000000..afea048
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedExternalFlow.java
@@ -0,0 +1,68 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+
+public class VersionedExternalFlow {
+    private VersionedProcessGroup flowContents;
+    private Map<String, ExternalControllerServiceReference> externalControllerServices;
+    private Map<String, VersionedParameterContext> parameterContexts;
+    private Map<String, ParameterProviderReference> parameterProviders;
+    private VersionedExternalFlowMetadata metadata;
+
+    public VersionedProcessGroup getFlowContents() {
+        return flowContents;
+    }
+
+    public void setFlowContents(final VersionedProcessGroup flowContents) {
+        this.flowContents = flowContents;
+    }
+
+    public Map<String, ExternalControllerServiceReference> getExternalControllerServices() {
+        return externalControllerServices;
+    }
+
+    public void setExternalControllerServices(final Map<String, ExternalControllerServiceReference> externalControllerServices) {
+        this.externalControllerServices = externalControllerServices;
+    }
+
+    public Map<String, VersionedParameterContext> getParameterContexts() {
+        return parameterContexts;
+    }
+
+    public void setParameterContexts(final Map<String, VersionedParameterContext> parameterContexts) {
+        this.parameterContexts = parameterContexts;
+    }
+
+    public VersionedExternalFlowMetadata getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(final VersionedExternalFlowMetadata metadata) {
+        this.metadata = metadata;
+    }
+
+    public Map<String, ParameterProviderReference> getParameterProviders() {
+        return parameterProviders;
+    }
+
+    public void setParameterProviders(final Map<String, ParameterProviderReference> parameterProviders) {
+        this.parameterProviders = parameterProviders;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedExternalFlowMetadata.java b/src/main/java/org/apache/nifi/flow/VersionedExternalFlowMetadata.java
new file mode 100644
index 0000000..09f32d3
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedExternalFlowMetadata.java
@@ -0,0 +1,84 @@
+/*
+ * 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.flow;
+
+public class VersionedExternalFlowMetadata {
+    private String bucketId;
+    private String flowId;
+    private String version;
+    private String flowName;
+    private String author;
+    private String comments;
+    private long timestamp;
+
+    public String getBucketIdentifier() {
+        return bucketId;
+    }
+
+    public void setBucketIdentifier(final String bucketIdentifier) {
+        this.bucketId = bucketIdentifier;
+    }
+
+    public String getFlowIdentifier() {
+        return flowId;
+    }
+
+    public void setFlowIdentifier(final String flowId) {
+        this.flowId = flowId;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(final String version) {
+        this.version = version;
+    }
+
+    public String getFlowName() {
+        return flowName;
+    }
+
+    public void setFlowName(final String flowName) {
+        this.flowName = flowName;
+    }
+
+    public String getAuthor() {
+        return author;
+    }
+
+    public void setAuthor(final String author) {
+        this.author = author;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(final String comments) {
+        this.comments = comments;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(final long timestamp) {
+        this.timestamp = timestamp;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java b/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java
new file mode 100644
index 0000000..0d5abd8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedFlowAnalysisRule.java
@@ -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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.nifi.flowanalysis.EnforcementPolicy;
+
+public class VersionedFlowAnalysisRule extends VersionedConfigurableExtension {
+
+    private ScheduledState scheduledState;
+    private EnforcementPolicy enforcementPolicy;
+
+    @Schema(description = "How to handle violations.")
+    public EnforcementPolicy getEnforcementPolicy() {
+        return enforcementPolicy;
+    }
+
+    public void setEnforcementPolicy(EnforcementPolicy enforcementPolicy) {
+        this.enforcementPolicy = enforcementPolicy;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.FLOW_ANALYSIS_RULE;
+    }
+
+    @Schema(description = "Indicates the scheduled state for the flow analysis rule")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(final ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedFlowCoordinates.java b/src/main/java/org/apache/nifi/flow/VersionedFlowCoordinates.java
new file mode 100644
index 0000000..f96b36d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedFlowCoordinates.java
@@ -0,0 +1,124 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Objects;
+
+public class VersionedFlowCoordinates {
+    private String registryId;
+    private String storageLocation;
+    private String branch;
+    private String bucketId;
+    private String flowId;
+    private String version;
+    private Boolean latest;
+
+    @Schema(description = "The identifier of the Flow Registry that contains the flow")
+    public String getRegistryId() {
+        return registryId;
+    }
+
+    public void setRegistryId(String registryId) {
+        this.registryId = registryId;
+    }
+
+    @Schema(description = "The location of the Flow Registry that stores the flow")
+    public String getStorageLocation() {
+        return storageLocation;
+    }
+
+    public void setStorageLocation(String storageLocation) {
+        this.storageLocation = storageLocation;
+    }
+
+    @Schema(description = "The name of the branch that the flow resides in")
+    public String getBranch() {
+        return branch;
+    }
+
+    public void setBranch(final String branch) {
+        this.branch = branch;
+    }
+
+    @Schema(description = "The UUID of the bucket that the flow resides in")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @Schema(description = "The UUID of the flow")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    @Schema(description = "The version of the flow")
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    @Schema(description = "Whether or not these coordinates point to the latest version of the flow")
+    public Boolean getLatest() {
+        return latest;
+    }
+
+    public void setLatest(Boolean latest) {
+        this.latest = latest;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(registryId, storageLocation, branch, bucketId, flowId, version);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof final VersionedFlowCoordinates other)) {
+            return false;
+        }
+
+        return Objects.equals(storageLocation, other.storageLocation)
+                && Objects.equals(branch, other.branch)
+                && Objects.equals(bucketId, other.bucketId)
+                && Objects.equals(flowId, other.flowId)
+                && Objects.equals(version, other.version);
+    }
+
+    @Override
+    public String toString() {
+        return "VersionedFlowCoordinates[branch=" + branch + ", bucketId=" + bucketId + ", flowId=" + flowId + ", version=" + version + ", storageLocation=" + storageLocation + "]";
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedFlowRegistryClient.java b/src/main/java/org/apache/nifi/flow/VersionedFlowRegistryClient.java
new file mode 100644
index 0000000..3b4c9eb
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedFlowRegistryClient.java
@@ -0,0 +1,48 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedFlowRegistryClient extends VersionedConfigurableExtension {
+    private String description;
+    private String annotationData;
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.FLOW_REGISTRY_CLIENT;
+    }
+
+    @Schema(description = "The description of the registry")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    @Schema(description = "The annotation for the reporting task. This is how the custom UI relays configuration to the reporting task.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedFunnel.java b/src/main/java/org/apache/nifi/flow/VersionedFunnel.java
new file mode 100644
index 0000000..aa66d92
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedFunnel.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.flow;
+
+public class VersionedFunnel extends VersionedComponent {
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.FUNNEL;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedLabel.java b/src/main/java/org/apache/nifi/flow/VersionedLabel.java
new file mode 100644
index 0000000..d0e8e24
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedLabel.java
@@ -0,0 +1,85 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedLabel extends VersionedComponent {
+    private String label;
+    private Long zIndex;
+
+    private Double width;
+    private Double height;
+
+    private Map<String, String> style;
+
+
+    @Schema(description = "The text that appears in the label.")
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(final String label) {
+        this.label = label;
+    }
+
+    @Schema(description = "The styles for this label (font-size : 12px, background-color : #eee, etc).")
+    public Map<String, String> getStyle() {
+        return style;
+    }
+
+    public void setStyle(final Map<String, String> style) {
+        this.style = style;
+    }
+
+    @Schema(description = "The height of the label in pixels when at a 1:1 scale.")
+    public Double getHeight() {
+        return height;
+    }
+
+    public void setHeight(Double height) {
+        this.height = height;
+    }
+
+    @Schema(description = "The width of the label in pixels when at a 1:1 scale.")
+    public Double getWidth() {
+        return width;
+    }
+
+    public void setWidth(Double width) {
+        this.width = width;
+    }
+
+    @Schema(
+            description = "The z index of the connection.",
+            name = "zIndex")  // Jackson maps this method name to JSON key "zIndex", but Swagger does not by default
+    public Long getzIndex() {
+        return zIndex;
+    }
+
+    public void setzIndex(Long zIndex) {
+        this.zIndex = zIndex;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.LABEL;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedParameter.java b/src/main/java/org/apache/nifi/flow/VersionedParameter.java
new file mode 100644
index 0000000..35dd4f8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedParameter.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+import java.util.Objects;
+
+public class VersionedParameter {
+
+    private String name;
+    private String description;
+    private boolean sensitive;
+    private boolean provided;
+    private String value;
+    private List<VersionedAsset> referencedAssets;
+
+    @Schema(description = "The name of the parameter")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Schema(description = "The description of the param")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Schema(description = "Whether or not the parameter value is sensitive")
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+    public void setSensitive(boolean sensitive) {
+        this.sensitive = sensitive;
+    }
+
+    @Schema(description = "Whether or not the parameter value is provided by a ParameterProvider")
+    public boolean isProvided() {
+        return provided;
+    }
+
+    public void setProvided(boolean provided) {
+        this.provided = provided;
+    }
+
+    @Schema(description = "The value of the parameter")
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Schema(description = "The assets that are referenced by this parameter")
+    public List<VersionedAsset> getReferencedAssets() {
+        return referencedAssets;
+    }
+
+    public void setReferencedAssets(final List<VersionedAsset> referencedAssets) {
+        this.referencedAssets = referencedAssets;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        VersionedParameter that = (VersionedParameter) o;
+        return Objects.equals(name, that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedParameterContext.java b/src/main/java/org/apache/nifi/flow/VersionedParameterContext.java
new file mode 100644
index 0000000..717ccbd
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedParameterContext.java
@@ -0,0 +1,90 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+import java.util.Set;
+
+public class VersionedParameterContext extends VersionedComponent {
+    private Set<VersionedParameter> parameters;
+    private List<String> inheritedParameterContexts;
+    private String description;
+    private String parameterProvider;
+    private String parameterGroupName;
+    private Boolean isSynchronized;
+
+    @Schema(description = "The description of the parameter context")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Schema(description = "The parameters in the context")
+    public Set<VersionedParameter> getParameters() {
+        return parameters;
+    }
+
+    public void setParameters(Set<VersionedParameter> parameters) {
+        this.parameters = parameters;
+    }
+
+    @Schema(description = "The names of additional parameter contexts from which to inherit parameters")
+    public List<String> getInheritedParameterContexts() {
+        return inheritedParameterContexts;
+    }
+
+    public void setInheritedParameterContexts(List<String> parameterContextNames) {
+        this.inheritedParameterContexts = parameterContextNames;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PARAMETER_CONTEXT;
+    }
+
+    @Schema(description = "The identifier of an optional parameter provider")
+    public String getParameterProvider() {
+        return parameterProvider;
+    }
+
+    public void setParameterProvider(String parameterProvider) {
+        this.parameterProvider = parameterProvider;
+    }
+
+    @Schema(description = "The corresponding parameter group name fetched from the parameter provider, if applicable")
+    public String getParameterGroupName() {
+        return parameterGroupName;
+    }
+
+    public void setParameterGroupName(String parameterGroupName) {
+        this.parameterGroupName = parameterGroupName;
+    }
+
+    @Schema(description = "True if the parameter provider is set and the context should receive updates when its parameters are next fetched")
+    public Boolean isSynchronized() {
+        return isSynchronized;
+    }
+
+    public void setSynchronized(Boolean aSynchronized) {
+        isSynchronized = aSynchronized;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedParameterProvider.java b/src/main/java/org/apache/nifi/flow/VersionedParameterProvider.java
new file mode 100644
index 0000000..576b1d0
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedParameterProvider.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Map;
+
+public class VersionedParameterProvider extends VersionedComponent implements VersionedConfigurableComponent, VersionedExtensionComponent {
+    private String type;
+    private Bundle bundle;
+    private Map<String, String> properties;
+    private Map<String, VersionedPropertyDescriptor> propertyDescriptors;
+    private String annotationData;
+
+
+    @Override
+    @Schema(description = "The type of the parameter provider.")
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    @Override
+    @Schema(description = "The details of the artifact that bundled this parameter provider type.")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    @Override
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @Override
+    @Schema(description = "The properties of the parameter provider.")
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    @Schema(description = "The property descriptors for the parameter provider.")
+    public Map<String, VersionedPropertyDescriptor> getPropertyDescriptors() {
+        return propertyDescriptors;
+    }
+
+    @Override
+    public void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors) {
+        this.propertyDescriptors = propertyDescriptors;
+    }
+
+    @Schema(description = "The annotation for the parameter provider. This is how the custom UI relays configuration to the parameter provider.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PARAMETER_PROVIDER;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedPort.java b/src/main/java/org/apache/nifi/flow/VersionedPort.java
new file mode 100644
index 0000000..cd052c4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedPort.java
@@ -0,0 +1,87 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.nifi.components.PortFunction;
+
+public class VersionedPort extends VersionedComponent {
+    private PortType type;
+    private Integer concurrentlySchedulableTaskCount;
+    private ScheduledState scheduledState;
+    private Boolean allowRemoteAccess;
+    private PortFunction portFunction;
+
+    @Schema(description = "The number of tasks that should be concurrently scheduled for the port.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @Schema(description = "The type of port.")
+    public PortType getType() {
+        return type;
+    }
+
+    public void setType(PortType type) {
+        this.type = type;
+    }
+
+    @Schema(description = "The scheduled state of the component")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+
+    @Schema(description = "Whether or not this port allows remote access for site-to-site")
+    public Boolean isAllowRemoteAccess() {
+        return allowRemoteAccess != null && allowRemoteAccess;
+    }
+
+    public Boolean getAllowRemoteAccess() {
+        return allowRemoteAccess;
+    }
+
+    public void setAllowRemoteAccess(Boolean allowRemoteAccess) {
+        this.allowRemoteAccess = allowRemoteAccess;
+    }
+
+    @Schema(description = "Specifies how the Port should function")
+    public PortFunction getPortFunction() {
+        return portFunction;
+    }
+
+    public void setPortFunction(final PortFunction portFunction) {
+        this.portFunction = portFunction;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        if (type == PortType.OUTPUT_PORT) {
+            return ComponentType.OUTPUT_PORT;
+        }
+
+        return ComponentType.INPUT_PORT;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedProcessGroup.java b/src/main/java/org/apache/nifi/flow/VersionedProcessGroup.java
new file mode 100644
index 0000000..e63c59b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedProcessGroup.java
@@ -0,0 +1,246 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class VersionedProcessGroup extends VersionedComponent {
+
+    private Set<VersionedProcessGroup> processGroups = new HashSet<>();
+    private Set<VersionedRemoteProcessGroup> remoteProcessGroups = new HashSet<>();
+    private Set<VersionedProcessor> processors = new HashSet<>();
+    private Set<VersionedPort> inputPorts = new HashSet<>();
+    private Set<VersionedPort> outputPorts = new HashSet<>();
+    private Set<VersionedConnection> connections = new HashSet<>();
+    private Set<VersionedLabel> labels = new HashSet<>();
+    private Set<VersionedFunnel> funnels = new HashSet<>();
+    private Set<VersionedControllerService> controllerServices = new HashSet<>();
+    private VersionedFlowCoordinates versionedFlowCoordinates = null;
+
+    private String parameterContextName;
+    private String flowfileConcurrency;
+    private String flowfileOutboundPolicy;
+
+    private String defaultFlowFileExpiration;
+    private Long defaultBackPressureObjectThreshold;
+    private String defaultBackPressureDataSizeThreshold;
+    private ScheduledState scheduledState;
+    private ExecutionEngine executionEngine;
+    private Integer maxConcurrentTasks;
+    private String statelessFlowTimeout;
+
+    private String logFileSuffix;
+
+
+    @Schema(description = "The child Process Groups")
+    public Set<VersionedProcessGroup> getProcessGroups() {
+        return processGroups;
+    }
+
+    public void setProcessGroups(Set<VersionedProcessGroup> processGroups) {
+        this.processGroups = new HashSet<>(processGroups);
+    }
+
+    @Schema(description = "The Remote Process Groups")
+    public Set<VersionedRemoteProcessGroup> getRemoteProcessGroups() {
+        return remoteProcessGroups;
+    }
+
+    public void setRemoteProcessGroups(Set<VersionedRemoteProcessGroup> remoteProcessGroups) {
+        this.remoteProcessGroups = new HashSet<>(remoteProcessGroups);
+    }
+
+    @Schema(description = "The Processors")
+    public Set<VersionedProcessor> getProcessors() {
+        return processors;
+    }
+
+    public void setProcessors(Set<VersionedProcessor> processors) {
+        this.processors = new HashSet<>(processors);
+    }
+
+    @Schema(description = "The Input Ports")
+    public Set<VersionedPort> getInputPorts() {
+        return inputPorts;
+    }
+
+    public void setInputPorts(Set<VersionedPort> inputPorts) {
+        this.inputPorts = new HashSet<>(inputPorts);
+    }
+
+    @Schema(description = "The Output Ports")
+    public Set<VersionedPort> getOutputPorts() {
+        return outputPorts;
+    }
+
+    public void setOutputPorts(Set<VersionedPort> outputPorts) {
+        this.outputPorts = new HashSet<>(outputPorts);
+    }
+
+    @Schema(description = "The Connections")
+    public Set<VersionedConnection> getConnections() {
+        return connections;
+    }
+
+    public void setConnections(Set<VersionedConnection> connections) {
+        this.connections = new HashSet<>(connections);
+    }
+
+    @Schema(description = "The Labels")
+    public Set<VersionedLabel> getLabels() {
+        return labels;
+    }
+
+    public void setLabels(Set<VersionedLabel> labels) {
+        this.labels = new HashSet<>(labels);
+    }
+
+    @Schema(description = "The Funnels")
+    public Set<VersionedFunnel> getFunnels() {
+        return funnels;
+    }
+
+    public void setFunnels(Set<VersionedFunnel> funnels) {
+        this.funnels = new HashSet<>(funnels);
+    }
+
+    @Schema(description = "The Controller Services")
+    public Set<VersionedControllerService> getControllerServices() {
+        return controllerServices;
+    }
+
+    public void setControllerServices(Set<VersionedControllerService> controllerServices) {
+        this.controllerServices = new HashSet<>(controllerServices);
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PROCESS_GROUP;
+    }
+
+    public void setVersionedFlowCoordinates(VersionedFlowCoordinates flowCoordinates) {
+        this.versionedFlowCoordinates = flowCoordinates;
+    }
+
+    @Schema(description = "The coordinates where the remote flow is stored, or null if the Process Group is not directly under Version Control")
+    public VersionedFlowCoordinates getVersionedFlowCoordinates() {
+        return versionedFlowCoordinates;
+    }
+
+    @Schema(description = "The name of the parameter context used by this process group")
+    public String getParameterContextName() {
+        return parameterContextName;
+    }
+
+    public void setParameterContextName(String parameterContextName) {
+        this.parameterContextName = parameterContextName;
+    }
+
+    @Schema(description = "The configured FlowFile Concurrency for the Process Group")
+    public String getFlowFileConcurrency() {
+        return flowfileConcurrency;
+    }
+
+    public void setFlowFileConcurrency(final String flowfileConcurrency) {
+        this.flowfileConcurrency = flowfileConcurrency;
+    }
+
+    @Schema(description = "The FlowFile Outbound Policy for the Process Group")
+    public String getFlowFileOutboundPolicy() {
+        return flowfileOutboundPolicy;
+    }
+
+    public void setFlowFileOutboundPolicy(final String outboundPolicy) {
+        this.flowfileOutboundPolicy = outboundPolicy;
+    }
+
+    @Schema(description = "The default FlowFile Expiration for this Process Group.")
+    public String getDefaultFlowFileExpiration() {
+        return defaultFlowFileExpiration;
+    }
+
+    public void setDefaultFlowFileExpiration(String defaultFlowFileExpiration) {
+        this.defaultFlowFileExpiration = defaultFlowFileExpiration;
+    }
+
+    @Schema(description = "Default value used in this Process Group for the maximum number of objects that can be queued before back pressure is applied.")
+    public Long getDefaultBackPressureObjectThreshold() {
+        return defaultBackPressureObjectThreshold;
+    }
+
+    public void setDefaultBackPressureObjectThreshold(final Long defaultBackPressureObjectThreshold) {
+        this.defaultBackPressureObjectThreshold = defaultBackPressureObjectThreshold;
+    }
+
+    @Schema(description = "Default value used in this Process Group for the maximum data size of objects that can be queued before back pressure is applied.")
+    public String getDefaultBackPressureDataSizeThreshold() {
+        return defaultBackPressureDataSizeThreshold;
+    }
+
+    public void setDefaultBackPressureDataSizeThreshold(final String defaultBackPressureDataSizeThreshold) {
+        this.defaultBackPressureDataSizeThreshold = defaultBackPressureDataSizeThreshold;
+    }
+
+    @Schema(description = "The log file suffix for this Process Group for dedicated logging.")
+    public String getLogFileSuffix() {
+        return logFileSuffix;
+    }
+
+    public void setLogFileSuffix(final String logFileSuffix) {
+        this.logFileSuffix = logFileSuffix;
+    }
+
+    @Schema(description = "The Scheduled State of the Process Group, if the group is configured to use the Stateless Execution Engine. Otherwise, this value has no relevance.")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(final ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+
+    @Schema(description = "The Execution Engine that should be used to run the components within the group.")
+    public ExecutionEngine getExecutionEngine() {
+        return executionEngine;
+    }
+
+    public void setExecutionEngine(final ExecutionEngine executionEngine) {
+        this.executionEngine = executionEngine;
+    }
+
+    @Schema(description = "The maximum number of concurrent tasks that should be scheduled for this Process Group when using the Stateless Engine")
+    public Integer getMaxConcurrentTasks() {
+        return maxConcurrentTasks;
+    }
+
+    public void setMaxConcurrentTasks(final Integer maxConcurrentTasks) {
+        this.maxConcurrentTasks = maxConcurrentTasks;
+    }
+
+    @Schema(description = "The maximum amount of time that the flow is allows to run using the Stateless engine before it times out and is considered a failure")
+    public String getStatelessFlowTimeout() {
+        return statelessFlowTimeout;
+    }
+
+    public void setStatelessFlowTimeout(final String timeout) {
+        this.statelessFlowTimeout = timeout;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedProcessor.java b/src/main/java/org/apache/nifi/flow/VersionedProcessor.java
new file mode 100644
index 0000000..266c6e9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedProcessor.java
@@ -0,0 +1,200 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Map;
+import java.util.Set;
+
+public class VersionedProcessor extends VersionedConfigurableExtension {
+
+    private Map<String, String> style;
+    private String annotationData;
+
+    private String schedulingPeriod;
+    private String schedulingStrategy;
+    private String executionNode;
+    private String penaltyDuration;
+    private String yieldDuration;
+    private String bulletinLevel;
+    private Long runDurationMillis;
+    private Integer concurrentlySchedulableTaskCount;
+    private Set<String> autoTerminatedRelationships;
+    private ScheduledState scheduledState;
+
+    private Integer retryCount;
+    private Set<String> retriedRelationships;
+    private String backoffMechanism;
+    private String maxBackoffPeriod;
+
+    @Schema(description = "The frequency with which to schedule the processor. The format of the value will depend on th value of schedulingStrategy.")
+    public String getSchedulingPeriod() {
+        return schedulingPeriod;
+    }
+
+    public void setSchedulingPeriod(String setSchedulingPeriod) {
+        this.schedulingPeriod = setSchedulingPeriod;
+    }
+
+    @Schema(description = "Indicates how the processor should be scheduled to run.")
+    public String getSchedulingStrategy() {
+        return schedulingStrategy;
+    }
+
+    public void setSchedulingStrategy(String schedulingStrategy) {
+        this.schedulingStrategy = schedulingStrategy;
+    }
+
+    @Schema(description = "Indicates the node where the process will execute.")
+    public String getExecutionNode() {
+        return executionNode;
+    }
+
+    public void setExecutionNode(String executionNode) {
+        this.executionNode = executionNode;
+    }
+
+    @Schema(description = "The amout of time that is used when the process penalizes a flowfile.")
+    public String getPenaltyDuration() {
+        return penaltyDuration;
+    }
+
+    public void setPenaltyDuration(String penaltyDuration) {
+        this.penaltyDuration = penaltyDuration;
+    }
+
+    @Schema(description = "The amount of time that must elapse before this processor is scheduled again after yielding.")
+    public String getYieldDuration() {
+        return yieldDuration;
+    }
+
+    public void setYieldDuration(String yieldDuration) {
+        this.yieldDuration = yieldDuration;
+    }
+
+    @Schema(description = "The level at which the processor will report bulletins.")
+    public String getBulletinLevel() {
+        return bulletinLevel;
+    }
+
+    public void setBulletinLevel(String bulletinLevel) {
+        this.bulletinLevel = bulletinLevel;
+    }
+
+    @Schema(description = "The number of tasks that should be concurrently schedule for the processor. If the processor doesn't allow parallol processing then any positive input will be ignored.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @Schema(description = "The annotation data for the processor used to relay configuration between a custom UI and the procesosr.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+
+    @Schema(description = "The names of all relationships that cause a flow file to be terminated if the relationship is not connected elsewhere. This property differs "
+        + "from the 'isAutoTerminate' property of the RelationshipDTO in that the RelationshipDTO is meant to depict the current configuration, whereas this "
+        + "property can be set in a DTO when updating a Processor in order to change which Relationships should be auto-terminated.")
+    public Set<String> getAutoTerminatedRelationships() {
+        return autoTerminatedRelationships;
+    }
+
+    public void setAutoTerminatedRelationships(final Set<String> autoTerminatedRelationships) {
+        this.autoTerminatedRelationships = autoTerminatedRelationships;
+    }
+
+    @Schema(description = "The run duration for the processor in milliseconds.")
+    public Long getRunDurationMillis() {
+        return runDurationMillis;
+    }
+
+    public void setRunDurationMillis(Long runDurationMillis) {
+        this.runDurationMillis = runDurationMillis;
+    }
+
+    @Schema(description = "Stylistic data for rendering in a UI")
+    public Map<String, String> getStyle() {
+        return style;
+    }
+
+    public void setStyle(Map<String, String> style) {
+        this.style = style;
+    }
+
+    @Schema(description = "The scheduled state of the component")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PROCESSOR;
+    }
+
+    @Schema(description = "Overall number of retries.")
+    public Integer getRetryCount() {
+        return retryCount;
+    }
+
+    public void setRetryCount(Integer retryCount) {
+        this.retryCount = retryCount;
+    }
+
+    @Schema(description = "All the relationships should be retried.")
+    public Set<String> getRetriedRelationships() {
+        return retriedRelationships;
+    }
+
+    public void setRetriedRelationships(Set<String> retriedRelationships) {
+        this.retriedRelationships = retriedRelationships;
+    }
+
+    @Schema(
+            description = "Determines whether the FlowFile should be penalized or the processor should be yielded between retries.",
+            allowableValues = "PENALIZE_FLOWFILE, YIELD_PROCESSOR"
+    )
+    public String getBackoffMechanism() {
+        return backoffMechanism;
+    }
+
+    public void setBackoffMechanism(String backoffMechanism) {
+        this.backoffMechanism = backoffMechanism;
+    }
+
+    @Schema(description = "Maximum amount of time to be waited during a retry period.")
+    public String getMaxBackoffPeriod() {
+        return maxBackoffPeriod;
+    }
+
+    public void setMaxBackoffPeriod(String maxBackoffPeriod) {
+        this.maxBackoffPeriod = maxBackoffPeriod;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java b/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java
new file mode 100644
index 0000000..1540935
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedPropertyDescriptor.java
@@ -0,0 +1,83 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedPropertyDescriptor {
+    private String name;
+    private String displayName;
+    private boolean identifiesControllerService;
+    private boolean sensitive;
+    private boolean dynamic;
+    private VersionedResourceDefinition resourceDefinition;
+
+    @Schema(description = "The name of the property")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Schema(description = "The display name of the property")
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    @Schema(description = "Whether or not the property provides the identifier of a Controller Service")
+    public boolean getIdentifiesControllerService() {
+        return identifiesControllerService;
+    }
+
+    public void setIdentifiesControllerService(boolean identifiesControllerService) {
+        this.identifiesControllerService = identifiesControllerService;
+    }
+
+    @Schema(description = "Whether or not the property is considered sensitive")
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+    public void setSensitive(boolean sensitive) {
+        this.sensitive = sensitive;
+    }
+
+    @Schema(description = "Whether or not the property is user-defined")
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+    public void setDynamic(boolean dynamic) {
+        this.dynamic = dynamic;
+    }
+
+    @Schema(description = "Returns the Resource Definition that defines which type(s) of resource(s) this property references, if any")
+    public VersionedResourceDefinition getResourceDefinition() {
+        return resourceDefinition;
+    }
+
+    public void setResourceDefinition(final VersionedResourceDefinition resourceDefinition) {
+        this.resourceDefinition = resourceDefinition;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedRemoteGroupPort.java b/src/main/java/org/apache/nifi/flow/VersionedRemoteGroupPort.java
new file mode 100644
index 0000000..777866a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedRemoteGroupPort.java
@@ -0,0 +1,119 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Objects;
+
+public class VersionedRemoteGroupPort extends VersionedComponent {
+    private String remoteGroupId;
+    private Integer concurrentlySchedulableTaskCount;
+    private Boolean useCompression;
+    private BatchSize batchSize;
+    private ComponentType componentType;
+    private String targetId;
+    private ScheduledState scheduledState;
+
+    @Schema(description = "The number of task that may transmit flowfiles to the target port concurrently.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @Schema(description = "The id of the remote process group that the port resides in.")
+    public String getRemoteGroupId() {
+        return remoteGroupId;
+    }
+
+    public void setRemoteGroupId(String groupId) {
+        this.remoteGroupId = groupId;
+    }
+
+
+    @Schema(description = "Whether the flowfiles are compressed when sent to the target port.")
+    public Boolean isUseCompression() {
+        return useCompression;
+    }
+
+    public void setUseCompression(Boolean useCompression) {
+        this.useCompression = useCompression;
+    }
+
+    @Schema(description = "The batch settings for data transmission.")
+    public BatchSize getBatchSize() {
+        return batchSize;
+    }
+
+    public void setBatchSize(BatchSize batchSize) {
+        this.batchSize = batchSize;
+    }
+
+    @Schema(description = "The ID of the port on the target NiFi instance")
+    public String getTargetId() {
+        return targetId;
+    }
+
+    public void setTargetId(final String targetId) {
+        this.targetId = targetId;
+    }
+
+    @Schema(description = "The scheduled state of the component")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+
+    @Override
+    public int hashCode() {
+        return 923847 + String.valueOf(getName()).hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof VersionedRemoteGroupPort)) {
+            return false;
+        }
+
+        final VersionedRemoteGroupPort other = (VersionedRemoteGroupPort) obj;
+        return Objects.equals(getName(), other.getName());
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return componentType;
+    }
+
+    @Override
+    public void setComponentType(final ComponentType componentType) {
+        if (componentType != ComponentType.REMOTE_INPUT_PORT && componentType != ComponentType.REMOTE_OUTPUT_PORT) {
+            throw new IllegalArgumentException();
+        }
+
+        this.componentType = componentType;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedRemoteProcessGroup.java b/src/main/java/org/apache/nifi/flow/VersionedRemoteProcessGroup.java
new file mode 100644
index 0000000..b2fdd14
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedRemoteProcessGroup.java
@@ -0,0 +1,146 @@
+/*
+ * 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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Set;
+
+public class VersionedRemoteProcessGroup extends VersionedComponent {
+    private String targetUris;
+
+    private String communicationsTimeout;
+    private String yieldDuration;
+    private String transportProtocol;
+    private String localNetworkInterface;
+    private String proxyHost;
+    private Integer proxyPort;
+    private String proxyUser;
+    private String proxyPassword;
+
+    private Set<VersionedRemoteGroupPort> inputPorts;
+    private Set<VersionedRemoteGroupPort> outputPorts;
+
+    @Schema(description = "The target URIs of the remote process group. " +
+            "If target uris is not set but target uri is set, then returns the single target uri. " +
+            "If neither target uris nor target uri is set, then returns null.")
+    public String getTargetUris() {
+        return !isEmpty(targetUris) ? targetUris : null;
+
+    }
+
+    private boolean isEmpty(final String value) {
+        return value == null || value.isEmpty();
+    }
+
+    public void setTargetUris(String targetUris) {
+        this.targetUris = targetUris;
+    }
+
+    @Schema(description = "The time period used for the timeout when communicating with the target.")
+    public String getCommunicationsTimeout() {
+        return communicationsTimeout;
+    }
+
+    public void setCommunicationsTimeout(String communicationsTimeout) {
+        this.communicationsTimeout = communicationsTimeout;
+    }
+
+    @Schema(description = "When yielding, this amount of time must elapse before the remote process group is scheduled again.")
+    public String getYieldDuration() {
+        return yieldDuration;
+    }
+
+    public void setYieldDuration(String yieldDuration) {
+        this.yieldDuration = yieldDuration;
+    }
+
+    @Schema(description = "The Transport Protocol that is used for Site-to-Site communications", allowableValues = "RAW, HTTP")
+    public String getTransportProtocol() {
+        return transportProtocol;
+    }
+
+    public void setTransportProtocol(String transportProtocol) {
+        this.transportProtocol = transportProtocol;
+    }
+
+    @Schema(description = "A Set of Input Ports that can be connected to, in order to send data to the remote NiFi instance")
+    public Set<VersionedRemoteGroupPort> getInputPorts() {
+        return inputPorts;
+    }
+
+    public void setInputPorts(Set<VersionedRemoteGroupPort> inputPorts) {
+        this.inputPorts = inputPorts;
+    }
+
+    @Schema(description = "A Set of Output Ports that can be connected to, in order to pull data from the remote NiFi instance")
+    public Set<VersionedRemoteGroupPort> getOutputPorts() {
+        return outputPorts;
+    }
+
+    public void setOutputPorts(Set<VersionedRemoteGroupPort> outputPorts) {
+        this.outputPorts = outputPorts;
+    }
+
+
+    @Schema(description = "The local network interface to send/receive data. If not specified, any local address is used. If clustered, all nodes must have an interface with this identifier.")
+    public String getLocalNetworkInterface() {
+        return localNetworkInterface;
+    }
+
+    public void setLocalNetworkInterface(String localNetworkInterface) {
+        this.localNetworkInterface = localNetworkInterface;
+    }
+
+    public String getProxyHost() {
+        return proxyHost;
+    }
+
+    public void setProxyHost(String proxyHost) {
+        this.proxyHost = proxyHost;
+    }
+
+    public Integer getProxyPort() {
+        return proxyPort;
+    }
+
+    public void setProxyPort(Integer proxyPort) {
+        this.proxyPort = proxyPort;
+    }
+
+    public String getProxyUser() {
+        return proxyUser;
+    }
+
+    public void setProxyUser(String proxyUser) {
+        this.proxyUser = proxyUser;
+    }
+
+    public String getProxyPassword() {
+        return proxyPassword;
+    }
+
+    public void setProxyPassword(String proxyPassword) {
+        this.proxyPassword = proxyPassword;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.REMOTE_PROCESS_GROUP;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedReportingTask.java b/src/main/java/org/apache/nifi/flow/VersionedReportingTask.java
new file mode 100644
index 0000000..6e9cc03
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedReportingTask.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class VersionedReportingTask extends VersionedConfigurableExtension {
+
+    private String annotationData;
+    private ScheduledState scheduledState;
+    private String schedulingPeriod;
+    private String schedulingStrategy;
+
+    @Schema(description = "The annotation for the reporting task. This is how the custom UI relays configuration to the reporting task.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+    @Schema(description = "The frequency with which to schedule the reporting task. The format of the value will depend on the value of schedulingStrategy.")
+    public String getSchedulingPeriod() {
+        return schedulingPeriod;
+    }
+
+    public void setSchedulingPeriod(String setSchedulingPeriod) {
+        this.schedulingPeriod = setSchedulingPeriod;
+    }
+
+    @Schema(description = "Indicates scheduling strategy that should dictate how the reporting task is triggered.")
+    public String getSchedulingStrategy() {
+        return schedulingStrategy;
+    }
+
+    public void setSchedulingStrategy(String schedulingStrategy) {
+        this.schedulingStrategy = schedulingStrategy;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.REPORTING_TASK;
+    }
+
+    @Schema(description = "Indicates the scheduled state for the Reporting Task")
+    public ScheduledState getScheduledState() {
+        return scheduledState;
+    }
+
+    public void setScheduledState(final ScheduledState scheduledState) {
+        this.scheduledState = scheduledState;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedReportingTaskSnapshot.java b/src/main/java/org/apache/nifi/flow/VersionedReportingTaskSnapshot.java
new file mode 100644
index 0000000..d1d7e53
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedReportingTaskSnapshot.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.List;
+
+@Schema
+public class VersionedReportingTaskSnapshot {
+
+    private List<VersionedReportingTask> reportingTasks;
+    private List<VersionedControllerService> controllerServices;
+
+    @Schema(description = "The controller services")
+    public List<VersionedControllerService> getControllerServices() {
+        return controllerServices;
+    }
+
+    public void setControllerServices(List<VersionedControllerService> controllerServices) {
+        this.controllerServices = controllerServices;
+    }
+
+    @Schema(description = "The reporting tasks")
+    public List<VersionedReportingTask> getReportingTasks() {
+        return reportingTasks;
+    }
+
+    public void setReportingTasks(List<VersionedReportingTask> reportingTasks) {
+        this.reportingTasks = reportingTasks;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedResourceCardinality.java b/src/main/java/org/apache/nifi/flow/VersionedResourceCardinality.java
new file mode 100644
index 0000000..3ce4495
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedResourceCardinality.java
@@ -0,0 +1,32 @@
+/*
+ * 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.flow;
+
+public enum VersionedResourceCardinality {
+
+    /**
+     * Exactly one resource must be specified
+     */
+    SINGLE,
+
+    /**
+     * One or more resources may be supplied, as a comma-separated list
+     */
+    MULTIPLE;
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedResourceDefinition.java b/src/main/java/org/apache/nifi/flow/VersionedResourceDefinition.java
new file mode 100644
index 0000000..a04e23b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedResourceDefinition.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.flow;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Set;
+
+public class VersionedResourceDefinition {
+    private VersionedResourceCardinality cardinality;
+    private Set<VersionedResourceType> resourceTypes;
+
+    @Schema(description = "The cardinality of the resource")
+    public VersionedResourceCardinality getCardinality() {
+        return cardinality;
+    }
+
+    public void setCardinality(final VersionedResourceCardinality cardinality) {
+        this.cardinality = cardinality;
+    }
+
+    @Schema(description = "The types of resource that the Property Descriptor is allowed to reference")
+    public Set<VersionedResourceType> getResourceTypes() {
+        return resourceTypes;
+    }
+
+    public void setResourceTypes(final Set<VersionedResourceType> resourceTypes) {
+        this.resourceTypes = resourceTypes;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flow/VersionedResourceType.java b/src/main/java/org/apache/nifi/flow/VersionedResourceType.java
new file mode 100644
index 0000000..7fb67d8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/VersionedResourceType.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.flow;
+
+public enum VersionedResourceType {
+    /**
+     * Referenced Resource is a File on a local (or mounted) file system
+     */
+    FILE,
+
+    /**
+     * Referenced Resource is a directory on a local (or mounted) file system
+     */
+    DIRECTORY,
+
+    /**
+     * Referenced Resource is UTF-8 text, rather than an external entity
+     */
+    TEXT,
+
+    /**
+     * Referenced Resource is a URL that uses the HTTP, HTTPS, or file protocol
+     * (i.e., <code>http://...</code>, <code>https://...</code>, or <code>file:...</code>)
+     */
+    URL;
+
+}
diff --git a/src/main/java/org/apache/nifi/flow/resource/ExternalResourceConflictResolutionStrategy.java b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceConflictResolutionStrategy.java
new file mode 100644
index 0000000..85f781d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceConflictResolutionStrategy.java
@@ -0,0 +1,32 @@
+/*
+ * 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.flow.resource;
+
+import java.io.File;
+
+/**
+ * Might be used by provider service in order to decide if an available resource should be acquired from the external provider.
+ */
+public interface ExternalResourceConflictResolutionStrategy {
+    /**
+     * @param targetDirectory The target directory.
+     * @param available Descriptor of the available resource.
+     *
+     * @return Returns true if the resource should be fetched from the provider. Returns false otherwise.
+     */
+    boolean shouldBeFetched(File targetDirectory, ExternalResourceDescriptor available);
+}
diff --git a/src/main/java/org/apache/nifi/flow/resource/ExternalResourceDescriptor.java b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceDescriptor.java
new file mode 100644
index 0000000..ed832c8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceDescriptor.java
@@ -0,0 +1,33 @@
+/*
+ * 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.flow.resource;
+
+/**
+ * Describes an available resource might be fetched from the external source.
+ */
+public interface ExternalResourceDescriptor {
+
+    /**
+     * @return The location of the resource, where the format depends on the actual provider implementation.
+     */
+    String getLocation();
+
+    /**
+     * @return Returns the modification time of the original resource file using Unix timestamp format.
+     */
+    long getLastModified();
+}
diff --git a/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProvider.java b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProvider.java
new file mode 100644
index 0000000..7cf7c96
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProvider.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.flow.resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+/**
+ * Represents an external source where the resource files might be acquired from. These external resources might be
+ * various: database drivers, different kind of configurations and so on.
+ */
+public interface ExternalResourceProvider {
+
+    /**
+     * Initializes the External Resource Provider based on the given set of properties.
+     *
+     * @param context Initialization context for the provider containing configuration properties
+     */
+    void initialize(ExternalResourceProviderInitializationContext context);
+
+    /**
+     * Performs a listing of all resources that are available.
+     *
+     * @return The result is a list of descriptors for the available resources.
+     * @throws IOException Thrown on failure to list resources
+     */
+    Collection<ExternalResourceDescriptor> listResources() throws IOException;
+
+    /**
+     * Fetches the resource determined by the descriptor. This usually happens based on {@code ExternalResourceDescriptor#getLocation}
+     * but implementations might differ.
+     *
+     * @param descriptor External Resource reference to be fetched
+     * @return Input Stream for external resource fetched
+     * @throws IOException Thrown on failure to fetch resources
+     */
+    InputStream fetchExternalResource(ExternalResourceDescriptor descriptor) throws IOException;
+}
diff --git a/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProviderInitializationContext.java b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProviderInitializationContext.java
new file mode 100644
index 0000000..aafca70
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/resource/ExternalResourceProviderInitializationContext.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.flow.resource;
+
+import javax.net.ssl.SSLContext;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * Contains necessary information for extensions of external resource provider functionality.
+ */
+public interface ExternalResourceProviderInitializationContext {
+
+    /**
+     * @return Returns with the available properties.
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * @return A predicate, which might filter out unwanted files from the external source during listing. If no filtering
+     * necessary, then the predicate should return {@code true} for every call.
+     */
+    default Predicate<ExternalResourceDescriptor> getFilter() {
+        return (descriptor) -> true;
+    }
+
+    /**
+     * @return Returns an SSLContext.
+     */
+    SSLContext getSSLContext();
+}
diff --git a/src/main/java/org/apache/nifi/flow/resource/ImmutableExternalResourceDescriptor.java b/src/main/java/org/apache/nifi/flow/resource/ImmutableExternalResourceDescriptor.java
new file mode 100644
index 0000000..f6d3e1a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flow/resource/ImmutableExternalResourceDescriptor.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.flow.resource;
+
+import java.util.Objects;
+
+public class ImmutableExternalResourceDescriptor implements ExternalResourceDescriptor {
+    private final String location;
+    private final long modifiedAt;
+
+    public ImmutableExternalResourceDescriptor(final String location, final long modifiedAt) {
+        if (modifiedAt < 0) {
+            throw new IllegalArgumentException("The modification time cannot be negative");
+        }
+
+        this.location = Objects.requireNonNull(location);
+        this.modifiedAt = modifiedAt;
+    }
+
+    @Override
+    public String getLocation() {
+        return location;
+    }
+
+    @Override
+    public long getLastModified() {
+        return modifiedAt;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java b/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java
new file mode 100644
index 0000000..e1eb61c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/AbstractAnalysisResult.java
@@ -0,0 +1,73 @@
+/*
+ * 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.flowanalysis;
+
+import java.util.StringJoiner;
+
+/**
+ * Abstract class holding information about a {@link FlowAnalysisRule} violation.
+ */
+public abstract class AbstractAnalysisResult {
+    protected final String issueId;
+    protected final String message;
+    protected final String explanation;
+
+    protected AbstractAnalysisResult(final String issueId, final String message, final String explanation) {
+        this.issueId = issueId;
+        this.message = message;
+        this.explanation = explanation;
+    }
+
+    /**
+     * A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     * Newer analysis runs may produce a result with the same issueId in which case the old one will
+     * be overwritten (or recreated if it is the same in other aspects as well).
+     * However, if the previous result was disabled the new one will be disabled as well.
+     *
+     * @return Issue Identifier
+     */
+    public String getIssueId() {
+        return issueId;
+    }
+
+    /**
+     * Get message associated with results
+     *
+     * @return the rule violation message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * Get explanation associated with results
+     *
+     * @return a detailed explanation of the nature of the violation
+     */
+    public String getExplanation() {
+        return explanation;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
+            .add("issueId='" + issueId + "'")
+            .add("message='" + message + "'")
+            .add("explanation='" + explanation + "'")
+            .toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.java b/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.java
new file mode 100644
index 0000000..e1b2ae9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/AbstractFlowAnalysisRule.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.flowanalysis;
+
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.reporting.InitializationException;
+
+public abstract class AbstractFlowAnalysisRule extends AbstractConfigurableComponent implements FlowAnalysisRule {
+    private String identifier;
+    private String description;
+
+    private ComponentLog logger;
+
+    @Override
+    public void initialize(FlowAnalysisRuleInitializationContext context) throws InitializationException {
+        identifier = context.getIdentifier();
+        description = getClass().getSimpleName() + "[id=" + identifier + "]";
+        logger = context.getLogger();
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String toString() {
+        return description;
+    }
+
+    /**
+     * @return the logger that has been provided to the component by the
+     * framework in its initialize method
+     */
+    protected ComponentLog getLogger() {
+        return logger;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.java b/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.java
new file mode 100644
index 0000000..047c434
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/ComponentAnalysisResult.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.flowanalysis;
+
+/**
+ * Holds information about a component violating a {@link FlowAnalysisRule}
+ */
+public class ComponentAnalysisResult extends AbstractAnalysisResult {
+    /**
+     * @param issueId A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                be overwritten (or recreated if it is the same in other aspects as well).
+     *                However, if the previous result was disabled the new one will be disabled as well.
+     * @param message A violation message
+     */
+    public ComponentAnalysisResult(final String issueId, final String message) {
+        this(issueId, message, null);
+    }
+
+    /**
+     * @param issueId A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                be overwritten (or recreated if it is the same in other aspects as well).
+     *                However, if the previous result was disabled the new one will be disabled as well.
+     * @param message A violation message
+     * @param explanation A detailed explanation of the violation
+     */
+    public ComponentAnalysisResult(final String issueId, final String message, final String explanation) {
+        super(issueId, message, explanation);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java b/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java
new file mode 100644
index 0000000..c1cda44
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/EnforcementPolicy.java
@@ -0,0 +1,29 @@
+/*
+ * 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.flowanalysis;
+
+public enum EnforcementPolicy {
+    /**
+     * Rules with this enforcement policy only warns about rule violations.
+     */
+    WARN,
+    /**
+     * Rules with this enforcement policy also invalidate the corresponding components and fixing
+     *  these problems are to be considered mandatory.
+     */
+    ENFORCE;
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java
new file mode 100644
index 0000000..330cba8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisContext.java
@@ -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.flowanalysis;
+
+import org.apache.nifi.controller.VersionedControllerServiceLookup;
+
+import java.util.Optional;
+
+/**
+ * Used for accessing flow- or other analysis-related information
+ */
+public interface FlowAnalysisContext {
+    /**
+     * @return the {@link VersionedControllerServiceLookup} which can be used to obtain
+     * Versioned Controller Services during flow analysis
+     */
+    VersionedControllerServiceLookup getVersionedControllerServiceLookup();
+
+    /**
+     * @return the currently configured maximum number of threads that can be
+     * used for executing processors at any given time.
+     */
+    int getMaxTimerDrivenThreadCount();
+
+    /**
+     * @return <code>true</code> if this instance of NiFi is configured to be part of a cluster, <code>false</code>
+     * if this instance of NiFi is a standalone instance
+     */
+    boolean isClustered();
+
+    /**
+     * @return an Optional with the ID of this node in the cluster, or empty if either this node is not clustered or the Node Identifier
+     * has not yet been established
+     */
+    Optional<String> getClusterNodeIdentifier();
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.java b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.java
new file mode 100644
index 0000000..78aaf5a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRule.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.flowanalysis;
+
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A single rule that can analyze components or a flow (represented by a process group)
+ */
+public interface FlowAnalysisRule extends ConfigurableComponent {
+    /**
+     * Provides the Flow Analysis Rule with access to objects that may be of use
+     * throughout its lifecycle
+     *
+     * @param context see {@link FlowAnalysisRuleInitializationContext}
+     * @throws org.apache.nifi.reporting.InitializationException if unable to initialize
+     */
+    void initialize(FlowAnalysisRuleInitializationContext context) throws InitializationException;
+
+    /**
+     * Analyze a component provided by the framework.
+     * This is a callback method invoked by the framework.
+     * It should be expected that this method will be called with any and all available components.
+     *
+     * @param component the component to be analyzed
+     * @param context   see {@link FlowAnalysisRuleContext}
+     * @return a collection of {@link ComponentAnalysisResult} as the result of the analysis of the given component
+     */
+    default Collection<ComponentAnalysisResult> analyzeComponent(VersionedComponent component, FlowAnalysisRuleContext context) {
+        return Collections.emptySet();
+    }
+
+    /**
+     * Analyze a flow or a part of it, represented by a process group.
+     * This is a callback method invoked by the framework.
+     * It should be expected that this method will be called by the root process group and all of its child process groups.
+     * In case a flow analysis is requested for a particular process group this method will be called for all it's child
+     * process groups as well.
+     *
+     * @param processGroup the process group to be analyzed
+     * @param context      see {@link FlowAnalysisRuleContext}
+     * @return a collection of {@link GroupAnalysisResult} as the result of the analysis.
+     * One {@link GroupAnalysisResult} in the collection can either refer to a component within the analyzed process group,
+     * to a child process group or the entirety of the process group
+     */
+    default Collection<GroupAnalysisResult> analyzeProcessGroup(VersionedProcessGroup processGroup, FlowAnalysisRuleContext context) {
+        return Collections.emptySet();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.java b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.java
new file mode 100644
index 0000000..06e905f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleContext.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.flowanalysis;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.context.PropertyContext;
+
+import java.util.Map;
+
+/**
+ * This interface provides a bridge between the NiFi Framework and a
+ * {@link FlowAnalysisRule}. This context allows a FlowAnalysisRule to access
+ * configuration supplied by the user.
+ */
+public interface FlowAnalysisRuleContext extends PropertyContext {
+    /**
+     * @return the name of the rule that is being triggered
+     */
+    String getRuleName();
+
+    /**
+     * @return a Map of all known {@link PropertyDescriptor}s to their
+     * configured properties. This Map will contain a <code>null</code> for any
+     * Property that has not been configured by the user, even if the
+     * PropertyDescriptor has a default value
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this component
+     */
+    StateManager getStateManager();
+
+    /**
+     * @return a FlowAnalysisContext that can be used to access flow- or other analysis-related information
+     */
+    FlowAnalysisContext getFlowAnalysisContext();
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java
new file mode 100644
index 0000000..08c375e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleInitializationContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+
+/**
+ * Provides configuration information to a
+ * FlowAnalysisRule at the time of initialization
+ */
+public interface FlowAnalysisRuleInitializationContext extends KerberosContext {
+
+    /**
+     * @return the identifier for the FlowAnalysisRule
+     */
+    String getIdentifier();
+
+    /**
+     * @return a logger that can be used to log important events in a standard
+     * way and generate bulletins when appropriate
+     */
+    ComponentLog getLogger();
+
+    /**
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     * @since Apache NiFi 1.5.0
+     */
+    NodeTypeProvider getNodeTypeProvider();
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java
new file mode 100644
index 0000000..79731ba
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/FlowAnalysisRuleState.java
@@ -0,0 +1,22 @@
+/*
+ * 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.flowanalysis;
+
+public enum FlowAnalysisRuleState {
+    ENABLED,
+    DISABLED;
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java b/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java
new file mode 100644
index 0000000..5366bf7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/GroupAnalysisResult.java
@@ -0,0 +1,112 @@
+/*
+ * 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.flowanalysis;
+
+import org.apache.nifi.flow.VersionedComponent;
+
+import java.util.Optional;
+import java.util.StringJoiner;
+
+/**
+ * Holds information about a {@link FlowAnalysisRule} violation after analyzing (a part of) the flow, represented by a process group.
+ * One such analysis can result in multiple instances of this class.
+ */
+public class GroupAnalysisResult extends AbstractAnalysisResult {
+    private final Optional<VersionedComponent> component;
+
+    private GroupAnalysisResult(final String issueId, final String message, final String explanation, final Optional<VersionedComponent> component) {
+        super(issueId, message, explanation);
+        this.component = component;
+    }
+
+    /**
+     * @return the component this result corresponds to or empty if this result corresponds to the entirety of the process group that was analyzed
+     */
+    public Optional<VersionedComponent> getComponent() {
+        return component;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]")
+                .add("issueId='" + issueId + "'")
+                .add("message='" + message + "'")
+                .add("explanation='" + explanation + "'")
+                .add("component='" + component + "'")
+                .toString();
+    }
+
+    /**
+     * Build a new analysis result tied to the currently analyzed process group
+     *
+     * @param issueId     A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                    Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                    be overwritten (or recreated if it is the same in other aspects as well).
+     *                    However, if the previous result was disabled the new one will be disabled as well.
+     * @param message     A violation message
+     * @return a Builder for a new analysis result instance tied to the currently analyzed process group
+     */
+    public static Builder forGroup(final String issueId, final String message) {
+        return new Builder(null, issueId, message);
+    }
+
+    /**
+     * Build a new analysis result tied to a component.
+     * Note that the result will be scoped to the process group of the component and not the currently analyzed group.
+     * This means that even when a new analysis is run against that process group, this result will become obsolete.
+     *
+     * @param component The component that this result is tied to
+     * @param issueId   A rule-defined id that corresponds to a unique type of issue recognized by the rule.
+     *                  Newer analysis runs may produce a result with the same issueId in which case the old one will
+     *                  be overwritten (or recreated if it is the same in other aspects as well).
+     *                  However, if the previous result was disabled the new one will be disabled as well.
+     * @param message   A violation message
+     * @return a Builder for a new analysis result tied to a component
+     */
+    public static Builder forComponent(final VersionedComponent component, final String issueId, final String message) {
+        return new Builder(component, issueId, message);
+    }
+
+    public static class Builder {
+        private final VersionedComponent component;
+        private final String issueId;
+        private final String message;
+        private String explanation;
+
+        private Builder(final VersionedComponent component, final String issueId, final String message) {
+            this.component = component;
+            this.issueId = issueId;
+            this.message = message;
+        }
+
+        /**
+         * @param explanation A detailed explanation of the violation
+         * @return this Builder
+         */
+        public Builder explanation(final String explanation) {
+            this.explanation = explanation;
+            return this;
+        }
+
+        /**
+         * @return the flow analysis result
+         */
+        public GroupAnalysisResult build() {
+            return new GroupAnalysisResult(issueId, message, explanation, Optional.ofNullable(component));
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.java b/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.java
new file mode 100644
index 0000000..aa6b08e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowanalysis/VerifiableFlowAnalysisRule.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.flowanalysis;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.util.List;
+
+/**
+ * <p>
+ *     Any Flow Analysis Rule that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Flow Analysis Rule. This allows the Flow Analysis Rule to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Flow Analysis Rule is not valid, it cannot be started. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when starting a component).
+ * </p>
+ *
+ * <p>
+ *     Verification is allowed to be run only when a Flow Analysis Rule is fully stopped. I.e., it has no active threads
+ *     and currently has a state of STOPPED. Therefore, any initialization logic that may need to be performed
+ *     before the Flow Analysis Rule is triggered may also be required for verification. However, the framework is not responsible
+ *     for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ConfigurationContext, ComponentLog)} itself.
+ *     The {@link #verify(ConfigurationContext, ComponentLog)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableFlowAnalysisRule {
+
+    /**
+     * Verifies that the configuration defined by the given ConfigurationContext is valid.
+     *
+     * @param context the Configuration Context that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     *
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ConfigurationContext context, ComponentLog verificationLogger);
+
+}
diff --git a/src/main/java/org/apache/nifi/flowfile/FlowFile.java b/src/main/java/org/apache/nifi/flowfile/FlowFile.java
new file mode 100644
index 0000000..ef5f61e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowfile/FlowFile.java
@@ -0,0 +1,133 @@
+/*
+ * 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.flowfile;
+
+import java.util.Map;
+
+/**
+ * <p>
+ * A flow file is a logical notion of an item in a flow with its associated
+ * attributes and identity which can be used as a reference for its actual
+ * content.</p>
+ *
+ * <b>All FlowFile implementations must be Immutable - Thread safe.</b>
+ */
+public interface FlowFile extends Comparable<FlowFile> {
+
+    /**
+     * @return the unique identifier for this flow file which is guaranteed
+     * to be unique within a single running instance of nifi.  This identifier
+     * should not be used for true universal unique type needs.  For that consider
+     * using the attribute found in the flow file's attribute map keyed by
+     * CoreAttributes.UUID.
+     * For example, by calling getAttribute(CoreAttributes.UUID.key()).
+     */
+    long getId();
+
+    /**
+     * @return the date at which the flow file entered the flow
+     */
+    long getEntryDate();
+
+    /**
+     * @return the date at which the origin of this FlowFile entered the flow.
+     * For example, if FlowFile Z were derived from FlowFile Y and FlowFile Y
+     * was derived from FlowFile X, this date would be the entryDate (see
+     * {@link #getEntryDate()} of FlowFile X.
+     */
+    long getLineageStartDate();
+
+    /**
+     * Returns a 64-bit integer that indicates the order in which the FlowFile was added to the
+     * flow with respect to other FlowFiles that have the same last lineage start date.
+     * I.e., if two FlowFiles return the same value for {@link #getLineageStartDate()}, the order
+     * in which those FlowFiles were added to the flow can be determined by looking at the result of
+     * this method. However, no guarantee is made by this method about the ordering of FlowFiles
+     * that have different values for the {@link #getLineageStartDate()} method.
+     *
+     * @return the index that can be used to compare two FlowFiles with the same lineage start date
+     * to understand the order in which the two FlowFiles were enqueued.
+     */
+    long getLineageStartIndex();
+
+    /**
+     * @return the time at which the FlowFile was most recently added to a
+     * FlowFile queue, or {@code null} if the FlowFile has never been enqueued.
+     * This value will always be populated before it is passed to a
+     * FlowFilePrioritizer
+     */
+    Long getLastQueueDate();
+
+    /**
+     * Returns a 64-bit integer that indicates the order in which the FlowFile was added to the
+     * FlowFile queue with respect to other FlowFiles that have the same last queue date.
+     * I.e., if two FlowFiles return the same value for {@link #getLastQueueDate()}, the order
+     * in which those FlowFiles were enqueued can be determined by looking at the result of
+     * this method. However, no guarantee is made by this method about the ordering of FlowFiles
+     * that have different values for the {@link #getLastQueueDate()} method.
+     *
+     * @return the index that can be used to compare two FlowFiles with the same last queue date
+     * to understand the order in which the two FlowFiles were enqueued.
+     */
+    long getQueueDateIndex();
+
+    /**
+     * @return true if flow file is currently penalized; false otherwise;
+     */
+    boolean isPenalized();
+
+    /**
+     * Obtains the attribute value for the given key
+     *
+     * @param key of the attribute
+     * @return value if found; null otherwise
+     */
+    String getAttribute(String key);
+
+    /**
+     * @return size of flow file contents in bytes
+     */
+    long getSize();
+
+    /**
+     * @return an unmodifiable map of the flow file attributes
+     */
+    Map<String, String> getAttributes();
+
+    class KeyValidator {
+
+        private KeyValidator() {
+
+        }
+
+        public static String validateKey(final String key) {
+            // We used to validate the key by disallowing a handful of keywords, but this requirement no longer exists.
+            // Therefore this method simply verifies that the key is not empty.
+            if (key == null) {
+                throw new IllegalArgumentException("Invalid attribute key: null");
+            }
+            if (key.trim().isEmpty()) {
+                throw new IllegalArgumentException("Invalid attribute key: <Empty String>");
+            }
+            return key;
+        }
+
+        public static boolean isValid(final String key) {
+            return key != null && !key.trim().isEmpty();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/flowfile/attributes/CoreAttributes.java b/src/main/java/org/apache/nifi/flowfile/attributes/CoreAttributes.java
new file mode 100644
index 0000000..65b41b2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowfile/attributes/CoreAttributes.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.flowfile.attributes;
+
+public enum CoreAttributes implements FlowFileAttributeKey {
+
+    /**
+     * The FlowFile's path indicates the relative directory to which a FlowFile belongs and does not contain the filename
+     */
+    PATH("path"),
+    /**
+     * The FlowFile's absolute path indicates the absolute directory to which a FlowFile belongs and does not contain the filename
+     */
+    ABSOLUTE_PATH("absolute.path"),
+    /**
+     * The filename of the FlowFile. The filename should not contain any directory structure.
+     */
+    FILENAME("filename"),
+    /**
+     * A unique UUID assigned to this FlowFile
+     */
+    UUID("uuid"),
+    /**
+     * A numeric value indicating the FlowFile priority
+     */
+    PRIORITY("priority"),
+    /**
+     * The MIME Type of this FlowFile
+     */
+    MIME_TYPE("mime.type"),
+    /**
+     * Specifies the reason that a FlowFile is being discarded
+     */
+    DISCARD_REASON("discard.reason"),
+    /**
+     * Indicates an identifier other than the FlowFile's UUID that is known to refer to this FlowFile.
+     */
+    ALTERNATE_IDENTIFIER("alternate.identifier");
+
+    private final String key;
+
+    CoreAttributes(final String key) {
+        this.key = key;
+    }
+
+    @Override
+    public String key() {
+        return key;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/flowfile/attributes/FlowFileAttributeKey.java b/src/main/java/org/apache/nifi/flowfile/attributes/FlowFileAttributeKey.java
new file mode 100644
index 0000000..9637631
--- /dev/null
+++ b/src/main/java/org/apache/nifi/flowfile/attributes/FlowFileAttributeKey.java
@@ -0,0 +1,22 @@
+/*
+ * 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.flowfile.attributes;
+
+public interface FlowFileAttributeKey {
+
+    String key();
+}
diff --git a/src/main/java/org/apache/nifi/kerberos/KerberosContext.java b/src/main/java/org/apache/nifi/kerberos/KerberosContext.java
new file mode 100644
index 0000000..a30e7cd
--- /dev/null
+++ b/src/main/java/org/apache/nifi/kerberos/KerberosContext.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.kerberos;
+
+import java.io.File;
+
+public interface KerberosContext {
+
+    /**
+     * The Kerberos service principal used by NiFi to communicate with the KDC
+     * in order to obtain tickets on behalf of NiFi. Typically of the form
+     * NIFI/fully.qualified.domain@REALM.
+     *
+     * @return the principal, or null if this NiFi instance is not configured
+     * with a NiFi Kerberos service principal
+     */
+    String getKerberosServicePrincipal();
+
+    /**
+     * The File instance for the Kerberos service keytab. The service principal
+     * and service keytab will be used to communicate with the KDC to obtain
+     * tickets on behalf of NiFi.
+     *
+     * @return the File instance of the service keytab, or null if this NiFi
+     * instance is not configured with a NiFi Kerberos service keytab
+     */
+    File getKerberosServiceKeytab();
+
+    /**
+     * The Kerberos configuration file (typically krb5.conf) that will be used
+     * by this JVM during all Kerberos operations.
+     *
+     * @return the File instance for the Kerberos configuration file, or null if
+     * this NiFi instance is not configured with a Kerberos configuration file
+     */
+    File getKerberosConfigurationFile();
+}
diff --git a/src/main/java/org/apache/nifi/logging/ComponentLog.java b/src/main/java/org/apache/nifi/logging/ComponentLog.java
new file mode 100644
index 0000000..db03fd7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/logging/ComponentLog.java
@@ -0,0 +1,220 @@
+/*
+ * 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.logging;
+
+/**
+ * <p>
+ * The ComponentLog provides a mechanism to ensure that all NiFi components are
+ * logging and reporting information in a consistent way. When messages are
+ * logged to the ComponentLog, each message has the following characteristics:
+ * </p>
+ *
+ * <ul>
+ * <li>
+ * The <code>toString()</code> of the component is automatically prepended to
+ * the message so that it is clear which component is providing the information.
+ * This is important, since a single component may have many different instances
+ * within the same NiFi instance.
+ * </li>
+ * <li>
+ * If the last value in an Object... argument that is passed to the logger is a
+ * Throwable, then the logged message will include a <code>toString()</code> of
+ * the Throwable; in addition, if the component's logger is set to DEBUG level
+ * via the logback configuration, the Stacktrace will also be logged. This
+ * provides a mechanism to easily enable stacktraces in the logs when they are
+ * desired without filling the logs with unneeded stack traces for messages that
+ * end up occurring often.
+ * </li>
+ * <li>
+ * Any message that is logged with a Severity level that meets or exceeds the
+ * configured Bulletin Level for that component will also cause a Bulletin to be
+ * generated, so that the message is visible in the UI, allowing Dataflow
+ * Managers to understand that a problem exists and what the issue is.
+ * </li>
+ * </ul>
+ */
+public interface ComponentLog {
+
+    void warn(String msg, Throwable t);
+
+    void warn(String msg, Object... os);
+
+    void warn(String msg);
+
+    default void warn(LogMessage logMessage) {
+        log(LogLevel.WARN, logMessage);
+    }
+
+    void trace(String msg, Throwable t);
+
+    void trace(String msg, Object... os);
+
+    void trace(String msg);
+
+    default void trace(LogMessage logMessage) {
+        log(LogLevel.TRACE, logMessage);
+    }
+
+    boolean isWarnEnabled();
+
+    boolean isTraceEnabled();
+
+    boolean isInfoEnabled();
+
+    boolean isErrorEnabled();
+
+    boolean isDebugEnabled();
+
+    void info(String msg, Throwable t);
+
+    void info(String msg, Object... os);
+
+    void info(String msg);
+
+    default void info(LogMessage logMessage) {
+        log(LogLevel.INFO, logMessage);
+    }
+
+    String getName();
+
+    void error(String msg, Throwable t);
+
+    void error(String msg, Object... os);
+
+    void error(String msg);
+
+    default void error(LogMessage logMessage) {
+        log(LogLevel.ERROR, logMessage);
+    }
+
+    void debug(String msg, Throwable t);
+
+    void debug(String msg, Object... os);
+
+    void debug(String msg);
+
+    default void debug(LogMessage logMessage) {
+        log(LogLevel.ERROR, logMessage);
+    }
+
+    default void log(LogLevel level, String msg, Throwable t) {
+        switch (level) {
+            case DEBUG:
+                debug(msg, t);
+                break;
+            case ERROR:
+            case FATAL:
+                error(msg, t);
+                break;
+            case INFO:
+                info(msg, t);
+                break;
+            case TRACE:
+                trace(msg, t);
+                break;
+            case WARN:
+            default:
+                warn(msg, t);
+                break;
+        }
+    }
+
+    default void log(LogLevel level, String msg, Object... os) {
+        switch (level) {
+            case DEBUG:
+                debug(msg, os);
+                break;
+            case ERROR:
+            case FATAL:
+                error(msg, os);
+                break;
+            case INFO:
+                info(msg, os);
+                break;
+            case TRACE:
+                trace(msg, os);
+                break;
+            case WARN:
+            default:
+                warn(msg, os);
+                break;
+        }
+    }
+
+    default void log(LogLevel level, String msg) {
+        switch (level) {
+            case DEBUG:
+                debug(msg);
+                break;
+            case ERROR:
+            case FATAL:
+                error(msg);
+                break;
+            case INFO:
+                info(msg);
+                break;
+            case TRACE:
+                trace(msg);
+                break;
+            case WARN:
+            default:
+                warn(msg);
+                break;
+        }
+    }
+
+    default void log(LogMessage message) {
+        switch (message.getLogLevel()) {
+            case DEBUG:
+                debug(message);
+                break;
+            case ERROR:
+            case FATAL:
+                error(message);
+                break;
+            case INFO:
+                info(message);
+                break;
+            case TRACE:
+                trace(message);
+                break;
+            case WARN:
+            default:
+                warn(message);
+                break;
+        }
+    }
+
+    default void log(LogLevel level, LogMessage logMessage) {
+        String msg = logMessage.getMessage();
+        Throwable t = logMessage.getThrowable();
+        Object[] os = logMessage.getObjects();
+
+        if (os != null && t != null) {
+            Object[] ost = new Object[os.length + 1];
+            System.arraycopy(os, 0, ost, 0, os.length);
+            ost[ost.length - 1] = t;
+            log(level, msg, ost);
+        } else if (os != null) {
+            log(level, msg, os);
+        } else if (t != null) {
+            log(level, msg, t);
+        } else {
+            log(level, msg);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/logging/LogLevel.java b/src/main/java/org/apache/nifi/logging/LogLevel.java
new file mode 100644
index 0000000..c063bf8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/logging/LogLevel.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.logging;
+
+public enum LogLevel {
+
+    TRACE,
+    DEBUG,
+    INFO,
+    WARN,
+    ERROR,
+    FATAL,
+    NONE;
+}
diff --git a/src/main/java/org/apache/nifi/logging/LogMessage.java b/src/main/java/org/apache/nifi/logging/LogMessage.java
new file mode 100644
index 0000000..bae8154
--- /dev/null
+++ b/src/main/java/org/apache/nifi/logging/LogMessage.java
@@ -0,0 +1,123 @@
+/*
+ * 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.logging;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+
+public class LogMessage {
+
+    private final long time;
+    private final String message;
+    private final LogLevel logLevel;
+    private final Throwable throwable;
+    private final String flowFileUuid;
+    private final Object[] objects;
+
+    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
+    private static final String TO_STRING_FORMAT = "%1$s %2$s - %3$s";
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
+
+    public static class Builder {
+
+        private final long time;
+        private final LogLevel logLevel;
+        private String message;
+        private Throwable throwable;
+        private String flowFileUuid;
+        private Object[] objects;
+
+        public Builder(final long time, final LogLevel logLevel) {
+            this.time = time;
+            this.logLevel = logLevel;
+        }
+
+        public Builder message(String message) {
+            this.message = message;
+            return this;
+        }
+
+        public Builder throwable(Throwable throwable) {
+            this.throwable = throwable;
+            return this;
+        }
+
+        public Builder flowFileUuid(String flowFileUuid) {
+            this.flowFileUuid = flowFileUuid;
+            return this;
+        }
+
+        public Builder objects(Object[] objects) {
+            this.objects = objects;
+            return this;
+        }
+
+        public LogMessage createLogMessage() {
+            return new LogMessage(time, logLevel, message, throwable, flowFileUuid, objects);
+        }
+    }
+
+    private LogMessage(final long time, final LogLevel logLevel, final String message, final Throwable throwable, final String flowFileUuid, final Object[] objects) {
+        this.logLevel = logLevel;
+        this.throwable = throwable;
+        this.message = message;
+        this.time = time;
+        this.flowFileUuid = flowFileUuid;
+        this.objects = objects;
+    }
+
+    public long getTime() {
+        return time;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public LogLevel getLogLevel() {
+        return logLevel;
+    }
+
+    public Throwable getThrowable() {
+        return throwable;
+    }
+
+    public String getFlowFileUuid() {
+        return flowFileUuid;
+    }
+
+    public Object[] getObjects() {
+        return objects;
+    }
+
+    @Override
+    public String toString() {
+        final String formattedTime = DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(time));
+
+        String formattedMsg = String.format(TO_STRING_FORMAT, formattedTime, logLevel.toString(), message);
+        if (throwable != null) {
+            final StringWriter sw = new StringWriter();
+            final PrintWriter pw = new PrintWriter(sw);
+            throwable.printStackTrace(pw);
+            formattedMsg += System.lineSeparator() + sw;
+        }
+
+        return formattedMsg;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/migration/PropertyConfiguration.java b/src/main/java/org/apache/nifi/migration/PropertyConfiguration.java
new file mode 100644
index 0000000..e6e2cc9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/migration/PropertyConfiguration.java
@@ -0,0 +1,210 @@
+/*
+ * 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.migration;
+
+import org.apache.nifi.components.PropertyDescriptor;
+
+import java.util.Map;
+import java.util.Optional;
+
+public interface PropertyConfiguration {
+
+    /**
+     * Renames an existing property, if it exists. If the property does not exist, this is a no-op
+     *
+     * @param propertyName the current name of the property
+     * @param newName the new name for the property
+     * @return <code>true</code> if the property was renamed, <code>false</code> if the property did not exist
+     */
+    boolean renameProperty(String propertyName, String newName);
+
+    /**
+     * Removes the property with the given name, if it exists. If the property does not exist, this is a no-op.
+     * @param propertyName the name of the property to remove
+     * @return <code>true</code> if the property was removed, <code>false</code> if the property did not exist
+     */
+    boolean removeProperty(String propertyName);
+
+    /**
+     * <p>
+     *     Determines whether or not the configuration has an entry for the given property. This method will return <code>true</code>
+     *     even if the value for the given property is <code>null</code>. A value of <code>false</code> will be returned only if the
+     *     property name is not known to the configuration. This allows for disambiguation between a value that is configured to be
+     *     <code>null</code> and a value that has not yet been configured.
+     * </p>
+     * <p>
+     *     An idiom to determine if the property was explicitly set to <code>null</code> is as follows:
+     * </p>
+     * <pre><code>
+     *     final boolean setToNull = configuration.hasProperty("MyProperty") &amp;&amp; !configuration.isPropertySet("MyProperty");
+     * </code></pre>
+     *
+     * @param propertyName the name of the property
+     * @return <code>true</code> if the property name is known to this configuration, <code>false</code> otherwise.
+     */
+    boolean hasProperty(String propertyName);
+
+    /**
+     * <p>
+     *     Determines whether or not the configuration has an entry for the given property. This method will return <code>true</code>
+     *     even if the value for the given property is <code>null</code>. A value of <code>false</code> will be returned only if the
+     *     property identified by the descriptor is not known to the configuration. This allows for disambiguation between a value that is configured to be
+     *     <code>null</code> and a value that has not yet been configured.
+     * </p>
+     * <p>
+     *     An idiom to determine if the property was explicitly set to <code>null</code> is as follows:
+     * </p>
+     * <pre><code>
+     *     final boolean setToNull = configuration.hasProperty(MY_PROPERTY) &amp;&amp; !configuration.isSet(MY_PROPERTY);
+     * </code></pre>
+     *
+     * @param descriptor the property descriptor that identifies the property
+     * @return <code>true</code> if the property name is known to this configuration, <code>false</code> otherwise.
+     */
+    default boolean hasProperty(PropertyDescriptor descriptor) {
+        return hasProperty(descriptor.getName());
+    }
+
+    /**
+     * Indicates whether or not the property with the given name has been set to a non-null value
+     * @param propertyName the name of the property
+     * @return <code>true</code> if the value has been set and is non-null. Returns <code>false</code> if the value is unset or is null
+     */
+    boolean isPropertySet(String propertyName);
+
+    /**
+     * Indicates whether or not the property identified by the given descriptor name has been set to a non-null value
+     * @param descriptor the descriptor that identifies the property
+     * @return <code>true</code> if the value has been set and is non-null. Returns <code>false</code> if the value is unset or is null
+     */
+    default boolean isPropertySet(PropertyDescriptor descriptor) {
+        return isPropertySet(descriptor.getName());
+    }
+
+    /**
+     * Sets the value of the property with the given name and value
+     * @param propertyName the name of the property
+     * @param propertyValue the value to set
+     */
+    void setProperty(String propertyName, String propertyValue);
+
+    /**
+     * Sets the value of the property identified by the given descriptor's name to the given value
+     * @param descriptor the property descriptor that identifies the property
+     * @param propertyValue the value to set
+     */
+    default void setProperty(PropertyDescriptor descriptor, String propertyValue) {
+        setProperty(descriptor.getName(), propertyValue);
+    }
+
+    /**
+     * Returns an optional value representing the value of the property with the given name
+     * @param propertyName the name of the property
+     * @return an empty optional if the value is null or unset, else an Optional representing the configured value
+     */
+    Optional<String> getPropertyValue(String propertyName);
+
+    /**
+     * Returns an optional value representing the value of the property identified by the given descriptor
+     * @param descriptor the property descriptor that identifies the property
+     * @return an empty optional if the value is null or unset, else an Optional representing the configured value
+     */
+    default Optional<String> getPropertyValue(PropertyDescriptor descriptor) {
+        return getPropertyValue(descriptor.getName());
+    }
+
+    /**
+     * Returns an optional value representing the "raw" value of the property with the given name. The "raw" value is
+     * the value before any parameters are substituted.
+     *
+     * @param propertyName the name of the property
+     * @return an empty optional if the value is null or unset, else an Optional representing the configured value
+     */
+    Optional<String> getRawPropertyValue(String propertyName);
+
+    /**
+     * Returns an optional value representing the "raw" value of the property identified by the given descriptor. The "raw" value is
+     * the value before any parameters are substituted.
+     *
+     * @param descriptor the descriptor that identifies the property
+     * @return an empty optional if the value is null or unset, else an Optional representing the configured value
+     */
+    default Optional<String> getRawPropertyValue(PropertyDescriptor descriptor) {
+        return getRawPropertyValue(descriptor.getName());
+    }
+
+    /**
+     * Returns a map containing all of the configured properties
+     * @return a Map containing the names and values of all configured properties
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * Returns a map containing all of the raw property values
+     *
+     * @return a Map containing the names and values of all configured properties
+     */
+    Map<String, String> getRawProperties();
+
+    /**
+     * <p>
+     * Creates a new Controller Service of the given type and configures it with the given property values. Note that if a Controller Service
+     * already exists within the same scope and with the same implementation and configuration, a new service may not be created and instead
+     * the existing service may be used.
+     * </p>
+     *
+     * <p>
+     * This allows for properties that were previously defined in the extension to be moved to a Controller Service. For example,
+     * consider a Processor that has "Username" and "Password" properties. In the next version of the Processor, we want to support
+     * multiple types of authentication, and we delegate the authentication to a Controller Service. Consider that the Controller Service
+     * implementation we wish to use has a classname of {@code org.apache.nifi.services.authentication.UsernamePassword}. We might then
+     * use this method as such:
+     * </p>
+     *
+     * <pre><code>
+     *     // Create a new Controller Service of type org.apache.nifi.services.authentication.UsernamePassword whose Username and Password
+     *     // properties match those currently configured for this Processor.
+     *     final Map&lt;String, String&gt; serviceProperties = Map.of("Username", propertyConfiguration.getRawPropertyValue("Username"),
+     *          "Password", propertyConfiguration.getRawPropertyValue("Password"));
+     *     final String serviceId = propertyConfiguration.createControllerService("org.apache.nifi.services.authentication.UsernamePassword", serviceProperties);
+     *
+     *     // Set our Authentication Service property to point to this new service.
+     *     propertyConfiguration.setProperty(AUTHENTICATION_SERVICE, serviceId);
+     *
+     *     // Remove the Username and Password properties from this Processor, since we are now going to use then Authentication Service.
+     *     propertyConfiguration.removeProperty("Username");
+     *     propertyConfiguration.removeProperty("Password");
+     * </code></pre>
+     *
+     * <p>
+     * Note the use of {@link #getRawPropertyValue(String)} here instead of {@link #getPropertyValue(String)}. Because we want to set
+     * the new Controller Service's value to the same value as is currently configured for the Processor's "Username" and "Password" properties,
+     * we use {@link #getRawPropertyValue(String)}. This ensures that if the Processor is configured using Parameters, those Parameter
+     * references are still held by the Controller Service.
+     * </p>
+     *
+     * <p>
+     * Also note that this method expects the classname of the implementation, not the classname of the interface.
+     * </p>
+     *
+     * @param implementationClassName the fully qualified classname of the Controller Service implementation
+     * @param serviceProperties       the property values to configure the newly created Controller Service with
+     * @return an identifier for the Controller Service
+     */
+    String createControllerService(String implementationClassName, Map<String, String> serviceProperties);
+}
diff --git a/src/main/java/org/apache/nifi/migration/RelationshipConfiguration.java b/src/main/java/org/apache/nifi/migration/RelationshipConfiguration.java
new file mode 100644
index 0000000..e43aa64
--- /dev/null
+++ b/src/main/java/org/apache/nifi/migration/RelationshipConfiguration.java
@@ -0,0 +1,105 @@
+/*
+ * 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.migration;
+
+import org.apache.nifi.processor.Relationship;
+
+/**
+ * <p>
+ * RelationshipConfiguration can be used to convey how a Processor's configuration should be migrated from an old configuration
+ * to the latest configuration. This provides the ability to split a single Relationship into multiple, or to rename Relationships.
+ * </p>
+ * <p>
+ * Note that it does not provide for the ability to merge multiple Relationships into a single Relationship, as doing so would cause unexpected
+ * data duplication. Consider, for example, that a Processor has two Connections. The first has Relationship A while the second has Relationship B.
+ * If we allows A and B to be merged together into C, anything that gets transferred to C would now need to go to both of those Connections, creating
+ * data duplication.
+ * </p>
+ * <p>
+ * Additionally, there is no option to remove a Relationship. This is because if a Relationship is to be removed, it can be simply dropped from the Processor.
+ * Any existing connection that has that Relationship will be "ghosted" in the UI to show the user that it's no longer supported, but NiFi and the Processor
+ * will continue to behave as expected.
+ * </p>
+ */
+public interface RelationshipConfiguration {
+
+    /**
+     * Changes the Relationship from the given older name to the updated name
+     * @param relationshipName the old name of the relationship
+     * @param newName the new name to use
+     * @return <code>true</code> if the Relationship is renamed, <code>false</code> if the given Relationship does not exist
+     * @throws IllegalStateException if both the relationship name and the new name already are defined as relationships
+     */
+    boolean renameRelationship(String relationshipName, String newName);
+
+    /**
+     * <p>
+     * Splits the given Relationship into multiple new relationships. Each connection that has the Relationship as a selected Relationship will be updated to
+     * have all of the new Relationships as selected relationships instead. If the existing Relationship is auto-terminated, all of the newly defined ones
+     * will be as well. If the existing Relationship is auto-retried, so will be the new Relationships.
+     * </p>
+     *
+     * <p>
+     * It is possible to split an existing relationship into the same relationship and additional relationships. For example, it is
+     * valid to call this method as:
+     * </p>
+     * <pre><code>
+     * relationshipConfiguration.splitRelationship("A", "A", "B", "C");
+     * </code></pre>
+     *
+     * <p>
+     * In order to split the "A" relationship into three relationships: "A", "B", and "C". However, upon restart, NiFi will have already split the "A"
+     * relationship into three. So relationships "B" and "C" will already exist, resulting in an {@code IllegalStateException}. Therefore, if splitting
+     * a relationship into multiple that include the original relationship, it is important to guard against this by checking of the new relationships
+     * exist first:
+     * </p>
+     *
+     * <pre><code>
+     * if (!relationshipConfiguration.hasRelationship("B")) {
+     *     relationshipConfiguration.splitRelationship("A", "A", "B", "C");
+     * }
+     * </code></pre>
+     *
+     * <p>
+     * This ensures that we do not attempt to split relationship "A" if it has already been done.
+     * </p>
+     *
+     * @param relationshipName the name of the existing relationship
+     * @param newRelationshipName the first of the new relationship names
+     * @param additionalRelationshipNames additional names for the new relationship
+     * @return <code>true</code> if the Relationship is split, <code>false</code> if the given Relationship does not exist
+     * @throws IllegalStateException if the given relationship name exists and one of the given new relationships already exists
+     */
+    boolean splitRelationship(String relationshipName, String newRelationshipName, String... additionalRelationshipNames);
+
+    /**
+     * Indicates whether or not the relationship with the given name exists in the configuration
+     * @param relationshipName the name of the relationship
+     * @return <code>true</code> if the relationship exists, <code>false</code> if the relationship is not known
+     */
+    boolean hasRelationship(String relationshipName);
+
+    /**
+     * Indicates whether or not the relationship with the given name exists in the configuration
+     * @param relationship the relationship to check
+     * @return <code>true</code> if the relationship exists, <code>false</code> if the relationship is not known
+     */
+    default boolean hasRelationship(Relationship relationship) {
+        return hasRelationship(relationship.getName());
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/AbstractParameterProvider.java b/src/main/java/org/apache/nifi/parameter/AbstractParameterProvider.java
new file mode 100644
index 0000000..bcdeb4e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/AbstractParameterProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.reporting.InitializationException;
+
+public abstract class AbstractParameterProvider extends AbstractConfigurableComponent implements ParameterProvider {
+    private String identifier;
+    private String name;
+    private ComponentLog logger;
+    private NodeTypeProvider nodeTypeProvider;
+
+    @Override
+    public final void initialize(final ParameterProviderInitializationContext config) throws InitializationException {
+        identifier = config.getIdentifier();
+        logger = config.getLogger();
+        name = config.getName();
+        nodeTypeProvider = config.getNodeTypeProvider();
+        init(config);
+    }
+
+    /**
+     * @return the identifier of this Parameter Provider
+     */
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the {@link NodeTypeProvider} that was passed to the
+     * {@link #initialize(ParameterProviderInitializationContext)} method
+     */
+    protected final NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+
+    /**
+     * @return the name of this Parameter Provider
+     */
+    protected String getName() {
+        return name;
+    }
+
+    /**
+     * @return the logger that has been provided to the component by the
+     * framework in its initialize method
+     */
+    protected ComponentLog getLogger() {
+        return logger;
+    }
+
+    /**
+     * Provides a mechanism by which subclasses can perform initialization of
+     * the Parameter Provider before its parameters are fetched
+     *
+     * @param config context
+     * @throws InitializationException if failure to init
+     */
+    protected void init(final ParameterProviderInitializationContext config) throws InitializationException {
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/Parameter.java b/src/main/java/org/apache/nifi/parameter/Parameter.java
new file mode 100644
index 0000000..ff3030f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/Parameter.java
@@ -0,0 +1,179 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.asset.Asset;
+
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class Parameter {
+    private final ParameterDescriptor descriptor;
+    private final String value;
+    private final String parameterContextId;
+    private final boolean provided;
+    private final List<Asset> referencedAssets;
+
+    private Parameter(final Builder builder) {
+        this.descriptor = new ParameterDescriptor.Builder()
+            .name(builder.name)
+            .description(builder.description)
+            .sensitive(builder.sensitive)
+            .build();
+
+        this.parameterContextId = builder.parameterContextId;
+        this.provided = builder.provided;
+
+        this.referencedAssets = builder.referencedAssets;
+        if (this.referencedAssets == null || this.referencedAssets.isEmpty()) {
+            this.value = builder.value;
+        } else {
+            this.value = referencedAssets.stream()
+                .map(Asset::getFile)
+                .map(File::getAbsolutePath)
+                .collect(Collectors.joining(","));
+        }
+    }
+
+    public ParameterDescriptor getDescriptor() {
+        return descriptor;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public List<Asset> getReferencedAssets() {
+        return referencedAssets;
+    }
+
+    public String getParameterContextId() {
+        return parameterContextId;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final Parameter parameter = (Parameter) o;
+        return Objects.equals(descriptor, parameter.descriptor)
+               && Objects.equals(value, parameter.value)
+               && Objects.equals(parameterContextId, parameter.parameterContextId)
+               && Objects.equals(referencedAssets, parameter.referencedAssets);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(descriptor, value);
+    }
+
+    /**
+     *
+     * @return True if this parameter is provided by a ParameterProvider.
+     */
+    public boolean isProvided() {
+        return provided;
+    }
+
+    public static class Builder {
+        private String name;
+        private String description;
+        private boolean sensitive;
+        private String value;
+        private String parameterContextId;
+        private boolean provided;
+        private List<Asset> referencedAssets = List.of();
+
+        public Builder fromParameter(final Parameter parameter) {
+            descriptor(parameter.getDescriptor());
+            this.parameterContextId = parameter.getParameterContextId();
+            this.provided = parameter.isProvided();
+            this.referencedAssets = parameter.getReferencedAssets() == null ? List.of() : parameter.getReferencedAssets();
+            if (this.referencedAssets.isEmpty()) {
+                this.value = parameter.getValue();
+            }
+
+            return this;
+        }
+
+        public Builder descriptor(final ParameterDescriptor descriptor) {
+            this.name = descriptor.getName();
+            this.description = descriptor.getDescription();
+            this.sensitive = descriptor.isSensitive();
+            return this;
+        }
+
+        public Builder name(final String name) {
+            this.name = name;
+            return this;
+        }
+
+        public Builder description(final String description) {
+            this.description = description;
+            return this;
+        }
+
+        public Builder sensitive(final boolean sensitive) {
+            this.sensitive = sensitive;
+            return this;
+        }
+
+        public Builder value(final String value) {
+            this.value = value;
+            this.referencedAssets = List.of();
+            return this;
+        }
+
+        public Builder parameterContextId(final String parameterContextId) {
+            this.parameterContextId = parameterContextId;
+            return this;
+        }
+
+        public Builder provided(final Boolean provided) {
+            this.provided = provided != null && provided;
+            return this;
+        }
+
+        public Builder referencedAssets(final List<Asset> referencedAssets) {
+            this.referencedAssets = referencedAssets == null ? List.of() : referencedAssets;
+            if (!this.referencedAssets.isEmpty()) {
+                this.value = null;
+            }
+
+            return this;
+        }
+
+        public Parameter build() {
+            if (name == null) {
+                throw new IllegalStateException("Name or Descriptor is required");
+            }
+            if (value != null && referencedAssets != null && !referencedAssets.isEmpty()) {
+                throw new IllegalStateException("A Parameter's value or referenced assets may be set but not both");
+            }
+
+            return new Parameter(this);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterDescriptor.java b/src/main/java/org/apache/nifi/parameter/ParameterDescriptor.java
new file mode 100644
index 0000000..5f6e2f4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterDescriptor.java
@@ -0,0 +1,115 @@
+/*
+ * 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.parameter;
+
+import java.util.Objects;
+
+/**
+ * An immutable class that holds information about a Parameter. Parameters are constructed through use of the {@link ParameterDescriptor.Builder} class.
+ */
+public class ParameterDescriptor {
+    private final String name;
+    private final String description;
+    private final boolean sensitive;
+
+    private ParameterDescriptor(final Builder builder) {
+        this.name = builder.name;
+        this.description = builder.description;
+        this.sensitive = builder.sensitive;
+    }
+
+    /**
+     * @return the name of the parameter
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @return a description of the parameter
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * @return whether or not the parameter is considered sensitive.
+     */
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+
+        final ParameterDescriptor other = (ParameterDescriptor) o;
+        return Objects.equals(this.name, other.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name);
+    }
+
+
+    public static class Builder {
+        private String name;
+        private String description;
+        private boolean sensitive;
+
+        public Builder name(final String name) {
+            Objects.requireNonNull(name);
+
+            this.name = name.trim();
+            return this;
+        }
+
+        public Builder description(final String description) {
+            this.description = description == null ? null : description.trim();
+            return this;
+        }
+
+        public Builder sensitive(final boolean sensitive) {
+            this.sensitive = sensitive;
+            return this;
+        }
+
+        public Builder from(final ParameterDescriptor descriptor) {
+            name(descriptor.getName());
+            description(descriptor.getDescription());
+            sensitive(descriptor.isSensitive());
+            return this;
+        }
+
+        public ParameterDescriptor build() {
+            if (name == null) {
+                throw new IllegalStateException("Must specify Parameter Name");
+            }
+
+            return new ParameterDescriptor(this);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterGroup.java b/src/main/java/org/apache/nifi/parameter/ParameterGroup.java
new file mode 100644
index 0000000..c393387
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterGroup.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.parameter;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates a named group of externally fetched parameters that can be provided to referencing Parameter Contexts.
+ */
+public class ParameterGroup {
+
+    private final String groupName;
+
+    private final List<Parameter> parameters;
+
+    /**
+     * Creates a named parameter group.
+     * @param groupName The parameter group name
+     * @param parameters A list of parameters
+     */
+    public ParameterGroup(final String groupName, final List<Parameter> parameters) {
+        this.groupName = groupName;
+        this.parameters = Collections.unmodifiableList(parameters);
+    }
+
+    /**
+     * @return The group name
+     */
+    public String getGroupName() {
+        return groupName;
+    }
+
+    /**
+     * @return The provided parameters
+     */
+    public List<Parameter> getParameters() {
+        return parameters;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterGroupConfiguration.java b/src/main/java/org/apache/nifi/parameter/ParameterGroupConfiguration.java
new file mode 100644
index 0000000..c519327
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterGroupConfiguration.java
@@ -0,0 +1,100 @@
+/*
+ * 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.parameter;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * User-provided configuration for a group of parameters fetched from a ParameterProvider.
+ */
+public class ParameterGroupConfiguration implements Comparable<ParameterGroupConfiguration> {
+
+    private final String groupName;
+
+    private final String parameterContextName;
+
+    private final Map<String, ParameterSensitivity> parameterSensitivities;
+
+    private final Boolean isSynchronized;
+
+    /**
+     * Creates a named group of parameter names.
+     * @param groupName The parameter group name
+     * @param parameterContextName The parameter context name to which parameters will be applied
+     * @param parameterSensitivities A map from parameter name to desired sensitivity.  Any parameter not included in this map will not be included
+     *                               when applied to the parameter context.
+     * @param isSynchronized If true, indicates that a ParameterContext should be created if not already existing, or updated if existing
+     */
+    public ParameterGroupConfiguration(final String groupName, final String parameterContextName, final Map<String, ParameterSensitivity> parameterSensitivities,
+                                       final Boolean isSynchronized) {
+        this.groupName = Objects.requireNonNull(groupName, "Parameter group name is required");
+        this.parameterContextName = Optional.ofNullable(parameterContextName).orElse(groupName);
+        this.parameterSensitivities = Collections.unmodifiableMap(Objects.requireNonNull(parameterSensitivities, "Parameter sensitivity map is required"));
+        this.isSynchronized = isSynchronized;
+    }
+
+    /**
+     * @return The external parameter group name
+     */
+    public String getGroupName() {
+        return groupName;
+    }
+
+    /**
+     * @return The name of the ParameterContext that maps to this group
+     */
+    public String getParameterContextName() {
+        return parameterContextName;
+    }
+
+    /**
+     * @return A map from parameter name to desired sensitivity.  If the sensitivity is null, this indicates that the parameter
+     * has not yet been configured by the user.
+     */
+    public Map<String, ParameterSensitivity> getParameterSensitivities() {
+        return parameterSensitivities;
+    }
+
+    /**
+     * @return True if this group should be synchronized with a parameter context.  If null, this indicates that
+     * it has not yet been configured by the user.
+     */
+    public Boolean isSynchronized() {
+        return isSynchronized;
+    }
+
+    @Override
+    public int compareTo(final ParameterGroupConfiguration other) {
+        if (other == null) {
+            return -1;
+        }
+
+        final String groupName = getGroupName();
+        final String otherGroupName = other.getGroupName();
+
+        if (groupName == null) {
+            return otherGroupName == null ? 0 : -1;
+        }
+        if (otherGroupName == null) {
+            return 1;
+        }
+        return groupName.compareTo(otherGroupName);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterProvider.java b/src/main/java/org/apache/nifi/parameter/ParameterProvider.java
new file mode 100644
index 0000000..7179bfb
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterProvider.java
@@ -0,0 +1,87 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Defines a provider that is responsible for fetching from an external source Parameters with
+ * which a ParameterContext can be populated.
+ *
+ * <p>
+ * <code>ParameterProvider</code>s are discovered following Java's
+ * <code>ServiceLoader</code> pattern. As a result, all implementations must
+ * follow these rules:
+ *
+ * <ul>
+ * <li>The implementation must implement this interface.</li>
+ * <li>The implementation must have a file named
+ * org.apache.nifi.parameter.ParameterProvider located within the jar's
+ * <code>META-INF/services</code> directory. This file contains a list of
+ * fully-qualified class names of all <code>ParameterProvider</code>s in the jar,
+ * one-per-line.
+ * <li>The implementation must support a default constructor.</li>
+ * </ul>
+ *
+ * <p>
+ * All implementations of this interface must be thread-safe.
+ * </p>
+ *
+ * <p>
+ * Parameter Providers may choose to annotate a method with the
+ * {@link OnConfigurationRestored @OnConfigurationRestored} annotation. If this is done, that method
+ * will be invoked after all properties have been set for the ParameterProvider and
+ * before its parameters are fetched.
+ * </p>
+ */
+public interface ParameterProvider extends ConfigurableComponent {
+
+    /**
+     * Provides the Parameter Provider with access to objects that may be of use
+     * throughout the life of the provider
+     *
+     * @param config of initialization context
+     * @throws org.apache.nifi.reporting.InitializationException if unable to init
+     */
+    void initialize(ParameterProviderInitializationContext config) throws InitializationException;
+
+    /**
+     * Fetches named groups of parameters from an external source.
+     *
+     * Any referencing Parameter Context will only receive the Parameters from a group if the Parameter Context name matches
+     * the group name (ignoring case) and the reference sensitivity matches the group sensitivity.
+     *
+     * If group name is null, all referencing Parameter Contexts will receive the Parameters in that group,
+     * regardless of their name.
+     *
+     * If more than one ProvidedParameterGroup matches a given ParameterContext, all parameters from these groups will be
+     * applied.  However, if any parameters among these matching groups have the same name but different value, the framework
+     * will throw a <code>RuntimeException</code>
+     *
+     * @param context The <code>ConfigurationContext</code>for the provider
+     * @return A list of fetched Parameter groups.  The framework will set the sensitivity appropriately based on how the ParameterProvider
+     * is referenced in a ParameterContext.
+     * @throws IOException if there is an I/O problem while fetching the Parameters
+     */
+    List<ParameterGroup> fetchParameters(ConfigurationContext context) throws IOException;
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterProviderInitializationContext.java b/src/main/java/org/apache/nifi/parameter/ParameterProviderInitializationContext.java
new file mode 100644
index 0000000..ebf813c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterProviderInitializationContext.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.parameter;
+
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+
+public interface ParameterProviderInitializationContext extends KerberosContext {
+
+    /**
+     * @return the identifier associated with the {@link ParameterProvider} with
+     * which this context is associated
+     */
+    String getIdentifier();
+
+    /**
+     * @return the configured name for this ParameterProvider
+     */
+    String getName();
+
+    /**
+     * @return a logger that can be used to log important events in a standard
+     * way and generate bulletins when appropriate
+     */
+    ComponentLog getLogger();
+
+    /**
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     */
+    NodeTypeProvider getNodeTypeProvider();
+}
diff --git a/src/main/java/org/apache/nifi/parameter/ParameterSensitivity.java b/src/main/java/org/apache/nifi/parameter/ParameterSensitivity.java
new file mode 100644
index 0000000..6d3c523
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/ParameterSensitivity.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.parameter;
+
+/**
+ * Indicates the sensitivity of a parameter.
+ */
+public enum ParameterSensitivity {
+    SENSITIVE("Sensitive"),
+    NON_SENSITIVE("Non-Sensitive");
+
+    private String name;
+
+    ParameterSensitivity(final String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/parameter/VerifiableParameterProvider.java b/src/main/java/org/apache/nifi/parameter/VerifiableParameterProvider.java
new file mode 100644
index 0000000..8c7db57
--- /dev/null
+++ b/src/main/java/org/apache/nifi/parameter/VerifiableParameterProvider.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.parameter;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.util.List;
+
+/**
+ * <p>
+ *     Any Parameter Provider that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Parameter Provider. This allows the Parameter Provider to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Parameter Provider is not valid, its parameters cannot be fetched. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when fetching the parameters).
+ * </p>
+ *
+ * <p>
+ *     The framework is not responsible for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ConfigurationContext, ComponentLog)} itself.
+ *     The {@link #verify(ConfigurationContext, ComponentLog)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableParameterProvider {
+
+    /**
+     * Verifies that the configuration defined by the given ConfigurationContext is valid.
+     *
+     * @param context the Configuration Context that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     *
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ConfigurationContext context, ComponentLog verificationLogger);
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/AbstractProcessor.java b/src/main/java/org/apache/nifi/processor/AbstractProcessor.java
new file mode 100644
index 0000000..21690d4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/AbstractProcessor.java
@@ -0,0 +1,36 @@
+/*
+ * 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.processor.exception.ProcessException;
+
+public abstract class AbstractProcessor extends AbstractSessionFactoryProcessor {
+
+    @Override
+    public final void onTrigger(final ProcessContext context, final ProcessSessionFactory sessionFactory) throws ProcessException {
+        final ProcessSession session = sessionFactory.createSession();
+        try {
+            onTrigger(context, session);
+            session.commitAsync();
+        } catch (final Throwable t) {
+            session.rollback(true);
+            throw t;
+        }
+    }
+
+    public abstract void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException;
+}
diff --git a/src/main/java/org/apache/nifi/processor/AbstractSessionFactoryProcessor.java b/src/main/java/org/apache/nifi/processor/AbstractSessionFactoryProcessor.java
new file mode 100644
index 0000000..2394805
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/AbstractSessionFactoryProcessor.java
@@ -0,0 +1,145 @@
+/*
+ * 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 java.util.Collections;
+import java.util.Set;
+import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnUnscheduled;
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+
+/**
+ * <p>
+ * Provides a standard partial implementation of a {@link Processor}. This
+ * implementation provides default behavior and various convenience hooks for
+ * processing.</p>
+ *
+ * <p>
+ * Implementation/Design note: This class follows the open/closed principle in a
+ * fairly strict manner meaning that subclasses are free to customize behavior
+ * in specifically designed points exclusively. If greater flexibility is
+ * necessary then it is still possible to simply implement the {@link Processor}
+ * interface.</p>
+ *
+ * <p>
+ * Thread safe</p>
+ *
+ */
+public abstract class AbstractSessionFactoryProcessor extends AbstractConfigurableComponent implements Processor {
+
+    private String identifier;
+    private ComponentLog logger;
+    private volatile boolean scheduled = false;
+    private volatile boolean configurationRestored = false;
+    private ControllerServiceLookup serviceLookup;
+    private NodeTypeProvider nodeTypeProvider;
+    private String description;
+
+    @Override
+    public final void initialize(final ProcessorInitializationContext context) {
+        identifier = context.getIdentifier();
+        logger = context.getLogger();
+        serviceLookup = context.getControllerServiceLookup();
+        nodeTypeProvider = context.getNodeTypeProvider();
+        init(context);
+
+        description = getClass().getSimpleName() + "[id=" + identifier + "]";
+    }
+
+    /**
+     * @return the {@link ControllerServiceLookup} that was passed to the
+     * {@link #init(ProcessorInitializationContext)} method
+     */
+    protected final ControllerServiceLookup getControllerServiceLookup() {
+        return serviceLookup;
+    }
+
+    /**
+     * @return the {@link NodeTypeProvider} that was passed to the
+     * {@link #init(ProcessorInitializationContext)} method
+     */
+    protected final NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return Collections.emptySet();
+    }
+
+    protected final ComponentLog getLogger() {
+        return logger;
+    }
+
+    /**
+     * Provides subclasses the ability to perform initialization logic
+     *
+     * @param context in which to perform initialization
+     */
+    protected void init(final ProcessorInitializationContext context) {
+        // Provided for subclasses to override
+    }
+
+    /**
+     * @return <code>true</code> if the processor is scheduled to run,
+     * <code>false</code> otherwise
+     */
+    protected final boolean isScheduled() {
+        return scheduled;
+    }
+
+    @OnScheduled
+    public final void updateScheduledTrue() {
+        scheduled = true;
+    }
+
+    @OnUnscheduled
+    public final void updateScheduledFalse() {
+        scheduled = false;
+    }
+
+    @OnConfigurationRestored
+    public final void updateConfiguredRestoredTrue() {
+        configurationRestored = true;
+    }
+
+    /**
+     * Returns a boolean indicating whether or not the configuration of the Processor has already been restored.
+     * See the {@link OnConfigurationRestored} annotation for more information about what it means for the configuration
+     * to be restored.
+     *
+     * @return <code>true</code> if configuration has been restored, <code>false</code> otherwise.
+     */
+    protected boolean isConfigurationRestored() {
+        return configurationRestored;
+    }
+
+    @Override
+    public final String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String toString() {
+        return description;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/DataUnit.java b/src/main/java/org/apache/nifi/processor/DataUnit.java
new file mode 100644
index 0000000..4980b97
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/DataUnit.java
@@ -0,0 +1,245 @@
+/*
+ * 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 java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public enum DataUnit {
+
+    /**
+     * Bytes
+     */
+    B {
+                @Override
+                public double toB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[3];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[4];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toB(sourceSize);
+                }
+            },
+    /**
+     * Kilobytes
+     */
+    KB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[3];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toKB(sourceSize);
+                }
+            },
+    /**
+     * Megabytes
+     */
+    MB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toMB(sourceSize);
+                }
+            },
+    /**
+     * Gigabytes
+     */
+    GB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[3];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toGB(sourceSize);
+                }
+            },
+    /**
+     * Terabytes
+     */
+    TB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[4];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[3];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toTB(sourceSize);
+                }
+            };
+
+    public double convert(final double sourceSize, final DataUnit sourceUnit) {
+        throw new AbstractMethodError();
+    }
+
+    public double toB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toKB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toMB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toGB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toTB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public static final double[] POWERS = {1,
+        1024D,
+        1024 * 1024D,
+        1024 * 1024 * 1024D,
+        1024 * 1024 * 1024 * 1024D};
+
+    public static final String DATA_SIZE_REGEX = "(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)";
+    public static final Pattern DATA_SIZE_PATTERN = Pattern.compile(DATA_SIZE_REGEX);
+
+    public static Double parseDataSize(final String value, final DataUnit units) {
+        if (value == null) {
+            return null;
+        }
+
+        final Matcher matcher = DATA_SIZE_PATTERN.matcher(value.toUpperCase());
+        if (!matcher.find()) {
+            throw new IllegalArgumentException("Invalid data size: " + value);
+        }
+
+        final String sizeValue = matcher.group(1);
+        final String unitValue = matcher.group(2);
+
+        final DataUnit sourceUnit = DataUnit.valueOf(unitValue);
+        final double size = Double.parseDouble(sizeValue);
+        return units.convert(size, sourceUnit);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/processor/FlowFileFilter.java b/src/main/java/org/apache/nifi/processor/FlowFileFilter.java
new file mode 100644
index 0000000..18501cb
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/FlowFileFilter.java
@@ -0,0 +1,93 @@
+/*
+ * 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.flowfile.FlowFile;
+
+/**
+ * <p>
+ * FlowFileFilter provides a mechanism for selectively choosing which FlowFiles
+ * to obtain from a Processor's incoming connections.
+ * </p>
+ *
+ * <p>
+ * Implementations of this interface need not be thread-safe.
+ * </p>
+ */
+public interface FlowFileFilter {
+
+    /**
+     * Indicates whether or not the given FlowFile should be selected and
+     * whether or not the Processor is interested in filtering additional
+     * FlowFiles
+     *
+     * @param flowFile to apply the filter to
+     * @return true if the given FlowFile should be selected and
+     * if Processor is interested in filtering additional
+     * FlowFiles
+     */
+    FlowFileFilterResult filter(FlowFile flowFile);
+
+    /**
+     * Provides a result type to indicate whether or not a FlowFile should be
+     * selected
+     */
+    enum FlowFileFilterResult {
+
+        /**
+         * Indicates that a FlowFile should be returned to the Processor to be
+         * processed and that additional FlowFiles should be processed by this
+         * filter.
+         */
+        ACCEPT_AND_CONTINUE(true, true),
+        /**
+         * Indicates that a FlowFile should be returned to the Processor to be
+         * processed and that this is the last FlowFile that should be processed
+         * by this filter.
+         */
+        ACCEPT_AND_TERMINATE(false, true),
+        /**
+         * Indicates that a FlowFile should not be processed by the Processor at
+         * this time but that additional FlowFiles should be processed by this
+         * filter.
+         */
+        REJECT_AND_CONTINUE(true, false),
+        /**
+         * Indicates that a FlowFile should not be processed by the Processor at
+         * this time and that no additional FlowFiles should be processed
+         * either.
+         */
+        REJECT_AND_TERMINATE(false, false);
+
+        private final boolean continueProcessing;
+        private final boolean accept;
+
+        FlowFileFilterResult(final boolean continueProcessing, final boolean accept) {
+            this.continueProcessing = continueProcessing;
+            this.accept = accept;
+        }
+
+        public boolean isAccept() {
+            return accept;
+        }
+
+        public boolean isContinue() {
+            return continueProcessing;
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/ProcessContext.java b/src/main/java/org/apache/nifi/processor/ProcessContext.java
new file mode 100644
index 0000000..6b15a1a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/ProcessContext.java
@@ -0,0 +1,167 @@
+/*
+ * 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.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.context.ClusterContext;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.scheduling.ExecutionNode;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>
+ * Provides a bridge between a Processor and the NiFi Framework
+ * </p>
+ *
+ * <p>
+ * <b>Note: </b>Implementations of this interface are NOT necessarily
+ * thread-safe.
+ * </p>
+ */
+public interface ProcessContext extends PropertyContext, ClusterContext {
+
+    /**
+     * Retrieves the current value set for the given descriptor, if a value is
+     * set - else uses the descriptor to determine the appropriate default value
+     *
+     * @param propertyName of the property to lookup the value for
+     * @return property value as retrieved by property name
+     */
+    PropertyValue getProperty(String propertyName);
+
+    /**
+     * Creates and returns a {@link PropertyValue} object that can be used for
+     * evaluating the value of the given String
+     *
+     * @param rawValue the raw input before any property evaluation has occurred
+     * @return a {@link PropertyValue} object that can be used for
+     * evaluating the value of the given String
+     */
+    PropertyValue newPropertyValue(String rawValue);
+
+    /**
+     * <p>
+     * Causes the Processor not to be scheduled for some pre-configured amount
+     * of time. The duration of time for which the processor will not be
+     * scheduled is configured in the same manner as the processor's scheduling
+     * period.
+     * </p>
+     *
+     * <p>
+     * <b>Note: </b>This is NOT a blocking call and does not suspend execution
+     * of the current thread.
+     * </p>
+     */
+    void yield();
+
+    /**
+     * @return the maximum number of threads that may be executing this
+     * processor's code at any given time
+     */
+    int getMaxConcurrentTasks();
+
+    /**
+     * @return the Nodes where this processor will be scheduled to run.
+     */
+    ExecutionNode getExecutionNode();
+
+    /**
+     * @return the annotation data configured for this processor
+     */
+    String getAnnotationData();
+
+    /**
+     * @return a Map of all PropertyDescriptors to their configured values. This
+     * Map may or may not be modifiable, but modifying its values will not
+     * change the values of the processor's properties
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return a {@code ControllerServiceLookup} that can be used to obtain a
+     * Controller Service
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @return the set of all relationships for which space is available to
+     * receive new objects
+     */
+    Set<Relationship> getAvailableRelationships();
+
+    /**
+     * Indicates whether or not the given relationship is configured to be auto-terminated
+     * @param relationship the relationship
+     * @return <code>true</code> if the given relationship is auto-terminated, <code>false</code> otherwise
+     */
+    boolean isAutoTerminated(Relationship relationship);
+
+    /**
+     * @return true if the processor has one or more incoming connections,
+     * false otherwise
+     */
+    boolean hasIncomingConnection();
+
+    /**
+     * @return <code>true</code> if the processor has one or more incoming connections for
+     *         which the source of the connection is NOT the processor; returns <code>false</code> if
+     *         the processor has no incoming connections or if all incoming connections are self-loops
+     *         (i.e., the processor is also the source of all incoming connections).
+     */
+    boolean hasNonLoopConnection();
+
+    /**
+     * @param relationship a relationship to check for connections
+     * @return true if the relationship has one or more outbound connections,
+     *         false otherwise
+     */
+    boolean hasConnection(Relationship relationship);
+
+    /**
+     * @param property the Property whose value should be inspected to determined if it contains an Expression Language Expression
+     * @return <code>true</code> if the value of the given Property contains a NiFi Expression
+     * Language Expression, <code>false</code> if it does not. Note that <code>false</code> will be returned if the Property Descriptor
+     * does not allow the Expression Language, even if a seemingly valid Expression is present in the value.
+     */
+    boolean isExpressionLanguagePresent(PropertyDescriptor property);
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this component
+     */
+    StateManager getStateManager();
+
+    /**
+     * @return the configured name of this processor
+     */
+    String getName();
+
+    /**
+     * @param relationship Relationship to be evaluated
+     * @return the configured relationships to be retried of this processor
+     */
+    boolean isRelationshipRetried(Relationship relationship);
+
+    /**
+     * @return the max number of times that the Processor will retry a FlowFile that is routed to a Relationship that is marked for Retry
+     */
+    int getRetryCount();
+}
diff --git a/src/main/java/org/apache/nifi/processor/ProcessSession.java b/src/main/java/org/apache/nifi/processor/ProcessSession.java
new file mode 100644
index 0000000..26106b1
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/ProcessSession.java
@@ -0,0 +1,901 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.controller.queue.QueueSize;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.exception.FlowFileAccessException;
+import org.apache.nifi.processor.exception.FlowFileHandlingException;
+import org.apache.nifi.processor.exception.MissingFlowFileException;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.io.InputStreamCallback;
+import org.apache.nifi.processor.io.OutputStreamCallback;
+import org.apache.nifi.processor.io.StreamCallback;
+import org.apache.nifi.provenance.ProvenanceReporter;
+import org.apache.nifi.provenance.ProvenanceEventType;
+
+/**
+ * A process session encompasses all the behaviors a processor can perform to
+ * obtain, clone, read, modify remove {@link FlowFile}s in an atomic unit.
+ * A process session is always tied to a single {@link Processor} at any one time
+ * and ensures no FlowFile can ever be accessed by any more than one processor at a given time.
+ * The session also ensures that all FlowFiles are always accounted for.
+ * The creator of a process session is always required to manage the session.
+ * <p>
+ * A session is not considered thread safe. The session supports a unit of work that is either committed or rolled back.
+ * <p>
+ * As noted on specific methods and for specific exceptions
+ * automated rollback will occur to ensure consistency of the repository.
+ * However, several situations can result in exceptions yet not cause automated rollback.
+ * In these cases the consistency of the repository will be retained
+ * but callers will be able to indicate whether it should result in rollback or continue on toward a commit.
+ * <p>
+ * A process session has two types of 'terminal' methods that will result in the session being in a 'fresh' state,
+ * containing no knowledge or any FlowFile, as if the session were newly created.
+ * After one of these methods is called, the instance may be used again.
+ * The terminal methods for a process session are  {@link #commitAsync()} and {@link #rollback()} (and their overloads).
+ * <p>
+ * Additionally, the {@link #migrate(ProcessSession, Collection)} method transfers all knowledge of the provided FlowFiles
+ * to the other process session, as if the FlowFiles never existed in this process session.
+ * Note, however, that even if all FlowFiles are migrated via the {@link #migrate(ProcessSession, Collection)} method,
+ * the session is not necessarily entirely cleared, as it still may have knowledge of counter adjustments or state changes,
+ * e.g. see methods {@link #adjustCounter(String, long, boolean)} or {@link #setState(Map, Scope)}.
+ * A commit or rollback will clear these changes as well.
+ */
+public interface ProcessSession {
+
+    /**
+     * Commits the current session ensuring all operations against {@link FlowFile}s within this session are atomically persisted.
+     * All FlowFiles operated on within this session must be accounted for by transfer or removal or the commit will fail.
+     * <p>
+     * As soon as the commit completes the session is again ready to be used.
+     * <p>
+     * See {@link #commitAsync()}, {@link #commitAsync(Runnable)}, and {@link #commitAsync(Runnable, Consumer)}.
+     * The asynchronous session commit methods are preferable to this commit call for most cases
+     * as it defers when the actual commit happens to the framework so that it can optimize when the commit call takes place.
+     *
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}).
+     * @throws FlowFileHandlingException if not all {@link FlowFile}s acted upon within this session are accounted for
+     *              such that they have a transfer identified or where marked for removal. Automated rollback occurs.
+     * @throws ProcessException if some general fault occurs while persisting the session.
+     *              Initiates automatic rollback. The root cause can be obtained via {@link Exception#getCause}.
+     */
+    void commit();
+
+    /**
+     * Commits the current session ensuring all operations against {@link FlowFile}s within this session are atomically persisted.
+     * All FlowFiles operated on within this session must be accounted for by transfer or removal or the commit will fail.
+     * <p>
+     * Unlike the {@link #commit()} method, the persistence of data to the repositories
+     * is not guaranteed to have occurred by the time that this method returns.
+     * Therefore, if any follow-on actions are necessary after the data has been persisted to the repository
+     * (for example, acknowledging receipt from a source system, removing a source file, etc.) that logic
+     * should be performed only by invoking {@link #commitAsync(Runnable)} or {@link #commitAsync(Runnable, Consumer)}
+     * and implementing that action in the provided callback.
+     * <p>
+     * As a result, the following very common idiom:
+     * <pre><code>
+     * getDataFromSource();
+     * session.commit();
+     * acknowledgeReceiptOfData();
+     * </code></pre>
+     * Cannot be simply changed to:
+     * <pre><code>
+     * getDataFromSource();
+     * session.commitAsync();
+     * acknowledgeReceiptOfData();
+     * </code></pre>
+     * Doing so could result in acknowledging receipt of data from the source system before data has been committed to the repositories.
+     * If NiFi were to then be restarted, there is potential for data loss.
+     * Rather, the following idiom should take its place to ensure that there is no data loss:
+     * <pre><code>
+     * getDataFromSource();
+     * session.commitAsync( () -> acknowledgeReceiptOfData() );
+     * </code></pre>
+     * <p>
+     * If the session cannot be committed, an error will be logged and the session will be rolled back instead.
+     *
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}).
+     * @throws FlowFileHandlingException if not all {@link FlowFile}s acted upon within this session are accounted for
+     *              such that they have a transfer identified or where marked for removal. Automated rollback occurs.
+     */
+    void commitAsync();
+
+    /**
+     * Commits the current session ensuring all operations against {@link FlowFile}s within this session are atomically persisted.
+     * All FlowFiles operated on within this session must be accounted for by transfer or removal or the commit will fail.
+     * <p>
+     * If the session is successfully committed, the given {@code onSuccess} {@link Runnable} will be called.
+     * At the point that the session commit is completed, any calls to {@link #rollback()} / {@link #rollback(boolean)}
+     * will not undo that session commit but instead roll back any changes that may have occurred since.
+     * <p>
+     * If, for any reason, the session could not be committed, an error-level log message will be generated,
+     * but the caller will not have a chance to perform any cleanup logic.
+     * If such logic is necessary, use {@link #commitAsync(Runnable, Consumer)} instead.
+     * <p>
+     * Unlike the {@link #commit()} method, the persistence of data to the repositories
+     * is not guaranteed to have occurred by the time that this method returns.
+     *
+     * @param onSuccess {@link Runnable} that will be called if and when the session is successfully committed; may be null
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}).
+     * @throws FlowFileHandlingException if not all {@link FlowFile}s acted upon within this session are accounted for
+     *              such that they have a transfer identified or where marked for removal. Automated rollback occurs.
+     */
+    default void commitAsync(Runnable onSuccess) {
+        commitAsync(onSuccess, null);
+    }
+
+    /**
+     * Commits the current session ensuring all operations against FlowFiles within this session are atomically persisted.
+     * All FlowFiles operated on within this session must be accounted for by transfer or removal or the commit will fail.
+     * <p>
+     * If the session is successfully committed, the given {@code onSuccess} {@link Runnable} will be called.
+     * At the point that the session commit is completed, any calls to {@link #rollback()} / {@link #rollback(boolean)}
+     * will not undo that session commit but instead roll back any changes that may have occurred since.
+     * <p>
+     * If, for any reason, the session could not be committed, the given {@code onFailure} {@link Consumer} will be called
+     * instead of the {@code onSuccess} {@link Runnable}.
+     * The Consumer will be provided the Throwable that prevented the session commit from completing.
+     * <p>
+     * Unlike the {@link #commit()} method, the persistence of data to the repositories
+     * is not guaranteed to have occurred by the time that this method returns.
+     *
+     * @param onSuccess {@link Runnable} that will be called if and when the session is successfully committed; may be null
+     * @param onFailure {@link Consumer} that will be called if, for any reason, the session could not be committed; may be null
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}).
+     * @throws FlowFileHandlingException if not all {@link FlowFile}s acted upon within this session are accounted for
+     *              such that they have a transfer identified or where marked for removal. Automated rollback occurs.
+     */
+    void commitAsync(Runnable onSuccess, Consumer<Throwable> onFailure);
+
+    /**
+     * Reverts any changes made during this session.
+     * All {@link FlowFile}s are restored back to their initial session state and back to their original queues.
+     * If no changes were made since this session was last committed or rolled back, then this method has no effect.
+     * This method can be called any number of times.
+     * Calling this method is identical to calling {@link #rollback(boolean)} passing {@code false} as the parameter.
+     */
+    void rollback();
+
+    /**
+     * Reverts any changes made during this session.
+     * All {@link FlowFile}s are restored back to their initial session state and back to their original queues,
+     * after optionally being penalized.
+     * If no changes were made since this session was last committed or rolled back, then this method has no effect.
+     * This method can be called any number of times.
+     *
+     * @param penalize whether the {@link FlowFile}s that are being restored back to their queues should be penalized
+     */
+    void rollback(boolean penalize);
+
+    /**
+     * Migrates ownership of the given {@code flowFiles} {@link FlowFile}s from {@code this} session to the given {@code newOwner} {@link ProcessSession}.
+     * <p>
+     * Note, that for any provided FlowFile, if the FlowFile has any child (e.g., by calling {@link #create(FlowFile)}
+     * and passing the FlowFile as the argument), then all children that were created must also be in the Collection of provided FlowFiles.
+     * <p>
+     * Also note, that if any FlowFile given is not the most up-to-date version of that FlowFile,
+     * then the most up-to-date version of the FlowFile will be migrated to the new owner.
+     * For example, if a call to {@link #putAttribute(FlowFile, String, String)} is made,
+     * passing {@code flowFile1} as the FlowFile, and then {@code flowFile1} is passed to this method,
+     * then the newest version (including the newly added attribute) will be migrated,
+     * not the outdated version of the FlowFile that {@code flowFile1} points to.
+     *
+     * @param newOwner the {@link ProcessSession} that is to become the new owner of the given {@link FlowFile}s
+     * @param flowFiles the {@link FlowFile}s to migrate
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the affected {@link FlowFile}s
+     */
+    void migrate(ProcessSession newOwner, Collection<FlowFile> flowFiles);
+
+    /**
+     * Migrates ownership of all {@link FlowFile}s from {@code this} session to the given {@code newOwner} {@link ProcessSession}.
+     * Calling this method is identical to calling {@link #migrate(ProcessSession, Collection)}
+     * passing all FlowFiles owned by this session as the parameter,
+     * this encompasses both FlowFiles retrieved from the work queue and newly created or cloned ones.
+     *
+     * @param newOwner the {@link ProcessSession} that is to become the new owner of all {@link FlowFile}s
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the affected {@link FlowFile}s
+     */
+    void migrate(ProcessSession newOwner);
+
+    /**
+     * Adjusts counter data for the given counter name and takes care of registering the counter if not already present.
+     * The adjustment occurs only if and when the process session is committed.
+     *
+     * @param name the name of the counter
+     * @param delta the delta by which to modify the counter (+ or -)
+     * @param immediate if true, the counter will be updated immediately, without regard to whether the session is committed or rolled back;
+     *            otherwise, the counter will be incremented only if and when the session is committed.
+     */
+    void adjustCounter(String name, long delta, boolean immediate);
+
+    /**
+     * Returns the {@link FlowFile} from the work queue that is next highest priority to process.
+     * If no FlowFiles are available, returns {@code null}.
+     *
+     * @return the {@link FlowFile} from the work queue that is next highest priority to process or {@code null}, if none available
+     */
+    FlowFile get();
+
+    /**
+     * Returns the next up to {@code maxResults} {@link FlowFile}s from the work queue that are the highest priority to process.
+     * If no FlowFiles are available, returns an empty list. Will not return {@code null}.
+     * <p>
+     * If multiple incoming queues are present, the behavior is unspecified in terms of
+     * whether all queues or only a single queue will be polled in a single call.
+     *
+     * @param maxResults the maximum number of {@link FlowFile}s to return
+     * @return up to {@code maxResults} {@link FlowFile}s from the work queue
+     * @throws IllegalArgumentException if {@code maxResults} is less than 0
+     */
+    List<FlowFile> get(int maxResults);
+
+    /**
+     * Returns all {@link FlowFile}s from all the incoming queues which the given {@link FlowFileFilter} accepts.
+     * <p>
+     * Calls to this method provide exclusive access to the underlying queues.
+     * That is, no other thread will be permitted to pull FlowFiles from or add FlowFiles
+     * to this {@link Processor}'s incoming queues until this method call has returned.
+     *
+     * @param filter a {@link FlowFileFilter} to limit which {@link FlowFile}s are returned
+     * @return all {@link FlowFile}s from all the incoming queues which the given {@link FlowFileFilter} {@code filter} accepts.
+     */
+    List<FlowFile> get(FlowFileFilter filter);
+
+    /**
+     * Returns the {@link QueueSize} that represents the number of {@link FlowFile}s and their combined data size
+     * for all FlowFiles waiting to be processed by the {@link Processor} that owns {@code this} {@link ProcessSession},
+     * regardless of which connection the FlowFiles live on.
+     *
+     * @return the number of {@link FlowFile}s and their combined data size in the work queue
+     */
+    QueueSize getQueueSize();
+
+    /**
+     * Creates a new {@link FlowFile} in the repository with no content and without any linkage to a parent FlowFile.
+     * <p>
+     * This method is appropriate only when data is received or created from an external system.
+     * Otherwise, this method should be avoided and instead {@link #create(FlowFile)} or {@link #create(Collection)} be used.
+     * <p>
+     * When this method is used, a {@link ProvenanceEventType#CREATE} or {@link ProvenanceEventType#RECEIVE} event should be generated.
+     * See the {@link #getProvenanceReporter()} method and {@link ProvenanceReporter} class for more information.
+     *
+     * @return newly created FlowFile
+     */
+    FlowFile create();
+
+    /**
+     * Creates a new {@link FlowFile} in the repository with no content but with a parent linkage to the {@code parent}.
+     * The newly created FlowFile will inherit all the parent's attributes, except for the UUID.
+     * <p>
+     * This method will automatically generate a {@link ProvenanceEventType#FORK} or a {@link ProvenanceEventType#JOIN} event,
+     * depending on whether other FlowFiles are generated from the same parent before the session is committed.
+     *
+     * @param parent to base the new {@link FlowFile} on, inheriting attributes from
+     * @return newly created {@link FlowFile}
+     */
+    FlowFile create(FlowFile parent);
+
+    /**
+     * Creates a new {@link FlowFile} in the repository with no content but with a parent linkage to all {@code parents}.
+     * The newly created FlowFile will inherit all the attributes that are in common to all parents, except for the UUID.
+     * <p>
+     * This method will automatically generate a {@link ProvenanceEventType#JOIN} event.
+     *
+     * @param parents to base the new {@link FlowFile} on, inheriting shared attributes from
+     * @return newly created {@link FlowFile}
+     */
+    FlowFile create(Collection<FlowFile> parents);
+
+    /**
+     * Creates a new {@link FlowFile} with a parent linkage to the {@code example} FlowFile.
+     * It is a clone of the given FlowFile as of the time this is called, both in attributes and content.
+     * <p>
+     * This method will automatically generate a {@link ProvenanceEventType#CLONE} event.
+     *
+     * @param example {@link FlowFile} to be the source of cloning - given FlowFile must be a part of the given session
+     * @return {@link FlowFile} that is a clone of the given {@code example} FlowFile
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code example} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    FlowFile clone(FlowFile example);
+
+    /**
+     * Creates a new {@link FlowFile} with a parent linkage to the {@code parent} FlowFile.
+     * It is a clone of the given FlowFile as of the time this is called, both in attributes and a subset of the content.
+     * The content of the new FlowFile will be a subset of the byte sequence of the given FlowFile,
+     * starting at the specified offset and with the length specified.
+     * <p>
+     * This method will automatically generate a {@link ProvenanceEventType#FORK} or a {@link ProvenanceEventType#CLONE} event,
+     * if the offset is 0 and the size is exactly equal to the size of the example FlowFile.
+     *
+     * @param parent {@link FlowFile} to be the source of cloning - given FlowFile must be a part of the given session
+     * @param offset of the parent {@link FlowFile}'s content to base the cloned FlowFile's content on
+     * @param size in bytes of the parent {@link FlowFile}'s content to clone starting from the {@code offset}
+     * @return {@link FlowFile} that is a partial clone of the given {@code parent} FlowFile whose content has the specified {@code size}
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code parent} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Or if the specified {@code offset} + {@code size} exceeds the size of the {@code parent} FlowFile's content.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    FlowFile clone(FlowFile parent, long offset, long size);
+
+    /**
+     * Sets a penalty for the given {@link FlowFile},
+     * which will make it unavailable to be operated on any further during the penalty period.
+     *
+     * @param flowFile to penalize
+     * @return the penalized {@link FlowFile}
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile penalize(FlowFile flowFile);
+
+    /**
+     * Updates the given {@link FlowFile}'s attributes with the given {@code key} / {@code value} pair.
+     * <p>
+     * If the {@code key} is named {@code uuid}, this attribute will be ignored.
+     *
+     * @param flowFile to update
+     * @param key of attribute to add or modify
+     * @param value of attribute to add or modify
+     * @return the updated {@link FlowFile} with the attribute added or modified
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile putAttribute(FlowFile flowFile, String key, String value);
+
+    /**
+     * Updates the given {@link FlowFile}'s attributes with the given {@code key} / {@code value} pairs.
+     * <p>
+     * If the map contains a key named {@code uuid}, this attribute will be ignored.
+     *
+     * @param flowFile to update
+     * @param attributes the attributes to add or modify
+     * @return the updated {@link FlowFile} with the attributes added or modified
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile putAllAttributes(FlowFile flowFile, Map<String, String> attributes);
+
+    /**
+     * Removes the attribute with the given {@code key} from the given {@link FlowFile}.
+     * <p>
+     * The attributes with the keys {@code uuid}, {@code path}, and {@code filename} will not be removed.
+     * If the {@code key} is one of those, this method will return the same FlowFile without removing any attribute.
+     *
+     * @param flowFile to update
+     * @param key of attribute to remove
+     * @return the updated {@link FlowFile} with the matching attribute removed
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile removeAttribute(FlowFile flowFile, String key);
+
+    /**
+     * Removes the attributes with the given {@code keys} from the given {@link FlowFile}.
+     * <p>
+     * The attributes with the keys {@code uuid}, {@code path}, and {@code filename} will not be removed.
+     *
+     * @param flowFile to update
+     * @param keys of attributes to remove
+     * @return the updated {@link FlowFile} with the matching attributes removed
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile removeAllAttributes(FlowFile flowFile, Set<String> keys);
+
+    /**
+     * Removes all attributes from the given {@link FlowFile} whose key matches the given pattern.
+     * <p>
+     * The attributes with the keys {@code uuid}, {@code path}, and {@code filename} will not be removed.
+     *
+     * @param flowFile to update
+     * @param keyPattern pattern to match each {@link FlowFile} attribute against; may be null, in which case no attribute is removed
+     * @return the updated {@link FlowFile} with the matching attributes removed
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    FlowFile removeAllAttributes(FlowFile flowFile, Pattern keyPattern);
+
+    /**
+     * Transfers the given {@link FlowFile} back to the work queue from which it was pulled.
+     * <p>
+     * The processor will not be able to operate on the given FlowFile until this session is committed.
+     * Any modifications that have been made to the FlowFile will be maintained.
+     * FlowFiles that are created by the processor cannot be transferred back to themselves via this method.
+     *
+     * @param flowFile to transfer
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws IllegalArgumentException if the given {@link FlowFile} was created by this processor
+     */
+    void transfer(FlowFile flowFile);
+
+    /**
+     * Transfers the given {@link FlowFile}s back to the work queues from which the FlowFiles were pulled.
+     * <p>
+     * The processor will not be able to operate on the given FlowFiles until this session is committed.
+     * Any modifications that have been made to the FlowFiles will be maintained.
+     * FlowFiles that are created by the processor cannot be transferred back to themselves via this method.
+     *
+     * @param flowFiles to transfer
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if any of the given {@link FlowFile}s is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws IllegalArgumentException if any of the given {@link FlowFile}s was created by this processor
+     */
+    void transfer(Collection<FlowFile> flowFiles);
+
+    /**
+     * Transfers the given {@link FlowFile} to the appropriate destination processor work queue(s) based on the given {@code relationship}.
+     * <p>
+     * If the relationship leads to more than one destination the state of the FlowFile is replicated
+     * such that each destination receives an exact copy of the FlowFile though each will have its own unique identity.
+     * The destination processors will not be able to operate on the given FlowFile until this session is committed or
+     * until the ownership of the session is migrated to another processor.
+     * If ownership of the session is passed to a destination processor then that destination processor will have immediate visibility
+     * of the transferred FlowFiles within the session.
+     *
+     * @param flowFile to transfer
+     * @param relationship to transfer to
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws IllegalArgumentException if given relationship is not a known or registered relationship
+     */
+    void transfer(FlowFile flowFile, Relationship relationship);
+
+    /**
+     * Transfers the given {@link FlowFile}s to the appropriate destination processor work queue(s) based on the given {@code relationship}.
+     * <p>
+     * If the relationship leads to more than one destination the state of each FlowFile is replicated
+     * such that each destination receives an exact copy of the FlowFile though each will have its own unique identity.
+     * The destination processors will not be able to operate on the given FlowFiles until this session is committed or
+     * until the ownership of the session is migrated to another processor.
+     * If ownership of the session is passed to a destination processor then that destination processor will have immediate visibility
+     * of the transferred FlowFiles within the session.
+     *
+     * @param flowFiles to transfer
+     * @param relationship to transfer to
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the {@code flowFiles} {@link FlowFile}s
+     * @throws FlowFileHandlingException if any of the given {@link FlowFile}s is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws IllegalArgumentException if given relationship is not a known or registered relationship
+     */
+    void transfer(Collection<FlowFile> flowFiles, Relationship relationship);
+
+    /**
+     * Ends the managed persistence for the given {@link FlowFile}.
+     * <p>
+     * The persistent attributes for the FlowFile are deleted and so is the content assuming nothing else references it.
+     * This FlowFile will no longer be available for further operation.
+     *
+     * @param flowFile to remove
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    void remove(FlowFile flowFile);
+
+    /**
+     * Ends the managed persistence for the given {@link FlowFile}s.
+     * <p>
+     * The persistent attributes for the FlowFiles are deleted and so is the content assuming nothing else references it.
+     * The FlowFiles will no longer be available for further operation.
+     *
+     * @param flowFiles to remove
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the given {@code flowFiles} {@link FlowFile}s
+     * @throws FlowFileHandlingException if any of the given {@link FlowFile}s is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     */
+    void remove(Collection<FlowFile> flowFiles);
+
+    /**
+     * Executes the given {code reader} {@link InputStreamCallback} against the content of the given {@link FlowFile}.
+     *
+     * @param source the {@link FlowFile} to retrieve the content from
+     * @param reader {@link InputStreamCallback} that will be called to read the {@link FlowFile} content
+     * @throws IllegalStateException if detected that this method is being called from within a write callback
+     *              (see {@link #write(FlowFile, StreamCallback)}, {@link #write(FlowFile, OutputStreamCallback)})
+     *              or while a write stream is open (see {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}.
+     *              Said another way, it is not permissible to call this method while writing to the same FlowFile.
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to access the {@link InputStream} provided to the given {@link InputStreamCallback}
+     *              after this method completed its execution
+     */
+    void read(FlowFile source, InputStreamCallback reader) throws FlowFileAccessException;
+
+    /**
+     * Provides an {@link InputStream} that can be used to read the content of the given {@link FlowFile}.
+     * <p>
+     * This method differs from those that make use of callbacks in that this method returns an InputStream and expects the caller
+     * to properly handle the lifecycle of the InputStream (i.e., the caller is responsible for ensuring that the InputStream is closed appropriately).
+     * The session may or may not handle closing the stream when the session is commited or rolled back,
+     * but the responsibility of doing so belongs to the caller.
+     *
+     * @param flowFile the {@link FlowFile} to retrieve the content from
+     * @return an {@link InputStream} that can be used to read the content of the {@link FlowFile}
+     * @throws IllegalStateException if detected that this method is being called from within a write callback
+     *              (see {@link #write(FlowFile, StreamCallback)}, {@link #write(FlowFile, OutputStreamCallback)})
+     *              or while a write stream is open (see {@link #write(FlowFile)}) for the given {@code flowFile} {@link FlowFile}.
+     *              Said another way, it is not permissible to call this method while writing to the same FlowFile.
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to read from the stream after the session is committed or rolled back.
+     */
+    InputStream read(FlowFile flowFile);
+
+    /**
+     * Combines the content of all given {@code sources} {@link FlowFile}s into a single given destination FlowFile.
+     *
+     * @param sources the {@link FlowFile}s whose content to merge
+     * @param destination the {@link FlowFile} to use as the merged result
+     * @return the updated {@code destination} destination {@link FlowFile} with changed content
+     * @throws IllegalArgumentException if the given destination is contained within the sources
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the given {@code sources} and {@code destination} {@link FlowFile}s
+     * @throws FlowFileHandlingException if any of the given {@link FlowFile}s is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if any of the given {@link FlowFile}'s content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              the state of the {@code destination} {@link FlowFile} will be as it was prior to this call.
+     */
+    FlowFile merge(Collection<FlowFile> sources, FlowFile destination);
+
+    /**
+     * Combines the content of all given {@code sources} {@link FlowFile}s into a single given destination FlowFile.
+     *
+     * @param sources the {@link FlowFile}s whose content to merge
+     * @param destination the {@link FlowFile} to use as the merged result
+     * @param header bytes that will be added to the beginning of the merged output; may be null or empty
+     * @param footer bytes that will be added to the end of the merged output; may be null or empty
+     * @param demarcator bytes that will be placed in between each object merged together; may be null or empty
+     * @return the updated {@code destination} {@link FlowFile} with changed content
+     * @throws IllegalArgumentException if the given destination is contained within the sources
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for any of the given {@code sources} and {@code destination} {@link FlowFile}s
+     * @throws FlowFileHandlingException if any of the given {@link FlowFile}s is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if any of the given {@link FlowFile}'s content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              the state of the {@code destination} {@link FlowFile} will be as it was prior to this call.
+     */
+    FlowFile merge(Collection<FlowFile> sources, FlowFile destination, byte[] header, byte[] footer, byte[] demarcator);
+
+    /**
+     * Executes the given {code writer} {@link OutputStreamCallback} against the content of the given {@link FlowFile}.
+     *
+     * @param source the {@link FlowFile} to write the content of
+     * @param writer {@link InputStreamCallback} that will be called to write the {@link FlowFile} content
+     * @return the updated {@code source} {@link FlowFile} with changed content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to access the {@link OutputStream} provided to the given {@link OutputStreamCallback}
+     *              after this method completed its execution
+     */
+    FlowFile write(FlowFile source, OutputStreamCallback writer) throws FlowFileAccessException;
+
+    /**
+     * Provides an {@link OutputStream} that can be used to write the content of the given {@link FlowFile}.
+     * <p>
+     * This method differs from those that make use of callbacks in that this method returns an OutputStream and expects the caller
+     * to properly handle the lifecycle of the OutputStream (i.e., the caller is responsible for ensuring that the OutputStream is closed appropriately).
+     * The session may or may not handle closing the stream when the session is commited or rolled back,
+     * but the responsibility of doing so belongs to the caller.
+     *
+     * @param source the {@link FlowFile} to write the content of
+     * @return an {@link OutputStream} that can be used to write the content of the {@link FlowFile}
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to write to the stream after the session is committed or rolled back.
+     */
+    OutputStream write(FlowFile source);
+
+    /**
+     * Executes the given {code writer} {@link StreamCallback} against the content of the given {@link FlowFile}.
+     *
+     * @param source the {@link FlowFile} to read and write the content of
+     * @param writer {@link StreamCallback} that will be called to read and write the {@link FlowFile} content
+     * @return the updated {@code source} {@link FlowFile} with changed content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to access the {@link InputStream} or {@link OutputStream}
+     *              provided to the given {@link StreamCallback} after this method completed its execution
+     */
+    FlowFile write(FlowFile source, StreamCallback writer) throws FlowFileAccessException;
+
+    /**
+     * Executes the given {code writer} {@link OutputStreamCallback} against the content of the given {@link FlowFile},
+     * such that any data written to the OutputStream will be appended to the end of FlowFile's content.
+     *
+     * @param source the {@link FlowFile} to extend the content of
+     * @param writer {@link OutputStreamCallback} that will be called to append the {@link FlowFile}'s content
+     * @return the updated {@code source} {@link FlowFile} with changed content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content;
+     *              if an attempt is made to access the {@link OutputStream} provided to the given {@link OutputStreamCallback}
+     *              after this method completed its execution
+     */
+    FlowFile append(FlowFile source, OutputStreamCallback writer) throws FlowFileAccessException;
+
+    /**
+     * Writes to contents of the file a the {@code source} {@link Path} to the given {@link FlowFile}'s content.
+     *
+     * @param source the {@link Path} to the file from which content will be obtained
+     * @param keepSourceFile if true the content is simply copied;
+     *              if false the original content might be used in a destructive way for efficiency,
+     *              such that the repository will have the data but the original data will be gone.
+     *              If false the source object will be removed or gone once imported.
+     *              It will not be restored if the session is rolled back so this must be used with caution.
+     *              In some cases it can result in tremendous efficiency gains but is also dangerous.
+     * @param destination the {@link FlowFile} whose content will be updated
+     * @return the updated {@code destination} {@link FlowFile} with changed content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    FlowFile importFrom(Path source, boolean keepSourceFile, FlowFile destination);
+
+    /**
+     * Writes to contents of the {@code source} {@link InputStream} to the given {@link FlowFile}'s content.
+     *
+     * @param source the {@link InputStream} from which content will be obtained
+     * @param destination the {@link FlowFile} whose content will be updated
+     * @return the updated {@code destination} {@link FlowFile} with changed content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code source} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    FlowFile importFrom(InputStream source, FlowFile destination);
+
+    /**
+     * Writes the content of the given {@link FlowFile} to the file at the given {@code destination} {@link Path}.
+     *
+     * @param flowFile the {@link FlowFile} to export the content of
+     * @param destination the {@link Path} to a file to export the {@link FlowFile}'s content to
+     * @param append if true will append to the current content of the file at the given path;
+     *              if false will replace any current content
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    void exportTo(FlowFile flowFile, Path destination, boolean append);
+
+    /**
+     * Writes the content of the given {@link FlowFile} to given {@code destination} {@link OutputStream}.
+     *
+     * @param flowFile the {@link FlowFile} to export the content of
+     * @param destination the {@link OutputStream} to export the {@link FlowFile}'s content to
+     * @throws IllegalStateException if detected that this method is being called from within a read or write callback
+     *              (see {@link #read(FlowFile, InputStreamCallback)}, {@link #write(FlowFile, StreamCallback)},
+     *              {@link #write(FlowFile, OutputStreamCallback)}) or while a read or write stream is open
+     *              (see {@link #read(FlowFile)}, {@link #write(FlowFile)}) for the given {@code flowFile} {@link FlowFile}
+     * @throws FlowFileHandlingException if the given {@link FlowFile} is already transferred or removed or doesn't belong to this session.
+     *              Automatic rollback will occur.
+     * @throws MissingFlowFileException if the given {@link FlowFile} content cannot be found.
+     *              The FlowFile should no longer be referenced, will be internally destroyed. The session is automatically rolled back.
+     * @throws FlowFileAccessException if some IO problem occurs accessing {@link FlowFile} content
+     */
+    void exportTo(FlowFile flowFile, OutputStream destination);
+
+    /**
+     * Returns the {@link ProvenanceReporter} that is tied to {@code this} {@link ProcessSession}.
+     *
+     * @return the {@link ProvenanceReporter} that is tied to {@code this} {@link ProcessSession}
+     */
+    ProvenanceReporter getProvenanceReporter();
+
+    /**
+     * Updates the value of the component's state, setting it to given value.
+     * <p>
+     * This method does not update the remote State Provider immediately but rather caches the value until the session is committed.
+     * At that point, it will publish the state to the remote State Provider, if the state is the latest according to the remote State Provider.
+     *
+     * @param state the value to change the state to
+     * @param scope the {@link Scope} to use when storing the state
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    void setState(Map<String, String> state, Scope scope) throws IOException;
+
+    /**
+     * Returns the current state for the component.
+     * <p>
+     * This return value will never be {@code null}.
+     * If the state has not yet been set, the StateMap's version will be -1, and the map of values will be empty.
+     *
+     * @param scope the {@link Scope} to use when fetching the state
+     * @return the current state for the component
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    StateMap getState(Scope scope) throws IOException;
+
+    /**
+     * Updates the value of the component's state, setting it to given {@code newValue},
+     * if and only if the current value is the same as the given {@code oldValue}.
+     * <p>
+     * The oldValue will be compared against the value of the state as it is known to {@code this} {@link ProcessSession}.
+     * If the Process Session does not currently know the state, it will be fetched from the StateProvider.
+     * <p>
+     * This method does not update the remote State Provider immediately but rather caches the value until the session is committed.
+     * At that point, it will publish the state to the remote State Provider, if the state is the latest according to the remote State Provider.
+     *
+     * @param oldValue the value to compare the state's current value against
+     * @param newValue the new value to use if and only if the state's current value is the same as the given {@code oldValue}
+     * @param scope the {@link Scope} to use for fetching the current and storing the new state
+     * @return {@code true} if the state was updated to the {@code newValue},
+     *              {@code false} if the state's current value was not equal to {@code oldValue}
+     * @throws IOException if unable to communicate with the underlying storage mechanism
+     */
+    boolean replaceState(StateMap oldValue, Map<String, String> newValue, Scope scope) throws IOException;
+
+    /**
+     * Clears all keys and values from the component's state.
+     * <p>
+     * This method does not update the remote State Provider immediately but rather caches the value until the session is committed.
+     * At that point, it will publish the state to the remote State Provider, if the state is the latest according to the remote State Provider.
+     *
+     * @param scope the {@link Scope} to use for clearing the state
+     * @throws IOException if unable to communicate with the underlying storage mechanism.
+     */
+    void clearState(Scope scope) throws IOException;
+}
diff --git a/src/main/java/org/apache/nifi/processor/ProcessSessionFactory.java b/src/main/java/org/apache/nifi/processor/ProcessSessionFactory.java
new file mode 100644
index 0000000..06c0479
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/ProcessSessionFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * <p>
+ * Factory for creating a {@link ProcessSession}
+ * </p>
+ *
+ * <p>
+ * MUST BE THREAD-SAFE</p>
+ */
+public interface ProcessSessionFactory {
+
+    ProcessSession createSession();
+}
diff --git a/src/main/java/org/apache/nifi/processor/Processor.java b/src/main/java/org/apache/nifi/processor/Processor.java
new file mode 100644
index 0000000..78851ec
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/Processor.java
@@ -0,0 +1,130 @@
+/*
+ * 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.annotation.behavior.Stateful;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.migration.PropertyConfiguration;
+import org.apache.nifi.migration.RelationshipConfiguration;
+import org.apache.nifi.processor.exception.ProcessException;
+
+import java.util.Set;
+
+/**
+ * <p>
+ * Processor objects operate on FlowFile objects where the processors are linked
+ * together via relationships forming a directed graph structure.</p>
+ *
+ * <p>
+ * The validity of a processor once established is considered safe until a
+ * configuration property of that processor is changed. Even if the property
+ * change is itself valid it still brings the validity of the processor as a
+ * whole into question. Therefore changing a processor as a whole must be
+ * reverified. Also, changing any configuration property of a processor can also
+ * imply that its supported relationships have changed.</p>
+ *
+ * <p>
+ * Each Processor object is a single node in a flow graph. The same processor
+ * object on a graph may be called concurrently to update its configuration
+ * state and to perform 'onTrigger' actions. The framework also provides
+ * numerous hooks via annotations that subclasses can use to control the
+ * lifecycle of a given processor instance.</p>
+ *
+ * <p>
+ * Processor objects are expected to be thread-safe and must have a public
+ * default no-args constructor to facilitate the java service loader
+ * mechanism.</p>
+ *
+ */
+public interface Processor extends ConfigurableComponent {
+
+    /**
+     * Provides the processor with access to objects that may be of use
+     * throughout the life of the Processor
+     *
+     * @param context of initialization
+     */
+    void initialize(ProcessorInitializationContext context);
+
+    /**
+     * @return Set of all relationships this processor expects to transfer a
+     * flow file to. An empty set indicates this processor does not have any
+     * destination relationships. Guaranteed non null.
+     */
+    Set<Relationship> getRelationships();
+
+    /**
+     * <p>
+     * The method called when this processor is triggered to operate by the
+     * controller. In the absence of the {@link org.apache.nifi.annotation.behavior.TriggerSerially} annotation,
+     * this method may be called concurrently from different threads.
+     * When this method is called depends on how this processor is
+     * configured within a controller to be triggered (timing or event
+     * based).</p>
+     *
+     * @param context provides access to convenience methods for obtaining
+     * property values, delaying the scheduling of the processor, provides
+     * access to Controller Services, etc.
+     * @param sessionFactory provides access to a {@link ProcessSession}, which
+     * can be used for accessing FlowFiles, etc.
+     *
+     * @throws ProcessException if processing did not complete normally though
+     * indicates the problem is an understood potential outcome of processing.
+     * The controller/caller will handle these exceptions gracefully such as
+     * logging, etc.. If another type of exception is allowed to propagate the
+     * controller may no longer trigger this processor to operate, as this would
+     * indicate a probable coding defect.
+     */
+    void onTrigger(ProcessContext context, ProcessSessionFactory sessionFactory) throws ProcessException;
+
+    /**
+     * Indicates whether this processor, configured with the given {@link ProcessContext}, stores state.
+     * @param context provides access to convenience methods for obtaining
+     * property values, delaying the scheduling of the processor, provides
+     * access to Controller Services, etc.
+     * @return True if this processor stores state
+     */
+    default boolean isStateful(ProcessContext context) {
+        return this.getClass().isAnnotationPresent(Stateful.class);
+    }
+
+    /**
+     * <p>
+     *     Allows for the migration of an old property configuration to a new configuration. This allows the Processor to evolve over time,
+     *     as it allows properties to be renamed, removed, or reconfigured.
+     * </p>
+     *
+     * <p>
+     *     This method is called only when a Processor is restored from a previous configuration. For example, when NiFi is restarted and the
+     *     flow is restored from disk, when a previously configured flow is imported (e.g., from a JSON file that was exported or a NiFi Registry),
+     *     or when a node joins a cluster and inherits a flow that has a new Processor. Once called, the method will not be invoked again for this
+     *     Processor until NiFi is restarted.
+     * </p>
+     *
+     * @param config the current property configuration
+     */
+    default void migrateProperties(PropertyConfiguration config) {
+    }
+
+    /**
+     * Allows for the migration of an old relationship configuration to a new configuration
+     *
+     * @param config the current relationship configuration
+     */
+    default void migrateRelationships(RelationshipConfiguration config) {
+    }
+}
diff --git a/src/main/java/org/apache/nifi/processor/ProcessorInitializationContext.java b/src/main/java/org/apache/nifi/processor/ProcessorInitializationContext.java
new file mode 100644
index 0000000..b4c7643
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/ProcessorInitializationContext.java
@@ -0,0 +1,55 @@
+/*
+ * 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.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+
+/**
+ * <p>
+ * The <code>ProcessorInitializationContext</code> provides
+ * {@link org.apache.nifi.processor.Processor Processor}s access to objects that
+ * may be of use throughout the life of the Processor.
+ * </p>
+ */
+public interface ProcessorInitializationContext extends KerberosContext {
+
+    /**
+     * @return the unique identifier for this processor
+     */
+    String getIdentifier();
+
+    /**
+     * @return a {@link ComponentLog} that is tied to this processor that can be
+     * used to log events
+     */
+    ComponentLog getLogger();
+
+    /**
+     * @return the {@link ControllerServiceLookup} which can be used to obtain
+     * Controller Services
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     */
+    NodeTypeProvider getNodeTypeProvider();
+}
diff --git a/src/main/java/org/apache/nifi/processor/Relationship.java b/src/main/java/org/apache/nifi/processor/Relationship.java
new file mode 100644
index 0000000..c15c149
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/Relationship.java
@@ -0,0 +1,144 @@
+/*
+ * 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;
+
+/**
+ * An immutable object for holding information about a type of relationship.
+ *
+ */
+public final class Relationship implements Comparable<Relationship> {
+
+    public static final Relationship SELF = new Relationship.Builder().build();
+    public static final Relationship ANONYMOUS = new Relationship.Builder().name("").build();
+
+    /**
+     * The proper name of the relationship. Determines the relationship
+     * 'identity'
+     */
+    private final String name;
+    /**
+     * A user displayable description of the purpose of this relationship.
+     */
+    private final String description;
+
+    /**
+     * The hash code, which is computed in the constructor because it is hashed
+     * very frequently and the hash code is constant
+     */
+    private final int hashCode;
+
+    /**
+     * The flag which tells the controller to auto terminate this
+     * relationship, so that the processor can be run even if it does
+     * not have connections from this relationship
+     */
+    private final boolean isAutoTerminate;
+
+    protected Relationship(final Builder builder) {
+        this.name = builder.name == null ? null : builder.name.intern();
+        this.description = builder.description;
+        this.isAutoTerminate = builder.autoTerminate;
+        this.hashCode = 301 + ((name == null) ? 0 : this.name.hashCode()); // compute only once, since it gets called a bunch and will never change
+    }
+
+    @Override
+    public int compareTo(final Relationship o) {
+        if (o == null) {
+            return -1;
+        }
+        final String thisName = getName();
+        final String thatName = o.getName();
+        if (thisName == null && thatName == null) {
+            return 0;
+        }
+        if (thisName == null) {
+            return 1;
+        }
+        if (thatName == null) {
+            return -1;
+        }
+
+        return thisName.compareTo(thatName);
+    }
+
+    public static final class Builder {
+
+        private String name = "";
+        private String description = "";
+        private boolean autoTerminate = false;
+
+        public Builder name(final String name) {
+            if (null != name) {
+                this.name = name;
+            }
+            return this;
+        }
+
+        public Builder description(final String description) {
+            if (null != description) {
+                this.description = description;
+            }
+            return this;
+        }
+
+        public Builder autoTerminateDefault(boolean autoTerminate) {
+            this.autoTerminate = autoTerminate;
+            return this;
+        }
+
+        public Relationship build() {
+            return new Relationship(this);
+        }
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getDescription() {
+        return this.description;
+    }
+
+    public boolean isAutoTerminated() {
+        return this.isAutoTerminate;
+    }
+
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof Relationship)) {
+            return false;
+        }
+        if (this == other) {
+            return true;
+        }
+        Relationship desc = (Relationship) other;
+        return this.name.equals(desc.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode;
+    }
+
+    @Override
+    public String toString() {
+        return this.name;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/processor/VerifiableProcessor.java b/src/main/java/org/apache/nifi/processor/VerifiableProcessor.java
new file mode 100644
index 0000000..3421879
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/VerifiableProcessor.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.processor;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ *     Any Processor that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Processor. This allows the Processor to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Processor is not valid, it cannot be started. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when starting a component).
+ * </p>
+ *
+ * <p>
+ *     Verification is allowed to be run only when a Processor is fully stopped. I.e., it has no active threads
+ *     and currently has a state of STOPPED. Therefore, any initialization logic that may need to be performed
+ *     before the Processor is triggered may also be required for verification. However, the framework is not responsible
+ *     for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ProcessContext, ComponentLog, Map)} itself.
+ *     The {@link #verify(ProcessContext, ComponentLog, Map)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableProcessor {
+
+    /**
+     * Verifies that the configuration defined by the given ProcessContext is valid.
+     * @param context the ProcessContext that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     * @param attributes a mapping of values that can be used as FlowFile attributes for the purpose of evaluating Expression Language for resolving property values
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ProcessContext context, ComponentLog verificationLogger, Map<String, String> attributes);
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/exception/FlowFileAccessException.java b/src/main/java/org/apache/nifi/processor/exception/FlowFileAccessException.java
new file mode 100644
index 0000000..c64ea1c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/exception/FlowFileAccessException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.exception;
+
+/**
+ * Indicates an issue occurred while accessing the content of a FlowFile, such
+ * as an IOException,  or obtaining a reference to the FlowFile
+ *
+ */
+public class FlowFileAccessException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public FlowFileAccessException(final String message) {
+        super(message);
+    }
+
+    public FlowFileAccessException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/exception/FlowFileHandlingException.java b/src/main/java/org/apache/nifi/processor/exception/FlowFileHandlingException.java
new file mode 100644
index 0000000..052f256
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/exception/FlowFileHandlingException.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.processor.exception;
+
+/**
+ * Thrown when a flow file is referenced that is not part of the appropriate
+ * session or is not properly accounted for by a transfer or removal within a
+ * session. In any event this exception indicates a logic or programming error
+ * within the processor interacting with the offending session.
+ *
+ */
+public class FlowFileHandlingException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public FlowFileHandlingException(final String message) {
+        super(message);
+    }
+
+    public FlowFileHandlingException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/exception/MissingFlowFileException.java b/src/main/java/org/apache/nifi/processor/exception/MissingFlowFileException.java
new file mode 100644
index 0000000..700afd5
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/exception/MissingFlowFileException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.exception;
+
+/**
+ * Thrown to indicate that the content for some FlowFile could not be found.
+ * This indicates that the data is gone and likely cannot be restored and any
+ * information about the FlowFile should be discarded entirely. This likely
+ * indicates some influence from an external process outside the control of the
+ * framework and it is not something any processor or the framework can recover
+ * so it must discard the object.
+ *
+ */
+public class MissingFlowFileException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public MissingFlowFileException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/exception/ProcessException.java b/src/main/java/org/apache/nifi/processor/exception/ProcessException.java
new file mode 100644
index 0000000..f162b14
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/exception/ProcessException.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.processor.exception;
+
+/**
+ * Indicates an issue occurred in user code while processing a FlowFile.
+ *
+ */
+public class ProcessException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public ProcessException() {
+        super();
+    }
+
+    public ProcessException(final String message) {
+        super(message);
+    }
+
+    public ProcessException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProcessException(final Throwable cause) {
+        super(cause);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/exception/TerminatedTaskException.java b/src/main/java/org/apache/nifi/processor/exception/TerminatedTaskException.java
new file mode 100644
index 0000000..a55ed62
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/exception/TerminatedTaskException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.exception;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.ProcessSessionFactory;
+
+/**
+ * This Exception is thrown whenever a user terminates a Processor and the
+ * Processor subsequently attempts to call a method on a {@link ProcessSession},
+ * {@link ProcessSessionFactory}, {@link ProcessContext}, or an
+ * {@link InputStream} or {@link OutputStream} that were generated from the associated
+ * Process Sessions
+ */
+public class TerminatedTaskException extends ProcessException {
+    public TerminatedTaskException() {
+        super();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/processor/io/InputStreamCallback.java b/src/main/java/org/apache/nifi/processor/io/InputStreamCallback.java
new file mode 100644
index 0000000..9c04581
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/io/InputStreamCallback.java
@@ -0,0 +1,34 @@
+/*
+ * 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.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface InputStreamCallback {
+
+    /**
+     * Provides a managed input stream for use. The input stream is
+     * automatically opened and closed though it is ok to close the stream
+     * manually.
+     *
+     * @param in the stream to read bytes from
+     * @throws IOException if issues reading from the underlying stream
+     */
+    void process(InputStream in) throws IOException;
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/io/OutputStreamCallback.java b/src/main/java/org/apache/nifi/processor/io/OutputStreamCallback.java
new file mode 100644
index 0000000..35dca62
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/io/OutputStreamCallback.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.processor.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface OutputStreamCallback {
+
+    /**
+     * Provides a managed output stream for use. The input stream is
+     * automatically opened and closed though it is ok to close the stream
+     * manually - and quite important if any streams wrapping these streams open
+     * resources which should be cleared.
+     *
+     * @param out the stream to write bytes to
+     * @throws IOException if issues writing to output stream
+     */
+    void process(OutputStream out) throws IOException;
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/io/StreamCallback.java b/src/main/java/org/apache/nifi/processor/io/StreamCallback.java
new file mode 100644
index 0000000..54f0e3b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/io/StreamCallback.java
@@ -0,0 +1,37 @@
+/*
+ * 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.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface StreamCallback {
+
+    /**
+     * Provides a managed output stream for use. The input stream is
+     * automatically opened and closed though it is ok to close the stream
+     * manually - and quite important if any streams wrapping these streams open
+     * resources which should be cleared.
+     *
+     * @param in the stream to read bytes from
+     * @param out the stream to write bytes to
+     * @throws IOException if issues occur reading or writing the underlying streams
+     */
+    void process(InputStream in, OutputStream out) throws IOException;
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/util/StandardValidators.java b/src/main/java/org/apache/nifi/processor/util/StandardValidators.java
new file mode 100644
index 0000000..4ba2431
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/util/StandardValidators.java
@@ -0,0 +1,969 @@
+/*
+ * 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.util;
+
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.Validator;
+import org.apache.nifi.expression.AttributeExpression.ResultType;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.time.DurationFormat;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+public class StandardValidators {
+
+    private StandardValidators() {
+
+    }
+
+    //
+    //
+    // STATICALLY DEFINED VALIDATORS
+    //
+    //
+    public static final Validator ATTRIBUTE_KEY_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            final ValidationResult.Builder builder = new ValidationResult.Builder();
+            builder.subject(subject).input(input);
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return builder.valid(true).explanation("Contains Expression Language").build();
+            }
+
+            try {
+                FlowFile.KeyValidator.validateKey(input);
+                builder.valid(true);
+            } catch (final IllegalArgumentException e) {
+                builder.valid(false).explanation(e.getMessage());
+            }
+
+            return builder.build();
+        }
+    };
+
+    public static final Validator ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            final ValidationResult.Builder builder = new ValidationResult.Builder();
+            builder.subject("Property Name").input(subject);
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return builder.valid(true).explanation("Contains Expression Language").build();
+            }
+
+            try {
+                FlowFile.KeyValidator.validateKey(subject);
+                builder.valid(true);
+            } catch (final IllegalArgumentException e) {
+                builder.valid(false).explanation(e.getMessage());
+            }
+
+            return builder.build();
+        }
+    };
+
+    public static final Validator POSITIVE_INTEGER_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                final int intVal = Integer.parseInt(value);
+
+                if (intVal <= 0) {
+                    reason = "not a positive value";
+                }
+            } catch (final NumberFormatException e) {
+                reason = "not a valid integer";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator POSITIVE_LONG_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                final long longVal = Long.parseLong(value);
+
+                if (longVal <= 0) {
+                    reason = "not a positive value";
+                }
+            } catch (final NumberFormatException e) {
+                reason = "not a valid 64-bit integer";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator NUMBER_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                NumberFormat.getInstance().parse(value);
+            } catch (ParseException e) {
+                reason = "not a valid Number";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator PORT_VALIDATOR = createLongValidator(0, 65535, true);
+
+    /**
+     * {@link Validator} that ensures that value's length > 0
+     */
+    public static final Validator NON_EMPTY_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            return new ValidationResult.Builder().subject(subject).input(value).valid(value != null && !value.isEmpty()).explanation(subject + " cannot be empty").build();
+        }
+    };
+
+    /**
+     * {@link Validator} that ensures that value's length > 0 and that expression language is present
+     */
+    public static final Validator NON_EMPTY_EL_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(String subject, String input, ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+            return NON_EMPTY_VALIDATOR.validate(subject, input, context);
+        }
+    };
+
+    /**
+     * {@link Validator} that ensures that value is a non-empty comma separated list of hostname:port
+     */
+    public static final Validator HOSTNAME_PORT_LIST_VALIDATOR = new Validator() {
+        private static final Validator NON_ZERO_PORT_VALIDATOR = createLongValidator(1, 65535, true);
+
+        @Override
+        public ValidationResult validate(String subject, String input, ValidationContext context) {
+            // expression language
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+
+            // not empty
+            final ValidationResult nonEmptyValidatorResult = NON_EMPTY_VALIDATOR.validate(subject, input, context);
+            if (!nonEmptyValidatorResult.isValid()) {
+                return nonEmptyValidatorResult;
+            }
+
+            // check format
+            final String[] hostnamePortList = input.split(",");
+            for (String hostnamePort : hostnamePortList) {
+                final String[] addresses = hostnamePort.split(":");
+                // Protect against invalid input like http://127.0.0.1:9300 (URL scheme should not be there)
+                if (addresses.length != 2) {
+                    return new ValidationResult.Builder().subject(subject).input(input).explanation(
+                            "Must be in hostname:port form (no scheme such as http://").valid(false).build();
+                }
+
+                // Validate the port
+                final String port = addresses[1].trim();
+                final ValidationResult portValidatorResult = NON_ZERO_PORT_VALIDATOR.validate(subject, port, context);
+                if (!portValidatorResult.isValid()) {
+                    return portValidatorResult;
+                }
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid cluster definition").valid(true).build();
+        }
+    };
+
+    /**
+     * {@link Validator} that ensures that value has 1+ non-whitespace
+     * characters
+     */
+    public static final Validator NON_BLANK_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            return new ValidationResult.Builder().subject(subject).input(value)
+                    .valid(value != null && !value.trim().isEmpty())
+                    .explanation(subject
+                            + " must contain at least one character that is not white space").build();
+        }
+    };
+
+    public static final Validator BOOLEAN_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            final boolean valid = "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value);
+            final String explanation = valid ? null : "Value must be 'true' or 'false'";
+            return new ValidationResult.Builder().subject(subject).input(value).valid(valid).explanation(explanation).build();
+        }
+    };
+
+    public static final Validator INTEGER_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                Integer.parseInt(value);
+            } catch (final NumberFormatException e) {
+                reason = "not a valid integer";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator LONG_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                Long.parseLong(value);
+            } catch (final NumberFormatException e) {
+                reason = "not a valid Long";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator ISO8601_INSTANT_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+
+            try {
+                Instant.parse(input);
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid ISO 8601 Instant Date").valid(true).build();
+            } catch (final Exception e) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid ISO 8601 Instant Date, please enter in UTC time").valid(false).build();
+            }
+        }
+    };
+
+    public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                final int intVal = Integer.parseInt(value);
+
+                if (intVal < 0) {
+                    reason = "value is negative";
+                }
+            } catch (final NumberFormatException e) {
+                reason = "value is not a valid integer";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator CHARACTER_SET_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                final ResultType resultType = context.newExpressionLanguageCompiler().getResultType(value);
+                if (!resultType.equals(ResultType.STRING)) {
+                    return new ValidationResult.Builder()
+                            .subject(subject)
+                            .input(value)
+                            .valid(false)
+                            .explanation("Expected Attribute Query to return type " + ResultType.STRING + " but query returns type " + resultType)
+                            .build();
+                }
+
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            String reason = null;
+            try {
+                if (!Charset.isSupported(value)) {
+                    reason = "Character Set is not supported by this JVM.";
+                }
+            } catch (final UnsupportedCharsetException uce) {
+                reason = "Character Set is not supported by this JVM.";
+            } catch (final IllegalArgumentException iae) {
+                reason = "Character Set value cannot be null.";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    /**
+     * This validator will evaluate an expression using ONLY environment properties,
+     * then validate that the result is a supported character set.
+     */
+    public static final Validator CHARACTER_SET_VALIDATOR_WITH_EVALUATION = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            String evaluatedInput = input;
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                try {
+                    PropertyValue propertyValue = context.newPropertyValue(input);
+                    evaluatedInput = (propertyValue == null) ? input : propertyValue.evaluateAttributeExpressions().getValue();
+                } catch (final Exception e) {
+                    return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid expression").valid(false).build();
+                }
+            }
+
+            String reason = null;
+            try {
+                if (!Charset.isSupported(evaluatedInput)) {
+                    reason = "Character Set is not supported by this JVM.";
+                }
+            } catch (final IllegalArgumentException iae) {
+                reason = "Character Set value is null or is not supported by this JVM.";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(evaluatedInput).explanation(reason).valid(reason == null).build();
+        }
+    };
+
+    public static final Validator SINGLE_CHAR_VALIDATOR = (subject, input, context) -> {
+        if (input == null) {
+            return new ValidationResult.Builder()
+                    .input(input)
+                    .subject(subject)
+                    .valid(false)
+                    .explanation("Input is null for this property")
+                    .build();
+        }
+        if (input.length() != 1) {
+            return new ValidationResult.Builder()
+                    .input(input)
+                    .subject(subject)
+                    .valid(false)
+                    .explanation("Value must be exactly 1 character but was " + input.length() + " in length")
+                    .build();
+        }
+        return new ValidationResult.Builder().input(input).subject(subject).valid(true).build();
+    };
+
+    public static final Validator URL_VALIDATOR = new URLValidator();
+
+    public static final Validator URI_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+
+            try {
+                new URI(input);
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid URI").valid(true).build();
+            } catch (final Exception e) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid URI").valid(false).build();
+            }
+        }
+    };
+
+    public static final Validator URI_LIST_VALIDATOR = (subject, input, context) -> {
+
+        if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+        }
+
+        if (input == null || input.isEmpty()) {
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid URI, value is missing or empty").valid(false).build();
+        }
+
+        Optional<ValidationResult> invalidUri = Arrays.stream(input.split(","))
+                .filter(uri -> uri != null && !uri.trim().isEmpty())
+                .map(String::trim)
+                .map((uri) -> URI_VALIDATOR.validate(subject, uri, context)).filter((uri) -> !uri.isValid()).findFirst();
+
+        return invalidUri.orElseGet(() -> new ValidationResult.Builder().subject(subject).input(input).explanation("Valid URI(s)").valid(true).build());
+    };
+
+    public static final Validator REGULAR_EXPRESSION_VALIDATOR = createRegexValidator(0, Integer.MAX_VALUE, false);
+
+    public static final Validator REGULAR_EXPRESSION_WITH_EL_VALIDATOR = createRegexValidator(0, Integer.MAX_VALUE, true);
+
+    public static final Validator ATTRIBUTE_EXPRESSION_LANGUAGE_VALIDATOR = new Validator() {
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                try {
+                    final String result = context.newExpressionLanguageCompiler().validateExpression(input, true);
+                    if (!isEmpty(result)) {
+                        return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation(result).build();
+                    }
+                } catch (final Exception e) {
+                    return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation(e.getMessage()).build();
+                }
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+        }
+
+    };
+
+    /**
+     * @param value to test
+     * @return true if value is null or empty string; does not trim before
+     * testing
+     */
+    private static boolean isEmpty(final String value) {
+        return value == null || value.length() == 0;
+    }
+
+    public static final Validator TIME_PERIOD_VALIDATOR = new Validator() {
+        private static final Pattern TIME_DURATION_PATTERN = Pattern.compile(DurationFormat.TIME_DURATION_REGEX);
+
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+
+            if (input == null) {
+                return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation("Time Period cannot be null").build();
+            }
+            if (TIME_DURATION_PATTERN.matcher(input.toLowerCase()).matches()) {
+                return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+            } else {
+                return new ValidationResult.Builder()
+                        .subject(subject)
+                        .input(input)
+                        .valid(false)
+                        .explanation("Must be of format <duration> <TimeUnit> where <duration> is a "
+                                + "non-negative integer and TimeUnit is a supported Time Unit, such "
+                                + "as: nanos, millis, secs, mins, hrs, days")
+                        .build();
+            }
+        }
+    };
+
+    public static final Validator DATA_SIZE_VALIDATOR = new Validator() {
+        private static final Pattern DATA_SIZE_PATTERN = Pattern.compile(DataUnit.DATA_SIZE_REGEX);
+
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+
+            if (input == null) {
+                return new ValidationResult.Builder()
+                        .subject(subject)
+                        .input(input)
+                        .valid(false)
+                        .explanation("Data Size cannot be null")
+                        .build();
+            }
+            if (DATA_SIZE_PATTERN.matcher(input.toUpperCase()).matches()) {
+                return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+            } else {
+                return new ValidationResult.Builder()
+                        .subject(subject).input(input)
+                        .valid(false)
+                        .explanation("Must be of format <Data Size> <Data Unit> where <Data Size>"
+                                + " is a non-negative integer and <Data Unit> is a supported Data"
+                                + " Unit, such as: B, KB, MB, GB, TB")
+                        .build();
+            }
+        }
+    };
+
+    public static final Validator FILE_EXISTS_VALIDATOR = new FileExistsValidator(true);
+
+    //
+    //
+    // FACTORY METHODS FOR VALIDATORS
+    //
+    //
+    public static Validator createDirectoryExistsValidator(final boolean allowExpressionLanguage, final boolean createDirectoryIfMissing) {
+        return new DirectoryExistsValidator(allowExpressionLanguage, createDirectoryIfMissing);
+    }
+
+    public static Validator createListValidator(boolean trimEntries, boolean excludeEmptyEntries,
+                                                Validator elementValidator) {
+        return createListValidator(trimEntries, excludeEmptyEntries, elementValidator, false);
+    }
+
+    public static Validator createListValidator(boolean trimEntries, boolean excludeEmptyEntries,
+                                                Validator validator,
+                                                boolean ensureElementValidation) {
+        return (subject, input, context) -> {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+            try {
+                if (input == null) {
+                    return new ValidationResult.Builder().subject(subject).input(null).explanation("List must have at least one non-empty element").valid(false).build();
+                }
+
+                final String[] list = ensureElementValidation ? input.split(",", -1) : input.split(",");
+                if (list.length == 0) {
+                    return new ValidationResult.Builder().subject(subject).input(null).explanation("List must have at least one non-empty element").valid(false).build();
+                }
+
+                for (String item : list) {
+                    String itemToValidate = trimEntries ? item.trim() : item;
+                    if (!isEmpty(itemToValidate) || !excludeEmptyEntries) {
+                        ValidationResult result = validator.validate(subject, itemToValidate, context);
+                        if (!result.isValid()) {
+                            return result;
+                        }
+                    }
+                }
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid List").valid(true).build();
+            } catch (final Exception e) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid list").valid(false).build();
+            }
+        };
+    }
+
+    public static Validator createTimePeriodValidator(final long minTime, final TimeUnit minTimeUnit, final long maxTime, final TimeUnit maxTimeUnit) {
+        return new TimePeriodValidator(minTime, minTimeUnit, maxTime, maxTimeUnit);
+    }
+
+    public static Validator createAttributeExpressionLanguageValidator(final ResultType expectedResultType) {
+        return createAttributeExpressionLanguageValidator(expectedResultType, true);
+    }
+
+    public static Validator createDataSizeBoundsValidator(final long minBytesInclusive, final long maxBytesInclusive) {
+        return new Validator() {
+
+            @Override
+            public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+                if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                    return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+                }
+
+                final ValidationResult vr = DATA_SIZE_VALIDATOR.validate(subject, input, context);
+                if (!vr.isValid()) {
+                    return vr;
+                }
+                final long dataSizeBytes = DataUnit.parseDataSize(input, DataUnit.B).longValue();
+                if (dataSizeBytes < minBytesInclusive) {
+                    return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation("Cannot be smaller than " + minBytesInclusive + " bytes").build();
+                }
+                if (dataSizeBytes > maxBytesInclusive) {
+                    return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation("Cannot be larger than " + maxBytesInclusive + " bytes").build();
+                }
+                return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+            }
+        };
+    }
+
+
+    public static Validator createRegexMatchingValidator(final Pattern pattern) {
+        return createRegexMatchingValidator(pattern, false, "Value does not match regular expression: " + pattern.pattern());
+    }
+
+    public static Validator createRegexMatchingValidator(final Pattern pattern, final boolean evaluateExpressions, final String validationMessage) {
+        return new Validator() {
+            @Override
+            public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+                String value = input;
+                if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                    if (evaluateExpressions) {
+                        try {
+                            value = context.newPropertyValue(input).evaluateAttributeExpressions().getValue();
+                        } catch (final Exception e) {
+                            return new ValidationResult.Builder()
+                                    .subject(subject)
+                                    .input(input)
+                                    .valid(false)
+                                .explanation("Failed to evaluate the Attribute Expression Language due to " + e)
+                                    .build();
+                        }
+                    } else {
+                        return new ValidationResult.Builder()
+                                .subject(subject)
+                                .input(input)
+                                .explanation("Expression Language Present")
+                                .valid(true)
+                                .build();
+                    }
+                }
+
+                final boolean matches = value != null && pattern.matcher(value).matches();
+                return new ValidationResult.Builder()
+                        .input(input)
+                        .subject(subject)
+                        .valid(matches)
+                        .explanation(matches ? null : validationMessage)
+                        .build();
+            }
+        };
+    }
+
+    public static Validator createRegexMatchingValidator(final Pattern pattern, final boolean evaluateExpressions) {
+        return createRegexMatchingValidator(pattern, evaluateExpressions, "Value does not match regular expression: " + pattern.pattern());
+    }
+
+    /**
+     * Creates a @{link Validator} that ensure that a value is a valid Java
+     * Regular Expression with at least <code>minCapturingGroups</code>
+     * capturing groups and at most <code>maxCapturingGroups</code> capturing
+     * groups. If <code>supportAttributeExpressionLanguage</code> is set to
+     * <code>true</code>, the value may also include the Expression Language,
+     * but the result of evaluating the Expression Language will be applied
+     * before the Regular Expression is performed. In this case, the Expression
+     * Language will not support FlowFile Attributes but only System/JVM
+     * Properties
+     *
+     * @param minCapturingGroups                 minimum capturing groups allowed
+     * @param maxCapturingGroups                 maximum capturing groups allowed
+     * @param supportAttributeExpressionLanguage whether or not to support
+     *                                           expression language
+     * @return validator
+     */
+    public static Validator createRegexValidator(final int minCapturingGroups, final int maxCapturingGroups, final boolean supportAttributeExpressionLanguage) {
+        return new Validator() {
+            @Override
+            public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+                try {
+                    final String substituted;
+                    if (supportAttributeExpressionLanguage) {
+                        try {
+                            substituted = context.newPropertyValue(value).evaluateAttributeExpressions().getValue();
+                        } catch (final Exception e) {
+                            return new ValidationResult.Builder()
+                                    .subject(subject)
+                                    .input(value)
+                                    .valid(false)
+                                .explanation("Failed to evaluate the Attribute Expression Language due to " + e)
+                                    .build();
+                        }
+                    } else {
+                        substituted = value;
+                    }
+
+                    final Pattern pattern = Pattern.compile(substituted);
+                    final int numGroups = pattern.matcher("").groupCount();
+                    if (numGroups < minCapturingGroups || numGroups > maxCapturingGroups) {
+                        return new ValidationResult.Builder()
+                                .subject(subject)
+                                .input(value)
+                                .valid(false)
+                                .explanation("RegEx is required to have between " + minCapturingGroups + " and " + maxCapturingGroups + " Capturing Groups but has " + numGroups)
+                                .build();
+                    }
+
+                    return new ValidationResult.Builder().subject(subject).input(value).valid(true).build();
+                } catch (final Exception e) {
+                    return new ValidationResult.Builder()
+                            .subject(subject)
+                            .input(value)
+                            .valid(false)
+                            .explanation("Not a valid Java Regular Expression")
+                            .build();
+                }
+
+            }
+        };
+    }
+
+    public static Validator createAttributeExpressionLanguageValidator(final ResultType expectedResultType, final boolean allowExtraCharacters) {
+        return new Validator() {
+            @Override
+            public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+                final String syntaxError = context.newExpressionLanguageCompiler().validateExpression(input, allowExtraCharacters);
+                if (syntaxError != null) {
+                    return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation(syntaxError).build();
+                }
+
+                final ResultType resultType = allowExtraCharacters ? ResultType.STRING : context.newExpressionLanguageCompiler().getResultType(input);
+                if (!resultType.equals(expectedResultType)) {
+                    return new ValidationResult.Builder()
+                            .subject(subject)
+                            .input(input)
+                            .valid(false)
+                            .explanation("Expected Attribute Query to return type " + expectedResultType + " but query returns type " + resultType)
+                            .build();
+                }
+
+                return new ValidationResult.Builder().subject(subject).input(input).valid(true).build();
+            }
+        };
+    }
+
+    public static Validator createLongValidator(final long minimum, final long maximum, final boolean inclusive) {
+        return new Validator() {
+            @Override
+            public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+                if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                    return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+                }
+
+                String reason = null;
+                try {
+                    final long longVal = Long.parseLong(input);
+                    if (longVal < minimum || (!inclusive && longVal == minimum) | longVal > maximum || (!inclusive && longVal == maximum)) {
+                        reason = "Value must be between " + minimum + " and " + maximum + " (" + (inclusive ? "inclusive" : "exclusive") + ")";
+                    }
+                } catch (final NumberFormatException e) {
+                    reason = "not a valid integer";
+                }
+
+                return new ValidationResult.Builder().subject(subject).input(input).explanation(reason).valid(reason == null).build();
+            }
+
+        };
+    }
+
+    public static Validator createNonNegativeFloatingPointValidator(final double maximum) {
+        return new Validator() {
+            @Override
+            public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+                if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                    return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+                }
+
+                String reason = null;
+                try {
+                    final double doubleValue = Double.parseDouble(input);
+                    if (doubleValue < 0) {
+                        reason = "Value must be non-negative but was " + doubleValue;
+                    }
+                    final double maxPlusDelta = maximum + 0.00001D;
+                    if (doubleValue < 0 || doubleValue > maxPlusDelta) {
+                        reason = "Value must be between 0 and " + maximum + " but was " + doubleValue;
+                    }
+                } catch (final NumberFormatException e) {
+                    reason = "not a valid integer";
+                }
+
+                return new ValidationResult.Builder().subject(subject).input(input).explanation(reason).valid(reason == null).build();
+            }
+
+        };
+    }
+
+    //
+    //
+    // SPECIFIC VALIDATOR IMPLEMENTATIONS THAT CANNOT BE ANONYMOUS CLASSES
+    //
+    //
+    static class TimePeriodValidator implements Validator {
+        private static final Pattern pattern = Pattern.compile(DurationFormat.TIME_DURATION_REGEX);
+
+        private final long minNanos;
+        private final long maxNanos;
+
+        private final String minValueEnglish;
+        private final String maxValueEnglish;
+
+        public TimePeriodValidator(final long minValue, final TimeUnit minTimeUnit, final long maxValue, final TimeUnit maxTimeUnit) {
+            this.minNanos = TimeUnit.NANOSECONDS.convert(minValue, minTimeUnit);
+            this.maxNanos = TimeUnit.NANOSECONDS.convert(maxValue, maxTimeUnit);
+            this.minValueEnglish = minValue + " " + minTimeUnit;
+            this.maxValueEnglish = maxValue + " " + maxTimeUnit;
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+                return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+            }
+
+            if (input == null) {
+                return new ValidationResult.Builder().subject(subject).input(input).valid(false).explanation("Time Period cannot be null").build();
+            }
+            final String lowerCase = input.toLowerCase();
+            final boolean validSyntax = pattern.matcher(lowerCase).matches();
+            final ValidationResult.Builder builder = new ValidationResult.Builder();
+            if (validSyntax) {
+                final long nanos = DurationFormat.getTimeDuration(lowerCase, TimeUnit.NANOSECONDS);
+
+                if (nanos < minNanos || nanos > maxNanos) {
+                    builder.subject(subject).input(input).valid(false)
+                            .explanation("Must be in the range of " + minValueEnglish + " to " + maxValueEnglish);
+                } else {
+                    builder.subject(subject).input(input).valid(true);
+                }
+            } else {
+                builder.subject(subject).input(input).valid(false)
+                        .explanation("Must be of format <duration> <TimeUnit> where <duration> is a non-negative "
+                                + "integer and TimeUnit is a supported Time Unit, such as: nanos, millis, secs, mins, hrs, days");
+            }
+            return builder.build();
+        }
+    }
+
+    public static class FileExistsValidator implements Validator {
+
+        private final boolean allowEL;
+        private final boolean allowFileOnly;
+
+        public FileExistsValidator(final boolean allowExpressionLanguage) {
+            this(allowExpressionLanguage, false);
+        }
+
+        public FileExistsValidator(final boolean allowExpressionLanguage, final boolean fileOnly) {
+            this.allowEL = allowExpressionLanguage;
+            this.allowFileOnly = fileOnly;
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            final String substituted;
+            if (allowEL) {
+                try {
+                    substituted = context.newPropertyValue(value).evaluateAttributeExpressions().getValue();
+                } catch (final Exception e) {
+                    return new ValidationResult.Builder().subject(subject).input(value).valid(false)
+                            .explanation("Not a valid Expression Language value: " + e.getMessage()).build();
+                }
+            } else {
+                substituted = value;
+            }
+
+            final File file = new File(substituted);
+            if (!file.exists()) {
+                return new ValidationResult.Builder().subject(subject).input(value).valid(false).explanation("File " + file + " does not exist").build();
+            }
+            if (allowFileOnly && !file.isFile()) {
+                return new ValidationResult.Builder().subject(subject).input(value).valid(false).explanation(file + " is not a file").build();
+            }
+            return new ValidationResult.Builder().subject(subject).input(value).valid(true).build();
+        }
+    }
+
+    public static class StringLengthValidator implements Validator {
+
+        private final int minimum;
+        private final int maximum;
+
+        public StringLengthValidator(int minimum, int maximum) {
+            this.minimum = minimum;
+            this.maximum = maximum;
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (value.length() < minimum || value.length() > maximum) {
+                return new ValidationResult.Builder()
+                        .subject(subject)
+                        .valid(false)
+                        .input(value)
+                        .explanation(String.format("String length invalid [min: %d, max: %d]", minimum, maximum))
+                        .build();
+            } else {
+                return new ValidationResult.Builder()
+                        .valid(true)
+                        .input(value)
+                        .subject(subject)
+                        .build();
+            }
+        }
+    }
+
+    public static class DirectoryExistsValidator implements Validator {
+
+        private final boolean allowEL;
+        private final boolean create;
+
+        public DirectoryExistsValidator(final boolean allowExpressionLanguage, final boolean create) {
+            this.allowEL = allowExpressionLanguage;
+            this.create = create;
+        }
+
+        @Override
+        public ValidationResult validate(final String subject, final String value, final ValidationContext context) {
+            if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(value)) {
+                return new ValidationResult.Builder().subject(subject).input(value).explanation("Expression Language Present").valid(true).build();
+            }
+
+            final String substituted;
+            if (allowEL) {
+                try {
+                    substituted = context.newPropertyValue(value).evaluateAttributeExpressions().getValue();
+                } catch (final Exception e) {
+                    return new ValidationResult.Builder().subject(subject).input(value).valid(false)
+                            .explanation("Not a valid Expression Language value: " + e.getMessage()).build();
+                }
+
+                if (substituted.trim().isEmpty() && !value.trim().isEmpty()) {
+                    // User specified an Expression and nothing more... assume valid.
+                    return new ValidationResult.Builder().subject(subject).input(value).valid(true).build();
+                }
+            } else {
+                substituted = value;
+            }
+
+            String reason = null;
+            try {
+                final File file = new File(substituted);
+                if (!file.exists()) {
+                    if (!create) {
+                        reason = "Directory does not exist";
+                    } else if (!file.mkdirs()) {
+                        reason = "Directory does not exist and could not be created";
+                    }
+                } else if (!file.isDirectory()) {
+                    reason = "Path does not point to a directory";
+                }
+            } catch (final Exception e) {
+                reason = "Value is not a valid directory name";
+            }
+
+            return new ValidationResult.Builder().subject(subject).input(value).explanation(reason).valid(reason == null).build();
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/processor/util/URLValidator.java b/src/main/java/org/apache/nifi/processor/util/URLValidator.java
new file mode 100644
index 0000000..95d5322
--- /dev/null
+++ b/src/main/java/org/apache/nifi/processor/util/URLValidator.java
@@ -0,0 +1,100 @@
+/*
+ * 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.util;
+
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.Validator;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class URLValidator implements Validator {
+
+    private static final String SCHEME_PATTERN = "([^:/?#]+):";
+    private static final String USERINFO_PATTERN = "([^@\\[/?#]*)";
+    private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*";
+    private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]";
+    private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")";
+    private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#]*)";
+    private static final String PATH_PATTERN = "([^?#]*)";
+    private static final String QUERY_PATTERN = "([^#]*)";
+    private static final String LAST_PATTERN = "(.*)";
+
+    // Regex patterns that matches URIs. See RFC 3986, appendix B
+    private static final Pattern URI_PATTERN = Pattern.compile(
+        "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
+        ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
+
+
+    @Override
+    public ValidationResult validate(final String subject, final String input, final ValidationContext context) {
+        if (context.isExpressionLanguageSupported(subject) && context.isExpressionLanguagePresent(input)) {
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Expression Language Present").valid(true).build();
+        }
+
+        try {
+            // Check that we can parse the value as a URL
+            final String evaluatedInput = context.newPropertyValue(input).evaluateAttributeExpressions().getValue();
+            createURL(evaluatedInput);
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid URL").valid(true).build();
+        } catch (final Exception e) {
+            return new ValidationResult.Builder().subject(subject).input(input).explanation("Not a valid URL").valid(false).build();
+        }
+    }
+
+    /**
+     * This method provides an alternative to the use of java.net.URI's single argument constructor and 'create' method.
+     * The drawbacks of the java.net.URI's single argument constructor and 'create' method are:
+     *   <ul>
+     *      <li>They do not provide quoting in the path section for any character not in the unreserved, punct, escaped, or other categories,
+     *          and not equal to the slash character ('/') or the commercial-at character ('{@literal @}').</li>
+     *      <li>They do not provide quoting for any illegal characters found in the query and fragment sections.</li>
+     *  </ul>
+     *  On the other hand, java.net.URI's seven argument constructor provides these quoting capabilities. In order
+     *  to take advantage of this constructor, this method parses the given string into the arguments needed
+     *  thereby allowing for instantiating a java.net.URI with the quoting of all illegal characters.
+     * @param url String representing a URL.
+     * @return Instance of java.net.URL
+     * @throws MalformedURLException if unable to create a URL from the given String representation
+     */
+    public static URL createURL(final String url) throws MalformedURLException {
+        final Matcher matcher = URI_PATTERN.matcher(url);
+        if (matcher.matches()) {
+            final String scheme = matcher.group(2);
+            final String userInfo = matcher.group(5);
+            final String host = matcher.group(6);
+            final String port = matcher.group(8);
+            final String path = matcher.group(9);
+            final String query = matcher.group(11);
+            final String fragment = matcher.group(13);
+
+            try {
+                return new URI(scheme, userInfo, host, port != null ? Integer.parseInt(port) : -1, path, query, fragment).toURL();
+            } catch (final URISyntaxException e) {
+                throw new MalformedURLException("Unable to create URL from " + url  + ": " + e.getMessage());
+            }
+        } else {
+            throw new MalformedURLException(url + " is not a valid URL");
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/provenance/ProvenanceEventBuilder.java b/src/main/java/org/apache/nifi/provenance/ProvenanceEventBuilder.java
new file mode 100644
index 0000000..0d212c2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/provenance/ProvenanceEventBuilder.java
@@ -0,0 +1,326 @@
+/*
+ * 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.provenance;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.Relationship;
+
+public interface ProvenanceEventBuilder {
+
+    /**
+     * Sets the type of {@link ProvenanceEventRecord}
+     *
+     * @param eventType of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setEventType(ProvenanceEventType eventType);
+
+    /**
+     * Populates the values of the Event being built from the values in the
+     * given event
+     *
+     * @param event the event from which to populate the Builders values
+     * @return the builder
+     */
+    ProvenanceEventBuilder fromEvent(ProvenanceEventRecord event);
+
+    /**
+     * Sets the date and time at which the FlowFile entered the flow
+     *
+     * @param entryDate of the flow file
+     * @return the builder
+     */
+    ProvenanceEventBuilder setFlowFileEntryDate(long entryDate);
+
+    /**
+     * Sets the Content Claim that the FlowFile was previously associated with
+     * before this event occurred.
+     *
+     * @param container for previous content
+     * @param section for previous content
+     * @param identifier for previous content
+     * @param offset for previous content
+     * @param size for previous content
+     * @return the builder
+     */
+    ProvenanceEventBuilder setPreviousContentClaim(String container, String section, String identifier, Long offset, long size);
+
+    /**
+     * Sets the Content Claim that the FlowFile is associated with as a result
+     * of this event
+     *
+     * @param container for resulting content
+     * @param section for resulting content
+     * @param identifier for resulting content
+     * @param offset for resulting content
+     * @param size for resulting content
+     * @return the builder
+     */
+    ProvenanceEventBuilder setCurrentContentClaim(String container, String section, String identifier, Long offset, long size);
+
+    /**
+     * Sets the identifier of the FlowFile Queue from which the FlowFile was
+     * pulled
+     *
+     * @param identifier of the source queue
+     * @return the builder
+     */
+    ProvenanceEventBuilder setSourceQueueIdentifier(String identifier);
+
+    /**
+     * Sets the attributes that existed on the FlowFile before this event
+     * occurred and any attributes that were added or updated as a result of
+     * this event.
+     *
+     * @param previousAttributes Map of all attributes before the event occurred
+     * @param updatedAttributes Map containing all attributes that were added or
+     * updated. If any entry has a value of <code>null</code>, that attribute is
+     * considered removed
+     *
+     * @return the builder
+     */
+    ProvenanceEventBuilder setAttributes(Map<String, String> previousAttributes, Map<String, String> updatedAttributes);
+
+    /**
+     * Sets the UUID to associate with the FlowFile
+     *
+     * @param uuid of the flowfile
+     * @return the builder
+     */
+    ProvenanceEventBuilder setFlowFileUUID(String uuid);
+
+    /**
+     * Sets the time at which the Provenance Event took place
+     *
+     * @param eventTime time of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setEventTime(long eventTime);
+
+    /**
+     * Sets the amount of time that was required in order to perform the
+     * function referred to by this event
+     *
+     * @param millis of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setEventDuration(long millis);
+
+    /**
+     * Sets the time at which the FlowFile's lineage began
+     *
+     * @param startDate start date of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setLineageStartDate(long startDate);
+
+    /**
+     * Sets the unique identifier of the NiFi Component (such as a
+     * {@link Processor}) that is generating the Event
+     *
+     * @param componentId that produced the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setComponentId(String componentId);
+
+    /**
+     * Sets the type of the Component that is generating the Event. For
+     * {@link Processor}s, this is the Simple Class Name of the Processor.
+     *
+     * @param componentType of the component that made the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setComponentType(String componentType);
+
+    /**
+     * Sets the identifier that is used by the remote system to refer to the
+     * FlowFile for which this Event is being created.
+     *
+     * This is valid only for Event Types {@link ProvenanceEventType#RECEIVE}
+     * and {@link ProvenanceEventType#SEND} and will be ignored for any other
+     * event types.
+     *
+     * @param sourceSystemFlowFileIdentifier identifier the remote system used
+     * @return the builder
+     */
+    ProvenanceEventBuilder setSourceSystemFlowFileIdentifier(String sourceSystemFlowFileIdentifier);
+
+    /**
+     * Sets the Transit URI that is used for the Event. This is a URI that
+     * provides information about the System and Protocol information over which
+     * the transfer occurred. The intent of this field is such that both the
+     * sender and the receiver can publish the events to an external
+     * Enterprise-wide system that is then able to correlate the SEND and
+     * RECEIVE events.
+     *
+     * This is valid only for Event Types {@link ProvenanceEventType#RECEIVE}
+     * and {@link ProvenanceEventType#SEND} and will be ignored for any other
+     * event types.
+     *
+     * @param transitUri of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setTransitUri(String transitUri);
+
+    /**
+     * Adds the given FlowFile as a parent for Events of type,
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE}
+     *
+     * This is valid only for
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE} events and will be ignored for any
+     * other event types.
+     *
+     * @param parent flowfile that this event is derived from
+     * @return the builder
+     */
+    ProvenanceEventBuilder addParentFlowFile(FlowFile parent);
+
+    /**
+     * Removes the given FlowFile as a parent for Events of type,
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE}
+     *
+     * This is valid only for
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE} events and will be ignored for any
+     * other event types.
+     *
+     * @param parent previous parent of this event
+     * @return the builder
+     */
+    ProvenanceEventBuilder removeParentFlowFile(FlowFile parent);
+
+    /**
+     * Adds the given FlowFile as a child for Events of type
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE}
+     *
+     * This is valid only for
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE} events and will be ignored for any
+     * other event types.
+     *
+     * @param child the child to add
+     * @return the builder
+     */
+    ProvenanceEventBuilder addChildFlowFile(FlowFile child);
+
+    /**
+     * Adds the given FlowFile identifier as a child for Events of type
+     * {@link ProvenanceEventType#FORK} and {@link ProvenanceEventType#CLONE}
+     *
+     * @param childId the ID of the FlowFile that is a child
+     * @return the builder
+     */
+    ProvenanceEventBuilder addChildFlowFile(String childId);
+
+    /**
+     * Removes the given FlowFile as a child for Events of type
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE}
+     *
+     * This is valid only for
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, and
+     * {@link ProvenanceEventType#CLONE} events and will be ignored for any
+     * other event types.
+     *
+     * @param child to remove
+     * @return the builder
+     */
+    ProvenanceEventBuilder removeChildFlowFile(FlowFile child);
+
+    /**
+     * Associates the given identifier with the FlowFile for which this Event is
+     * created. This provides a linkage between the given FlowFile and alternate
+     * identifier. This information can be useful if published to an external,
+     * enterprise-wide Provenance tracking system that is able to associate the
+     * data between different processes or services.
+     *
+     * This is valid only for {@link ProvenanceEventType#ADDINFO} events and
+     * will be ignored for any other event types.
+     *
+     * @param alternateIdentifierUri another identifier of the flowfile this event is for
+     * @return the builder
+     */
+    ProvenanceEventBuilder setAlternateIdentifierUri(String alternateIdentifierUri);
+
+    /**
+     * Sets the details for this event. This is a free-form String that can
+     * contain any information that is relevant to this event.
+     *
+     * @param details a description of the event
+     * @return the builder
+     */
+    ProvenanceEventBuilder setDetails(String details);
+
+    /**
+     * Sets the to which the FlowFile was routed for
+     * {@link ProvenanceEventType#ROUTE} events. This is valid only for
+     * {@link ProvenanceEventType#ROUTE} events and will be ignored for any
+     * other event types.
+     *
+     * @param relationship to which flowfiles in this event were routed
+     * @return the builder
+     */
+    ProvenanceEventBuilder setRelationship(Relationship relationship);
+
+    /**
+     * Populates the builder with as much information as it can from the given
+     * FlowFile
+     *
+     * @param flowFile to source attributes for this event from
+     * @return the builder
+     */
+    ProvenanceEventBuilder fromFlowFile(FlowFile flowFile);
+
+    /**
+     * Builds the Provenance Event. It is possible that calling
+     * {@link ProvenanceEventRecord#getEventId()} on the
+     * {@link ProvenanceEventRecord} that is returned will yield
+     * <code>-1</code>. This is because the implementation of the Event may
+     * depend on the Provevenance Event Repository to generate the unique
+     * identifier.
+     *
+     * @return the event
+     */
+    ProvenanceEventRecord build();
+
+    /**
+     * @return the ids of all FlowFiles that have been added as children via {@link #addChildFlowFile(FlowFile)}
+     */
+    List<String> getChildFlowFileIds();
+
+    /**
+     * @return the ids of all FlowFiles that have been added as parents via {@link #addParentFlowFile(FlowFile)}
+     */
+    List<String> getParentFlowFileIds();
+
+    /**
+     * @return the id of the FlowFile for which the event is being built
+     */
+    String getFlowFileId();
+
+    /**
+     * @return a new Provenance Event Builder that is identical to this one (a deep copy)
+     */
+    ProvenanceEventBuilder copy();
+}
diff --git a/src/main/java/org/apache/nifi/provenance/ProvenanceEventRecord.java b/src/main/java/org/apache/nifi/provenance/ProvenanceEventRecord.java
new file mode 100644
index 0000000..b781afe
--- /dev/null
+++ b/src/main/java/org/apache/nifi/provenance/ProvenanceEventRecord.java
@@ -0,0 +1,248 @@
+/*
+ * 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.provenance;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Describes an event that happened to a FlowFile.
+ */
+public interface ProvenanceEventRecord {
+
+    String REMOTE_INPUT_PORT_TYPE = "Remote Input Port";
+    String REMOTE_OUTPUT_PORT_TYPE = "Remote Output Port";
+
+    /**
+     * @return a unique ID for this Provenance Event. Depending on the
+     * implementation, the Event ID may be set to -1 until the event has been
+     * added to the {@link ProvenanceEventRepository}
+     */
+    long getEventId();
+
+    /**
+     * @return the time at which this Provenance Event was created, as the
+     * number of milliseconds since epoch
+     */
+    long getEventTime();
+
+    /**
+     * @return the EntryDate of the FlowFile to which this Event is associated
+     */
+    long getFlowFileEntryDate();
+
+    /**
+     * @return the time at which the lineage began
+     */
+    long getLineageStartDate();
+
+    /**
+     * @return the size of the FlowFile to which this Event is associated
+     */
+    long getFileSize();
+
+    /**
+     * @return the previous size of the FlowFile to which this Event is
+     * associated, if the FlowFile previously had content and its size was
+     * known; otherwise, returns <code>null</code>
+     */
+    Long getPreviousFileSize();
+
+    /**
+     * @return the amount of time in milliseconds that elapsed while performing
+     * this event. If not populated, the value -1 will be returned
+     */
+    long getEventDuration();
+
+    /**
+     * @return the type of this Provenance Event
+     */
+    ProvenanceEventType getEventType();
+
+    /**
+     * @return all FlowFile attributes that were associated with the FlowFile at
+     * the time that this ProvenanceEvent was created
+     */
+    Map<String, String> getAttributes();
+
+    /**
+     * Returns the attribute with the given name
+     *
+     * @param attributeName the name of the attribute to get
+     * @return the attribute with the given name or <code>null</code> if no attribute exists with the given name
+     */
+    default String getAttribute(String attributeName) {
+        return getAttributes().get(attributeName);
+    }
+
+    /**
+     * @return all FlowFile attributes that existed on the FlowFile before this
+     * event occurred
+     */
+    Map<String, String> getPreviousAttributes();
+
+    /**
+     * @return all FlowFile attributes that were updated as a result of this
+     * event
+     */
+    Map<String, String> getUpdatedAttributes();
+
+    /**
+     * @return the ID of the Processor/component that created this Provenance
+     * Event
+     */
+    String getComponentId();
+
+    /**
+     * @return the fully-qualified Class Name of the Processor/component that
+     * created this Provenance Event
+     */
+    String getComponentType();
+
+    /**
+     * @return whether this event originated from a remote group port
+     */
+    default boolean isRemotePortType() {
+        final String componentType = getComponentType();
+        return REMOTE_INPUT_PORT_TYPE.equals(componentType) || REMOTE_OUTPUT_PORT_TYPE.equals(componentType);
+    }
+
+    /**
+     * @return a URI that provides information about the System and Protocol
+     * information over which the transfer occurred. The intent of this field is
+     * such that both the sender and the receiver can publish the events to an
+     * external Enterprise-wide system that is then able to correlate the SEND
+     * and RECEIVE events.
+     */
+    String getTransitUri();
+
+    /**
+     * Since the receiving system will usually refer to the data using a
+     * different identifier than the source system, this information is used to
+     * correlate the receive system's FlowFile with the sending system's data
+     *
+     * @return the UUID that the Source System used to refer to this data; this
+     * is applicable only when the {@link ProvenanceEventType} is of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE}
+     */
+    String getSourceSystemFlowFileIdentifier();
+
+    /**
+     * @return the UUID of the FlowFile with which this Event is associated
+     */
+    String getFlowFileUuid();
+
+    /**
+     * @return the UUID's of all Parent FlowFiles. This is applicable only when
+     * the {@link ProvenanceEventType} is of type
+     * {@link ProvenanceEventType#CREATE CREATE}
+     */
+    List<String> getParentUuids();
+
+    /**
+     * @return the UUID's of all Child FlowFiles. This is applicable only when
+     * the {@link ProvenanceEventType} is of type
+     * {@link ProvenanceEventType#CREATE CREATE}
+     */
+    List<String> getChildUuids();
+
+    /**
+     * @return the Alternate Identifier associated with the FlowFile with which
+     * this Event is associated. This is applicable only when the
+     * {@link ProvenanceEventType} is of type
+     * {@link ProvenanceEventType#ADDINFO}
+     */
+    String getAlternateIdentifierUri();
+
+    /**
+     * @return the details for this record, if any were supplied. Otherwise,
+     * returns <code>null</code>
+     */
+    String getDetails();
+
+    /**
+     * @return the relationship to which this record was routed if the event
+     * type is {@link ProvenanceEventType#ROUTE}. The relationship is applicable
+     * only to this type
+     */
+    String getRelationship();
+
+    /**
+     * @return the identifier of the queue from which the FlowFile was taken, if
+     * any. If the FlowFile is created as a result of this event (in this case,
+     * the Event Type is one of null null null null null null null null     {@link ProvenanceEventType#CREATE}, {@link ProvenanceEventType#RECEIVE},
+     * {@link ProvenanceEventType#FORK}, {@link ProvenanceEventType#JOIN}, or
+     * {@link ProvenanceEventType#CLONE}), or if the queue identifier is
+     * unknown, then this method will return <code>null</code>
+     */
+    String getSourceQueueIdentifier();
+
+    /**
+     * @return the Section for the Content Claim that this Event refers to, if
+     * any; otherwise, returns <code>null</code>
+     */
+    String getContentClaimSection();
+
+    /**
+     * @return the Section for the Content Claim that the FlowFile previously
+     * referenced, if any; otherwise, returns <code>null</code>
+     */
+    String getPreviousContentClaimSection();
+
+    /**
+     * @return the Container for the Content Claim that this Event refers to, if
+     * any; otherwise, returns <code>null</code>
+     */
+    String getContentClaimContainer();
+
+    /**
+     * @return the Container for the Content Claim that the FlowFile previously
+     * referenced, if any; otherwise, returns <code>null</code>
+     */
+    String getPreviousContentClaimContainer();
+
+    /**
+     * @return the Identifier for the Content Claim that this Event refers to,
+     * if any; otherwise, returns <code>null</code>
+     */
+    String getContentClaimIdentifier();
+
+    /**
+     * @return the Identifier for the Content Claim that the FlowFile previously
+     * referenced, if any; otherwise, returns <code>null</code>
+     */
+    String getPreviousContentClaimIdentifier();
+
+    /**
+     * @return the offset into the Content Claim at which the FlowFile's content
+     * begins, if any; otherwise, returns <code>null</code>
+     */
+    Long getContentClaimOffset();
+
+    /**
+     * @return the offset into the Content Claim at which the FlowFile's
+     * previous content began, if any; otherwise, returns <code>null</code>
+     */
+    Long getPreviousContentClaimOffset();
+
+    /**
+     * Returns the best event identifier for this event (eventId if available, descriptive identifier if not yet persisted to allow for traceability).
+     *
+     * @return a descriptive event ID to allow tracing
+     */
+    String getBestEventIdentifier();
+}
diff --git a/src/main/java/org/apache/nifi/provenance/ProvenanceEventRepository.java b/src/main/java/org/apache/nifi/provenance/ProvenanceEventRepository.java
new file mode 100644
index 0000000..e9d3b14
--- /dev/null
+++ b/src/main/java/org/apache/nifi/provenance/ProvenanceEventRepository.java
@@ -0,0 +1,97 @@
+/*
+ * 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.provenance;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * This Repository houses Provenance Events. The repository is responsible for
+ * managing the life-cycle of the events, providing access to the events that it
+ * has stored, and providing query capabilities against the events.
+ *
+ */
+public interface ProvenanceEventRepository {
+
+    /**
+     * Returns a {@link ProvenanceEventBuilder} that is capable of building
+     * {@link ProvenanceEventRecord}s
+     *
+     * @return builder
+     */
+    ProvenanceEventBuilder eventBuilder();
+
+    /**
+     * Adds the given event to the repository and returns a new event for which
+     * the event id has been populated. Depending on the implementation, the
+     * returned event may or may not be the same event given
+     *
+     * @param event to register
+     */
+    void registerEvent(ProvenanceEventRecord event);
+
+    /**
+     * Adds the given events to the repository.
+     *
+     * <p>
+     * This interface makes no assumptions about whether or not the registration
+     * of the Collection are atomic. This detail is implementation-specific.
+     * </p>
+     *
+     * @param events to register
+     */
+    void registerEvents(Iterable<ProvenanceEventRecord> events);
+
+    /**
+     * Returns a List of all <code>ProvenanceEventRecord</code>s in the
+     * repository starting with the given ID. The first ID in the repository
+     * will always be 0 or higher. This method performs no authorization of
+     * the events.
+     *
+     * @param firstRecordId id of the first record to retrieve
+     * @param maxRecords maximum number of records to retrieve
+     * @return records
+     * @throws java.io.IOException if error reading from repository
+     */
+    List<ProvenanceEventRecord> getEvents(long firstRecordId, final int maxRecords) throws IOException;
+
+
+    /**
+     * @return the largest ID of any event that is queryable in the repository.
+     *         If no queryable events exists, returns null
+     */
+    Long getMaxEventId();
+
+    /**
+     * Retrieves the Provenance Event with the given ID.
+     *
+     * @param id to lookup
+     * @return the Provenance Event Record with the given ID, if it exists, or
+     *         {@code null} otherwise
+     * @throws IOException if failure while retrieving event
+     */
+    ProvenanceEventRecord getEvent(long id) throws IOException;
+
+    /**
+     * Closes the repository, freeing any resources
+     *
+     * @throws IOException if failure closing repository
+     */
+    void close() throws IOException;
+
+
+}
diff --git a/src/main/java/org/apache/nifi/provenance/ProvenanceEventType.java b/src/main/java/org/apache/nifi/provenance/ProvenanceEventType.java
new file mode 100644
index 0000000..b5a8aff
--- /dev/null
+++ b/src/main/java/org/apache/nifi/provenance/ProvenanceEventType.java
@@ -0,0 +1,139 @@
+/*
+ * 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.provenance;
+
+public enum ProvenanceEventType {
+
+    /**
+     * A CREATE event is used when a FlowFile is generated from data that was
+     * not received from a remote system or external process
+     */
+    CREATE,
+
+    /**
+     * Indicates a provenance event for receiving data from an external process. This Event Type
+     * is expected to be the first event for a FlowFile. As such, a Processor that receives data
+     * from an external source and uses that data to replace the content of an existing FlowFile
+     * should use the {@link #FETCH} event type, rather than the RECEIVE event type.
+     */
+    RECEIVE,
+
+    /**
+     * Indicates that the contents of a FlowFile were overwritten using the contents of some
+     * external resource. This is similar to the {@link #RECEIVE} event but varies in that
+     * RECEIVE events are intended to be used as the event that introduces the FlowFile into
+     * the system, whereas FETCH is used to indicate that the contents of an existing FlowFile
+     * were overwritten.
+     */
+    FETCH,
+
+    /**
+     * Indicates a provenance event for sending data to an external process
+     */
+    SEND,
+
+    /**
+     * Indicates a provenance event for sending data from a filesystem to an external process
+     */
+    UPLOAD,
+
+    /**
+     * Indicates a provenance event for sending remote invocation request to an external process.
+     * This event type is used to represent other operations than transferring data (RECEIVE, FETCH or SEND),
+     * for example, deleting object from an external process or storage.
+     */
+    REMOTE_INVOCATION,
+
+    /**
+     * Indicates that the contents of a FlowFile were downloaded by a user or external entity.
+     */
+    DOWNLOAD,
+
+    /**
+     * Indicates a provenance event for the conclusion of an object's life for
+     * some reason other than object expiration
+     */
+    DROP,
+
+    /**
+     * Indicates a provenance event for the conclusion of an object's life due
+     * to the fact that the object could not be processed in a timely manner
+     */
+    EXPIRE,
+
+    /**
+     * FORK is used to indicate that one or more FlowFile was derived from a
+     * parent FlowFile.
+     */
+    FORK,
+
+    /**
+     * JOIN is used to indicate that a single FlowFile is derived from joining
+     * together multiple parent FlowFiles.
+     */
+    JOIN,
+
+    /**
+     * CLONE is used to indicate that a FlowFile is an exact duplicate of its
+     * parent FlowFile.
+     */
+    CLONE,
+
+    /**
+     * CONTENT_MODIFIED is used to indicate that a FlowFile's content was
+     * modified in some way. When using this Event Type, it is advisable to
+     * provide details about how the content is modified.
+     */
+    CONTENT_MODIFIED,
+
+    /**
+     * ATTRIBUTES_MODIFIED is used to indicate that a FlowFile's attributes were
+     * modified in some way. This event is not needed when another event is
+     * reported at the same time, as the other event will already contain all
+     * FlowFile attributes.
+     */
+    ATTRIBUTES_MODIFIED,
+
+    /**
+     * ROUTE is used to show that a FlowFile was routed to a specified
+     * {@link org.apache.nifi.processor.Relationship Relationship} and should provide
+     * information about why the FlowFile was routed to this relationship.
+     */
+    ROUTE,
+
+    /**
+     * Indicates a provenance event for adding additional information such as a
+     * new linkage to a new URI or UUID
+     */
+    ADDINFO,
+
+    /**
+     * Indicates a provenance event for replaying a FlowFile. The UUID of the
+     * event will indicate the UUID of the original FlowFile that is being
+     * replayed. The event will contain exactly one Parent UUID that is also the
+     * UUID of the FlowFile that is being replayed and exactly one Child UUID
+     * that is the UUID of the a newly created FlowFile that will be re-queued
+     * for processing.
+     */
+    REPLAY,
+
+    /**
+     * Indicates that the type of the provenance event is unknown because the user
+     * who is attempting to access the event is not authorize to know the type.
+     */
+    UNKNOWN;
+}
diff --git a/src/main/java/org/apache/nifi/provenance/ProvenanceReporter.java b/src/main/java/org/apache/nifi/provenance/ProvenanceReporter.java
new file mode 100644
index 0000000..a29a275
--- /dev/null
+++ b/src/main/java/org/apache/nifi/provenance/ProvenanceReporter.java
@@ -0,0 +1,749 @@
+/*
+ * 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.provenance;
+
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+
+import java.util.Collection;
+
+/**
+ * ProvenanceReporter generates and records Provenance-related events. A
+ * ProvenanceReporter is always tied to a {@link ProcessSession}. Any events
+ * that are generated are reported to Provenance only after the session has been
+ * committed. If the session is rolled back, the events related to that session
+ * are purged.
+ */
+public interface ProvenanceReporter {
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE} that indicates that the given
+     * FlowFile was created from data received from an external source.
+     *
+     * @param flowFile the FlowFile that was received
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     */
+    void receive(FlowFile flowFile, String transitUri);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE} that indicates that the given
+     * FlowFile was created from data received from the specified URI and that
+     * the source system used the specified identifier (a URI with namespace) to
+     * refer to the data.
+     *
+     * @param flowFile the FlowFile that was received
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param sourceSystemFlowFileIdentifier the URI/identifier that the source
+     * system uses to refer to the data; if this value is non-null and is not a
+     * URI, the prefix "urn:tdo:" will be used to form a URI.
+     */
+    void receive(FlowFile flowFile, String transitUri, String sourceSystemFlowFileIdentifier);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE} that indicates that the given
+     * FlowFile was created from data received from an external source.
+     *
+     * @param flowFile the FlowFile that was received
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param transmissionMillis the number of milliseconds taken to transfer
+     * the data
+     */
+    void receive(FlowFile flowFile, String transitUri, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE} that indicates that the given
+     * FlowFile was created from data received from an external source and
+     * provides additional details about the receipt of the FlowFile, such as a
+     * remote system's Distinguished Name.
+     *
+     * @param flowFile the FlowFile that was received
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details details about the receive event; for example, it may be
+     * relevant to include the DN of the sending system
+     * @param transmissionMillis the number of milliseconds taken to transfer
+     * the data
+     */
+    void receive(FlowFile flowFile, String transitUri, String details, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#RECEIVE RECEIVE} that indicates that the given
+     * FlowFile was created from data received from an external source and
+     * provides additional details about the receipt of the FlowFile, such as a
+     * remote system's Distinguished Name.
+     *
+     * @param flowFile the FlowFile that was received
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param sourceSystemFlowFileIdentifier the URI/identifier that the source
+     * system uses to refer to the data; if this value is non-null and is not a
+     * URI, the prefix "urn:tdo:" will be used to form a URI.
+     * @param details details about the receive event; for example, it may be
+     * relevant to include the DN of the sending system
+     * @param transmissionMillis the number of milliseconds taken to transfer
+     * the data
+     */
+    void receive(FlowFile flowFile, String transitUri, String sourceSystemFlowFileIdentifier, String details, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#FETCH FETCH} that indicates that the content of the given
+     * FlowFile was overwritten with the data received from an external source.
+     *
+     * @param flowFile the FlowFile whose content was replaced
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred.
+     */
+    void fetch(FlowFile flowFile, String transitUri);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#FETCH FETCH} that indicates that the content of the given
+     * FlowFile was overwritten with the data received from an external source.
+     *
+     * @param flowFile the FlowFile whose content was replaced
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred.
+     * @param transmissionMillis the number of milliseconds taken to transfer the data
+     */
+    void fetch(FlowFile flowFile, String transitUri, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#FETCH FETCH} that indicates that the content of the given
+     * FlowFile was overwritten with the data received from an external source.
+     *
+     * @param flowFile the FlowFile whose content was replaced
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred.
+     * @param details details about the event
+     * @param transmissionMillis the number of milliseconds taken to transfer
+     * the data
+     */
+    void fetch(FlowFile flowFile, String transitUri, String details, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     */
+    void send(FlowFile flowFile, String transitUri);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details additional details related to the SEND event, such as a
+     * remote system's Distinguished Name
+     */
+    void send(FlowFile flowFile, String transitUri, String details);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param transmissionMillis the number of milliseconds spent sending the
+     * data to the remote system
+     */
+    void send(FlowFile flowFile, String transitUri, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details additional details related to the SEND event, such as a
+     * remote system's Distinguished Name
+     * @param transmissionMillis the number of milliseconds spent sending the
+     * data to the remote system
+     */
+    void send(FlowFile flowFile, String transitUri, String details, long transmissionMillis);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param force if <code>true</code>, this event will be added to the
+     * Provenance Repository immediately and will still be persisted if the
+     * {@link org.apache.nifi.processor.ProcessSession ProcessSession} to which this
+     * ProvenanceReporter is associated is rolled back. Otherwise, the Event
+     * will be recorded only on a successful session commit.
+     */
+    void send(FlowFile flowFile, String transitUri, boolean force);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details additional details related to the SEND event, such as a
+     * remote system's Distinguished Name
+     * @param force if <code>true</code>, this event will be added to the
+     * Provenance Repository immediately and will still be persisted if the
+     * {@link org.apache.nifi.processor.ProcessSession ProcessSession} to which this
+     * ProvenanceReporter is associated is rolled back. Otherwise, the Event
+     * will be recorded only on a successful session commit.
+     */
+    void send(FlowFile flowFile, String transitUri, String details, boolean force);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param transmissionMillis the number of milliseconds spent sending the
+     * data to the remote system
+     * @param force if <code>true</code>, this event will be added to the
+     * Provenance Repository immediately and will still be persisted if the
+     * {@link org.apache.nifi.processor.ProcessSession ProcessSession} to which this
+     * ProvenanceReporter is associated is rolled back. Otherwise, the Event
+     * will be recorded only on a successful session commit.
+     */
+    void send(FlowFile flowFile, String transitUri, long transmissionMillis, boolean force);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#SEND SEND}
+     * that indicates that a copy of the given FlowFile was sent to an external
+     * destination. The external destination may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details additional details related to the SEND event, such as a
+     * remote system's Distinguished Name
+     * @param transmissionMillis the number of milliseconds spent sending the
+     * data to the remote system
+     * @param force if <code>true</code>, this event will be added to the
+     * Provenance Repository immediately and will still be persisted if the
+     * {@link org.apache.nifi.processor.ProcessSession ProcessSession} to which this
+     * ProvenanceReporter is associated is rolled back. Otherwise, the Event
+     * will be recorded only on a successful session commit.
+     */
+    void send(FlowFile flowFile, String transitUri, String details, long transmissionMillis, boolean force);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#UPLOAD UPLOAD}
+     * that indicates that an external resource was sent to an external
+     * destination. The external resource may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param size the size of the resource in bytes that was uploaded
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     */
+    void upload(FlowFile flowFile, long size, String transitUri);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#UPLOAD UPLOAD}
+     * that indicates that an external resource was sent to an external
+     * destination. The external resource may be a remote system or may be a
+     * local destination, such as the local file system but is external to NiFi.
+     *
+     * @param flowFile the FlowFile that was sent
+     * @param size the size of the resource in bytes that was uploaded
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the transfer occurred. The intent of this
+     * field is such that both the sender and the receiver can publish the
+     * events to an external Enterprise-wide system that is then able to
+     * correlate the SEND and RECEIVE events.
+     * @param details additional details related to the SEND event, such as a
+     * remote system's Distinguished Name
+     * @param transmissionMillis the number of milliseconds spent sending the
+     * data to the remote system
+     * @param force if <code>true</code>, this event will be added to the
+     * Provenance Repository immediately and will still be persisted if the
+     * {@link org.apache.nifi.processor.ProcessSession ProcessSession} to which this
+     * ProvenanceReporter is associated is rolled back. Otherwise, the Event
+     * will be recorded only on a successful session commit.
+     */
+    void upload(FlowFile flowFile, long size, String transitUri, String details, long transmissionMillis, boolean force);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#REMOTE_INVOCATION}
+     * that indicates a remote invocation is requested to an external endpoint using
+     * the given FlowFile. The external endpoint may exist in a remote or a local system,
+     * but is external to NiFi.
+     * @param flowFile the FlowFile that was used to make the remote invocation
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the invocation occurred. The intent of this
+     * field is to identify they type and target resource or object of the invocation.
+     */
+    void invokeRemoteProcess(FlowFile flowFile, String transitUri);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#REMOTE_INVOCATION}
+     * that indicates a remote invocation is requested to an external endpoint using
+     * the given FlowFile. The external endpoint may exist in a remote or a local system,
+     * but is external to NiFi.
+     * @param flowFile the FlowFile that was used to make the remote invocation
+     * @param transitUri A URI that provides information about the System and
+     * Protocol information over which the invocation occurred. The intent of this
+     * field is to identify they type and target resource or object of the invocation.
+     * @param details additional details related to the REMOTE_INVOCATION event, such as an
+     * explanation of the invoked process.
+     */
+    void invokeRemoteProcess(FlowFile flowFile, String transitUri, String details);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#ADDINFO ADDINFO} that provides a linkage
+     * between the given FlowFile and alternate identifier. This information can
+     * be useful if published to an external, enterprise-wide Provenance
+     * tracking system that is able to associate the data between different
+     * processes.
+     *
+     * @param flowFile the FlowFile for which the association should be made
+     * @param alternateIdentifierNamespace the namespace of the alternate system
+     * @param alternateIdentifier the identifier that the alternate system uses
+     * when referring to the data that is encompassed by this FlowFile
+     */
+    void associate(FlowFile flowFile, String alternateIdentifierNamespace, String alternateIdentifier);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#FORK FORK}
+     * that establishes that the given parent was split into multiple child
+     * FlowFiles. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parent the FlowFile from which the children are derived
+     * @param children the FlowFiles that are derived from the parent.
+     */
+    void fork(FlowFile parent, Collection<FlowFile> children);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#FORK FORK}
+     * that establishes that the given parent was split into multiple child
+     * FlowFiles. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parent the FlowFile from which the children are derived
+     * @param children the FlowFiles that are derived from the parent.
+     * @param details any details pertinent to the fork
+     */
+    void fork(FlowFile parent, Collection<FlowFile> children, String details);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#FORK FORK}
+     * that establishes that the given parent was split into multiple child
+     * FlowFiles. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parent the FlowFile from which the children are derived
+     * @param children the FlowFiles that are derived from the parent.
+     * @param forkDuration the number of milliseconds that it took to perform
+     * the task
+     */
+    void fork(FlowFile parent, Collection<FlowFile> children, long forkDuration);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#FORK FORK}
+     * that establishes that the given parent was split into multiple child
+     * FlowFiles. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parent the FlowFile from which the children are derived
+     * @param children the FlowFiles that are derived from the parent.
+     * @param details any details pertinent to the fork
+     * @param forkDuration the number of milliseconds that it took to perform
+     * the task
+     */
+    void fork(FlowFile parent, Collection<FlowFile> children, String details, long forkDuration);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#JOIN JOIN}
+     * that establishes that the given parents were joined together to create a
+     * new child FlowFile. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parents the FlowFiles that are being joined together to create the
+     * child
+     * @param child the FlowFile that is being created by joining the parents
+     */
+    void join(Collection<FlowFile> parents, FlowFile child);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#JOIN JOIN}
+     * that establishes that the given parents were joined together to create a
+     * new child FlowFile. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parents the FlowFiles that are being joined together to create the
+     * child
+     * @param child the FlowFile that is being created by joining the parents
+     * @param details any details pertinent to the event
+     */
+    void join(Collection<FlowFile> parents, FlowFile child, String details);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#JOIN JOIN}
+     * that establishes that the given parents were joined together to create a
+     * new child FlowFile. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parents the FlowFiles that are being joined together to create the
+     * child
+     * @param child the FlowFile that is being created by joining the parents
+     * @param joinDuration the number of milliseconds that it took to join the
+     * FlowFiles
+     */
+    void join(Collection<FlowFile> parents, FlowFile child, long joinDuration);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#JOIN JOIN}
+     * that establishes that the given parents were joined together to create a
+     * new child FlowFile. In general, this method does not need to be called by
+     * Processors, as the ProcessSession will handle this automatically for you
+     * when calling {@link ProcessSession#create(FlowFile)}.
+     *
+     * @param parents the FlowFiles that are being joined together to create the
+     * child
+     * @param child the FlowFile that is being created by joining the parents
+     * @param details any details pertinent to the event
+     * @param joinDuration the number of milliseconds that it took to join the
+     * FlowFiles
+     */
+    void join(Collection<FlowFile> parents, FlowFile child, String details, long joinDuration);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#CLONE CLONE}
+     * that establishes that the given child is an exact replica of the parent.
+     * In general, this method does not need to be called by Processors, as the
+     * {@link ProcessSession} will handle this automatically for you when
+     * calling {@link ProcessSession#clone(FlowFile)}
+     *
+     * @param parent the FlowFile that was cloned
+     * @param child the clone
+     */
+    void clone(FlowFile parent, FlowFile child);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CONTENT_MODIFIED CONTENT_MODIFIED} that
+     * indicates that the content of the given FlowFile has been modified. One
+     * of the <code>modifyContent</code> methods should be called any time that
+     * the contents of a FlowFile are modified.
+     *
+     * @param flowFile the FlowFile whose content is being modified
+     */
+    void modifyContent(FlowFile flowFile);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CONTENT_MODIFIED CONTENT_MODIFIED} that
+     * indicates that the content of the given FlowFile has been modified. One
+     * of the <code>modifyContent</code> methods should be called any time that
+     * the contents of a FlowFile are modified.
+     *
+     * @param flowFile the FlowFile whose content is being modified
+     * @param details Any details about how the content of the FlowFile has been
+     * modified. Details should not be specified if they can be inferred by
+     * other information in the event, such as the name of the Processor, as
+     * specifying this information will add undue overhead
+     */
+    void modifyContent(FlowFile flowFile, String details);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CONTENT_MODIFIED CONTENT_MODIFIED} that
+     * indicates that the content of the given FlowFile has been modified. One
+     * of the <code>modifyContent</code> methods should be called any time that
+     * the contents of a FlowFile are modified.
+     *
+     * @param flowFile the FlowFile whose content is being modified
+     * @param processingMillis the number of milliseconds spent processing the
+     * FlowFile
+     */
+    void modifyContent(FlowFile flowFile, long processingMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CONTENT_MODIFIED CONTENT_MODIFIED} that
+     * indicates that the content of the given FlowFile has been modified. One
+     * of the <code>modifyContent</code> methods should be called any time that
+     * the contents of a FlowFile are modified.
+     *
+     * @param flowFile the FlowFile whose content is being modified
+     * @param details Any details about how the content of the FlowFile has been
+     * modified. Details should not be specified if they can be inferred by
+     * other information in the event, such as the name of the Processor, as
+     * specifying this information will add undue overhead
+     * @param processingMillis the number of milliseconds spent processing the
+     * FlowFile
+     */
+    void modifyContent(FlowFile flowFile, String details, long processingMillis);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#ATTRIBUTES_MODIFIED ATTRIBUTES_MODIFIED} that
+     * indicates that the Attributes of the given FlowFile were updated. It is
+     * not necessary to emit such an event for a FlowFile if other Events are
+     * already emitted by a Processor. For example, one should call both
+     * {@link #modifyContent(FlowFile)} and {@link #modifyAttributes(FlowFile)}
+     * for the same FlowFile in the same Processor. Rather, the Processor should
+     * call just the {@link #modifyContent(FlowFile)}, as the call to
+     * {@link #modifyContent(FlowFile)} will generate a Provenance Event that
+     * already contains all FlowFile attributes. As such, emitting another event
+     * that contains those attributes is unneeded and can result in a
+     * significant amount of overhead for storage and processing.
+     *
+     * @param flowFile the FlowFile whose attributes were modified
+     */
+    void modifyAttributes(FlowFile flowFile);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#ATTRIBUTES_MODIFIED ATTRIBUTES_MODIFIED} that
+     * indicates that the Attributes of the given FlowFile were updated. It is
+     * not necessary to emit such an event for a FlowFile if other Events are
+     * already emitted by a Processor. For example, one should call both
+     * {@link #modifyContent(FlowFile)} and {@link #modifyAttributes(FlowFile)}
+     * for the same FlowFile in the same Processor. Rather, the Processor should
+     * call just the {@link #modifyContent(FlowFile)}, as the call to
+     * {@link #modifyContent(FlowFile)} will generate a Provenance Event that
+     * already contains all FlowFile attributes. As such, emitting another event
+     * that contains those attributes is unneeded and can result in a
+     * significant amount of overhead for storage and processing.
+     *
+     * @param flowFile the FlowFile whose attributes were modified
+     * @param details any details should be provided about the attribute
+     * modification
+     */
+    void modifyAttributes(FlowFile flowFile, String details);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#ROUTE ROUTE}
+     * that indicates that the given FlowFile was routed to the given
+     * {@link Relationship}. <b>Note: </b> this Event is intended for Processors
+     * whose sole job it is to route FlowFiles and should NOT be used as a way
+     * to indicate that the given FlowFile was routed to a standard 'success' or
+     * 'failure' relationship. Doing so can be problematic, as DataFlow Managers
+     * often will loop 'failure' relationships back to the same processor. As
+     * such, emitting a Route event to indicate that a FlowFile was routed to
+     * 'failure' can result in creating thousands of Provenance Events for a
+     * given FlowFile, resulting in a very difficult-to- understand lineage.
+     *
+     * @param flowFile the FlowFile being routed
+     * @param relationship the Relationship to which the FlowFile was routed
+     */
+    void route(FlowFile flowFile, Relationship relationship);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#ROUTE ROUTE}
+     * that indicates that the given FlowFile was routed to the given
+     * {@link Relationship}. <b>Note: </b> this Event is intended ONLY for
+     * Processors whose sole job it is to route FlowFiles and should NOT be used
+     * as a way to indicate that hte given FlowFile was routed to a standard
+     * 'success' or 'failure' relationship. Doing so can be problematic, as
+     * DataFlow Managers often will loop 'failure' relationships back to the
+     * same processor. As such, emitting a Route event to indicate that a
+     * FlowFile was routed to 'failure' can result in creating thousands of
+     * Provenance Events for a given FlowFile, resulting in a very difficult-to-
+     * understand lineage.
+     *
+     * @param flowFile the FlowFile being routed
+     * @param relationship the Relationship to which the FlowFile was routed
+     * @param details any details pertinent to the Route event, such as why the
+     * FlowFile was routed to the specified Relationship
+     */
+    void route(FlowFile flowFile, Relationship relationship, String details);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#ROUTE ROUTE}
+     * that indicates that the given FlowFile was routed to the given
+     * {@link Relationship}. <b>Note: </b> this Event is intended ONLY for
+     * Processors whose sole job it is to route FlowFiles and should NOT be used
+     * as a way to indicate that hte given FlowFile was routed to a standard
+     * 'success' or 'failure' relationship. Doing so can be problematic, as
+     * DataFlow Managers often will loop 'failure' relationships back to the
+     * same processor. As such, emitting a Route event to indicate that a
+     * FlowFile was routed to 'failure' can result in creating thousands of
+     * Provenance Events for a given FlowFile, resulting in a very difficult-to-
+     * understand lineage.
+     *
+     * @param flowFile the FlowFile being routed
+     * @param relationship the Relationship to which the FlowFile was routed
+     * @param processingDuration the number of milliseconds that it took to
+     * determine how to route the FlowFile
+     */
+    void route(FlowFile flowFile, Relationship relationship, long processingDuration);
+
+    /**
+     * Emits a Provenance Event of type {@link ProvenanceEventType#ROUTE ROUTE}
+     * that indicates that the given FlowFile was routed to the given
+     * {@link Relationship}. <b>Note: </b> this Event is intended ONLY for
+     * Processors whose sole job it is to route FlowFiles and should NOT be used
+     * as a way to indicate that hte given FlowFile was routed to a standard
+     * 'success' or 'failure' relationship. Doing so can be problematic, as
+     * DataFlow Managers often will loop 'failure' relationships back to the
+     * same processor. As such, emitting a Route event to indicate that a
+     * FlowFile was routed to 'failure' can result in creating thousands of
+     * Provenance Events for a given FlowFile, resulting in a very difficult-to-
+     * understand lineage.
+     *
+     * @param flowFile the FlowFile being routed
+     * @param relationship the Relationship to which the FlowFile was routed
+     * @param details any details pertinent to the Route event, such as why the
+     * FlowFile was routed to the specified Relationship
+     * @param processingDuration the number of milliseconds that it took to
+     * determine how to route the FlowFile
+     */
+    void route(FlowFile flowFile, Relationship relationship, String details, long processingDuration);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CREATE CREATE} that indicates that the given
+     * FlowFile was created by NiFi from data that was not received from an
+     * external entity. If the data was received from an external source, use
+     * the {@link #receive(FlowFile, String)} event instead
+     *
+     * @param flowFile the FlowFile that was created
+     */
+    void create(FlowFile flowFile);
+
+    /**
+     * Emits a Provenance Event of type
+     * {@link ProvenanceEventType#CREATE CREATE} that indicates that the given
+     * FlowFile was created by NiFi from data that was not received from an
+     * external entity. If the data was received from an external source, use
+     * the {@link #receive(FlowFile, String, String, long)} event instead
+     *
+     * @param flowFile the FlowFile that was created
+     * @param details any relevant details about the CREATE event
+     */
+    void create(FlowFile flowFile, String details);
+    /**
+     * @return the number of FlowFiles for which there was a RECEIVE event
+     */
+    int getFlowFilesReceived();
+
+    /**
+     * @return the sum of the sizes of all FlowFiles for which there was a RECEIVE event
+     */
+    long getBytesReceived();
+
+    /**
+     * @return the number of FlowFiles for which there was a FETCH event
+     */
+    int getFlowFilesFetched();
+
+    /**
+     * @return the sum of the sizes of all FlowFiles for which there was a FETCH event
+     */
+    long getBytesFetched();
+
+    /**
+     * @return the number of FlowFiles for which there was a SEND event
+     */
+    int getFlowFilesSent();
+
+    /**
+     * @return the sum of the sizes of all FlowFiles for which there was a SEND event
+     */
+    long getBytesSent();
+}
diff --git a/src/main/java/org/apache/nifi/registry/EnvironmentVariables.java b/src/main/java/org/apache/nifi/registry/EnvironmentVariables.java
new file mode 100644
index 0000000..52721a8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/EnvironmentVariables.java
@@ -0,0 +1,122 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides a registry of variables available for use by various components and
+ * extension points. This enables components to reference variable names rather
+ * than explicit values which can make configurations of those components more
+ * portable.
+ */
+public interface EnvironmentVariables {
+
+    /**
+     * Provides a registry containing all environment variables and system
+     * properties. System properties receive precedence.
+     */
+    EnvironmentVariables ENVIRONMENT_VARIABLES = new EnvironmentVariables() {
+        final Map<VariableDescriptor, String> map = new HashMap<>();
+
+        {
+            System.getenv().entrySet().stream().forEach((entry) -> {
+                final VariableDescriptor desc = new VariableDescriptor.Builder(entry.getKey())
+                        .description("Env Var")
+                        .sensitive(false)
+                        .build();
+                map.put(desc, entry.getValue());
+            });
+            System.getProperties().entrySet().stream().forEach((entry) -> {
+                final VariableDescriptor desc = new VariableDescriptor.Builder(entry.getKey().toString())
+                        .description("System Property")
+                        .sensitive(false)
+                        .build();
+                map.put(desc, entry.getValue().toString());
+            });
+        }
+
+        @Override
+        public Map<VariableDescriptor, String> getEnvironmentVariablesMap() {
+            return Collections.unmodifiableMap(map);
+        }
+
+    };
+
+    /**
+     * Provides access to a map of variable key/value pairs. For variables
+     * considered to be sensitive care must be taken to ensure their values are
+     * protected whenever stored or exposed.
+     *
+     * @return An immutable map of all variables in the registry
+     */
+
+    Map<VariableDescriptor, String> getEnvironmentVariablesMap();
+
+    /**
+     * Returns the VariableDescriptor for the given key name if it exists.
+     *
+     * @param name the string name of the VariableDescriptor to lookup.
+     * @return the variable descriptor registered for this name if it exists;
+     * null otherwise
+     */
+    default VariableDescriptor getEnvironmentVariableKey(final String name) {
+        if (name == null) {
+            return null;
+        }
+        final VariableDescriptor spec = new VariableDescriptor(name);
+        for (final Map.Entry<VariableDescriptor, String> entry : getEnvironmentVariablesMap().entrySet()) {
+            if (entry.getKey().equals(spec)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+
+    }
+
+    /**
+     * Gets the variable value
+     *
+     * @param name the string name of the VariableDescriptor that is the key of
+     * the value to lookup.
+     * @return the value associated with the given variable name if found; null
+     * otherwise
+     */
+    default String getEnvironmentVariableValue(final String name) {
+        if (name == null) {
+            return null;
+        }
+        return getEnvironmentVariablesMap().get(new VariableDescriptor(name));
+    }
+
+    /**
+     * Gets the variable value
+     *
+     * @param descriptor the descriptor for which to lookup the variable value.
+     * @return the variable value if the given descriptor is equivalent to one
+     * of the entries in the registry; null otherwise
+     */
+    default String getEnvironmentVariableValue(final VariableDescriptor descriptor) {
+        if (descriptor == null) {
+            return null;
+        }
+        return getEnvironmentVariablesMap().get(descriptor);
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/registry/VariableDescriptor.java b/src/main/java/org/apache/nifi/registry/VariableDescriptor.java
new file mode 100644
index 0000000..f9e9ce2
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/VariableDescriptor.java
@@ -0,0 +1,153 @@
+/*
+ * 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;
+
+/**
+ * An immutable object for holding information about a type of processor
+ * property.
+ *
+ */
+public final class VariableDescriptor implements Comparable<VariableDescriptor> {
+
+    /**
+     * The name (or key) of the variable by which all access/lookups to the
+     * value will occur. This is the mechanism of establishing identity and
+     * comparing equality.
+     */
+    private final String name;
+
+    /**
+     * A brief description of the variable. This description is meant to be
+     * displayed to a user or simply provide a mechanism of documenting intent.
+     */
+    private final String description;
+
+    /**
+     * indicates that the value for this variable should be considered sensitive
+     * and protected whenever stored or represented
+     */
+    private final boolean sensitive;
+
+    /**
+     * Convenience constructor to create a descriptor based on name alone. To
+     * include additional parameters use Builder instead.
+     *
+     * @param name name used as unique identifier for this descriptor
+     */
+    public VariableDescriptor(final String name) {
+        this(new Builder(name));
+    }
+
+    protected VariableDescriptor(final Builder builder) {
+        this.name = builder.name;
+        this.description = builder.description;
+        this.sensitive = builder.sensitive;
+    }
+
+    @Override
+    public int compareTo(final VariableDescriptor o) {
+        if (o == null) {
+            return -1;
+        }
+        return getName().compareTo(o.getName());
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof VariableDescriptor)) {
+            return false;
+        }
+
+        final VariableDescriptor desc = (VariableDescriptor) other;
+        return this.name.equals(desc.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return 797 + this.name.hashCode() * 97;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + name + "]";
+    }
+
+    public static final class Builder {
+
+        private String name = null;
+        private String description = "";
+        private boolean sensitive = false;
+
+        /**
+         * Establishes the unique identifier or key name of the variable.
+         *
+         * @param name of the property
+         */
+        public Builder(final String name) {
+            if (null == name || name.trim().isEmpty()) {
+                throw new IllegalArgumentException("Name must not be null or empty");
+            }
+            this.name = name.trim();
+        }
+
+        /**
+         * @param description of the variable
+         * @return the builder
+         */
+        public Builder description(final String description) {
+            if (null != description) {
+                this.description = description;
+            }
+            return this;
+        }
+
+        /**
+         * @param sensitive true if sensitive; false otherwise
+         * @return the builder
+         */
+        public Builder sensitive(final boolean sensitive) {
+            this.sensitive = sensitive;
+            return this;
+        }
+
+        /**
+         * @return a VariableDescriptor as configured
+         *
+         */
+        public VariableDescriptor build() {
+            return new VariableDescriptor(this);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/AbstractFlowRegistryClient.java b/src/main/java/org/apache/nifi/registry/flow/AbstractFlowRegistryClient.java
new file mode 100644
index 0000000..eabb318
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/AbstractFlowRegistryClient.java
@@ -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.registry.flow;
+
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.logging.ComponentLog;
+
+import javax.net.ssl.SSLContext;
+import java.util.Optional;
+
+public abstract class AbstractFlowRegistryClient extends AbstractConfigurableComponent implements FlowRegistryClient {
+
+    private volatile String identifier;
+    private volatile Optional<SSLContext> systemSslContext;
+    private volatile ComponentLog logger;
+
+    @Override
+    public void initialize(final FlowRegistryClientInitializationContext context) {
+        this.identifier = context.getIdentifier();
+        this.logger = context.getLogger();
+        this.systemSslContext = context.getSystemSslContext();
+    }
+
+    @Override
+    public final String getIdentifier() {
+        return identifier;
+    }
+
+    protected final ComponentLog getLogger() {
+        return logger;
+    }
+
+    protected final Optional<SSLContext> getSystemSslContext() {
+        return systemSslContext;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/AuthenticationException.java b/src/main/java/org/apache/nifi/registry/flow/AuthenticationException.java
new file mode 100644
index 0000000..a1d3baa
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/AuthenticationException.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.registry.flow;
+
+public class AuthenticationException extends FlowRegistryException {
+
+    public AuthenticationException(final String message) {
+        super(message);
+    }
+
+    public AuthenticationException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/AuthorizationException.java b/src/main/java/org/apache/nifi/registry/flow/AuthorizationException.java
new file mode 100644
index 0000000..b803abf
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/AuthorizationException.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.registry.flow;
+
+public class AuthorizationException extends FlowRegistryException {
+
+    public AuthorizationException(final String message) {
+        super(message);
+    }
+
+    public AuthorizationException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/BucketLocation.java b/src/main/java/org/apache/nifi/registry/flow/BucketLocation.java
new file mode 100644
index 0000000..58dddae
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/BucketLocation.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.registry.flow;
+
+/**
+ * Information for locating a bucket in a flow registry.
+ */
+public class BucketLocation {
+
+    private String branch;
+    private String bucketId;
+
+    public BucketLocation() {
+
+    }
+
+    public BucketLocation(final String branch, final String bucketId) {
+        this.branch = branch;
+        this.bucketId = bucketId;
+    }
+
+    public String getBranch() {
+        return branch;
+    }
+
+    public void setBranch(final String branch) {
+        this.branch = branch;
+    }
+
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(final String bucketId) {
+        this.bucketId = bucketId;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowAlreadyExistsException.java b/src/main/java/org/apache/nifi/registry/flow/FlowAlreadyExistsException.java
new file mode 100644
index 0000000..f31b869
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowAlreadyExistsException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public class FlowAlreadyExistsException extends FlowRegistryException {
+
+    public FlowAlreadyExistsException(final String message) {
+        super(message);
+    }
+
+    public FlowAlreadyExistsException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
+
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowLocation.java b/src/main/java/org/apache/nifi/registry/flow/FlowLocation.java
new file mode 100644
index 0000000..c30f549
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowLocation.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.registry.flow;
+
+/**
+ * Information for locating a flow in a flow registry.
+ */
+public class FlowLocation extends BucketLocation {
+
+    private String flowId;
+
+    public FlowLocation() {
+
+    }
+
+    public FlowLocation(final String branch, final String bucketId, final String flowId) {
+        super(branch, bucketId);
+        this.flowId = flowId;
+    }
+
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(final String flowId) {
+        this.flowId = flowId;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBranch.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBranch.java
new file mode 100644
index 0000000..e9d944c
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBranch.java
@@ -0,0 +1,52 @@
+/*
+ *
+ *  * 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 java.util.Objects;
+
+public class FlowRegistryBranch {
+
+    private String name;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final FlowRegistryBranch that = (FlowRegistryBranch) o;
+        return Objects.equals(name, that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBucket.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBucket.java
new file mode 100644
index 0000000..13ddc85
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryBucket.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.registry.flow;
+
+public class FlowRegistryBucket {
+    private String identifier;
+    private String name;
+    private String description;
+    private long createdTimestamp;
+    private FlowRegistryPermissions permissions;
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public long getCreatedTimestamp() {
+        return createdTimestamp;
+    }
+
+    public FlowRegistryPermissions getPermissions() {
+        return permissions;
+    }
+
+    public void setIdentifier(final String identifier) {
+        this.identifier = identifier;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    public void setCreatedTimestamp(final long createdTimestamp) {
+        this.createdTimestamp = createdTimestamp;
+    }
+
+    public void setPermissions(final FlowRegistryPermissions permissions) {
+        this.permissions = permissions;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
new file mode 100644
index 0000000..860dfd8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
@@ -0,0 +1,266 @@
+/*
+ * 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.apache.nifi.components.ConfigurableComponent;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * <p>
+ * Represents and external source, where flows might be stored and retrieved from. The interface provides
+ * the possibility to have multiple different types of registries backing NiFi.
+ * </p>
+ *
+ * <p>
+ * <code>FlowRegistryClient</code>s are discovered using Java's
+ * <code>ServiceLoader</code> mechanism. As a result, all implementations must
+ * follow these rules:
+ *
+ * <ul>
+ * <li>The implementation must implement this interface.</li>
+ * <li>The implementation must have a file named
+ * org.apache.nifi.registry.flow.FlowRegistryClient located within the jar's
+ * <code>META-INF/services</code> directory. This file contains a list of
+ * fully-qualified class names of all <code>FlowRegistryClient</code>s in the
+ * jar, one-per-line.
+ * <li>The implementation must support a default constructor.</li>
+ * </ul>
+ *
+ * <p>
+ * <code>FlowRegistryClient</code> instances are always considered "active" and approachable when the validation status is considered as valid.
+ * Therefore after initialize, implementations must expect incoming requests any time.
+ * </p>
+ *
+ * <p>
+ * The argument list of the request methods contain instances <code>FlowRegistryClientConfigurationContext</code>, which always contains the current
+ * state of the properties. Caching properties between method calls is not recommended.
+ * </p>
+ *
+ */
+public interface FlowRegistryClient extends ConfigurableComponent {
+
+    String DEFAULT_BRANCH_NAME = "main";
+
+    void initialize(FlowRegistryClientInitializationContext context);
+
+    /**
+     * Decides if the given location is applicable by the repository instance. The format depends on the implementation.
+     *
+     * @param context Configuration context.
+     * @param location The location of versioned flow to check.
+     *
+     * @return True in case of the given storage location is applicable, false otherwise. An applicable location does not
+     * mean that the flow specified by the location is stored in the registry. Depending on the implementation it might be
+     * merely a static check on the format of the location string. In case of uncertainty, NiFi tries to load the versioned
+     * flow from registries with true return value first.
+     */
+    boolean isStorageLocationApplicable(FlowRegistryClientConfigurationContext context, String location);
+
+    /**
+     * Indicates if the registry supports branching.
+     *
+     * If the registry does not supporting branching, then this method should return false and the registry client should use
+     * the default implementations of getBranches and getDefaultBranch.
+     *
+     * If the registry does support branching, then this method should return true and the registry client should use
+     * override getBranches and getDefaultBranch to provide appropriate implementations.
+     *
+     * @param context Configuration context
+     * @return true if the registry supports branching, false otherwise
+     */
+    default boolean isBranchingSupported(final FlowRegistryClientConfigurationContext context) {
+        return false;
+    }
+
+    /**
+     * Get the available branches. Should return at least one branch that matches the response of getDefaultBranch.
+     *
+     * @param context Configuration context
+     * @return the set of available branches
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    default Set<FlowRegistryBranch> getBranches(final FlowRegistryClientConfigurationContext context) throws FlowRegistryException, IOException {
+        return Set.of(getDefaultBranch(context));
+    }
+
+    /**
+     * Gets the default branch. Must return a non-null FlowRegistryBranch instance with a non-null name.
+     * The interface provides a default implementation which will return a branch named 'main'.
+     *
+     * @param context Configuration context
+     * @return the default branch
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    default FlowRegistryBranch getDefaultBranch(final FlowRegistryClientConfigurationContext context) throws FlowRegistryException, IOException {
+        final FlowRegistryBranch branch = new FlowRegistryBranch();
+        branch.setName(DEFAULT_BRANCH_NAME);
+        return branch;
+    }
+
+    /**
+     * Gets the buckets for the specified user.
+     *
+     * @param context Configuration context.
+     * @param branch the branch
+     *
+     * @return Buckets for this user. In case there are no available buckets for the user the result will be an empty set.
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    Set<FlowRegistryBucket> getBuckets(FlowRegistryClientConfigurationContext context, String branch) throws FlowRegistryException, IOException;
+
+    /**
+     * Gets the bucket with the given id.
+     *
+     * @param context Configuration context.
+     * @param bucketLocation The location of the bucket.
+     * @return The bucket with the given id.
+     *
+     * @throws NoSuchBucketException If there is no bucket in the Registry with the given id.
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    FlowRegistryBucket getBucket(FlowRegistryClientConfigurationContext context, BucketLocation bucketLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Registers the given RegisteredFlow into the Flow Registry.
+     *
+     * @param context Configuration context.
+     * @param flow The RegisteredFlow to add to the Registry.
+     *
+     * @return The fully populated RegisteredFlow
+     *
+     * @throws FlowAlreadyExistsException If a Flow with the given identifier already exists in the registry.
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    RegisteredFlow registerFlow(FlowRegistryClientConfigurationContext context, RegisteredFlow flow) throws FlowRegistryException, IOException;
+
+    /**
+     * Deletes the specified flow from the Flow Registry.
+     *
+     * @param context Configuration context.
+     * @param flowLocation the location of the flow
+     *
+     * @return The deleted Flow.
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    RegisteredFlow deregisterFlow(FlowRegistryClientConfigurationContext context, FlowLocation flowLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Retrieves a flow by bucket id and Flow id.
+     *
+     * @param context Configuration context.
+     * @param flowLocation the location of the flow
+     *
+     * @return The Flow for the given bucket and Flow id's.
+     *
+     * @throws NoSuchFlowException If there is no Flow in the bucket with the given id.
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    RegisteredFlow getFlow(FlowRegistryClientConfigurationContext context, FlowLocation flowLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Retrieves the set of all Flows for the specified bucket.
+     *
+     * @param context Configuration context.
+     * @param bucketLocation the location of the bucket
+     *
+     * @return The set of all Flows from the specified bucket. In case there are no available flows in the bucket the result will be an empty set.
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    Set<RegisteredFlow> getFlows(FlowRegistryClientConfigurationContext context, BucketLocation bucketLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Retrieves the contents of the flow snapshot with the given Bucket id, Flow id, and version, from the Registry.
+     *
+     * @param context Configuration context.
+     * @param flowVersionLocation the location of the flow version
+     *
+     * @return The contents of the Flow from the Flow Registry.
+     *
+     * @throws NoSuchFlowVersionException If there is no version of the Flow with the given version number.
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    RegisteredFlowSnapshot getFlowContents(FlowRegistryClientConfigurationContext context, FlowVersionLocation flowVersionLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Adds the given snapshot to the Flow Registry for the given Flow.
+     *
+     * @param context Configuration context.
+     * @param flowSnapshot The flow snapshot to register.
+     * @param action The register action
+     *
+     * @return The flow snapshot.
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    RegisteredFlowSnapshot registerFlowSnapshot(FlowRegistryClientConfigurationContext context, RegisteredFlowSnapshot flowSnapshot, RegisterAction action) throws FlowRegistryException, IOException;
+
+    /**
+     * Retrieves the set of all versions of the specified flow.
+     *
+     * @param context Configuration context.
+     * @param flowLocation the location of the flow
+     *
+     * @return The set of all versions of the specified flow. In case there are no available versions for the specified flow the result will be an empty set.
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    Set<RegisteredFlowSnapshotMetadata> getFlowVersions(FlowRegistryClientConfigurationContext context, FlowLocation flowLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Returns the latest (most recent) version of the Flow in the Flow Registry for the given bucket and Flow.
+     *
+     * @param context Configuration context.
+     * @param flowLocation the location of the flow
+     *
+     * @return an Optional containing the latest version of the flow, or empty if no versions exist for the flow
+     *
+     * @throws FlowRegistryException If an issue happens during processing the request.
+     * @throws IOException If there is issue with the communication between NiFi and the Flow Registry.
+     */
+    Optional<String> getLatestVersion(FlowRegistryClientConfigurationContext context, FlowLocation flowLocation) throws FlowRegistryException, IOException;
+
+    /**
+     * Generates the id for registering a flow.
+     *
+     * @param flowName the name of the flow
+     * @return the generated id
+     */
+    default String generateFlowId(final String flowName) {
+        return UUID.randomUUID().toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientConfigurationContext.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientConfigurationContext.java
new file mode 100644
index 0000000..f0482ad
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientConfigurationContext.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.registry.flow;
+
+import org.apache.nifi.context.PropertyContext;
+
+import java.util.Optional;
+
+public interface FlowRegistryClientConfigurationContext extends PropertyContext {
+    Optional<String> getNiFiUserIdentity();
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientInitializationContext.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientInitializationContext.java
new file mode 100644
index 0000000..06baef6
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClientInitializationContext.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.registry.flow;
+
+import org.apache.nifi.logging.ComponentLog;
+
+import javax.net.ssl.SSLContext;
+import java.util.Optional;
+
+public interface FlowRegistryClientInitializationContext {
+
+    String getIdentifier();
+
+    ComponentLog getLogger();
+
+    Optional<SSLContext> getSystemSslContext();
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryException.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryException.java
new file mode 100644
index 0000000..f137c28
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryException.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.registry.flow;
+
+/**
+ * Indicates an error interacting with the NiFi registry for a reason other than IOException.
+ */
+public class FlowRegistryException extends Exception {
+
+    public FlowRegistryException(final String message) {
+        super(message);
+    }
+
+    public FlowRegistryException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryInvalidException.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryInvalidException.java
new file mode 100644
index 0000000..4278ead
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryInvalidException.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.registry.flow;
+
+import java.util.List;
+
+public class FlowRegistryInvalidException extends FlowRegistryException {
+    public FlowRegistryInvalidException(final List<String> validationErrors) {
+        super("The flow registry client instance has the following validation errors: " + String.join(", ", validationErrors));
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPermissions.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPermissions.java
new file mode 100644
index 0000000..28c7d8b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPermissions.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.registry.flow;
+
+public class FlowRegistryPermissions {
+    private boolean canRead;
+    private boolean canWrite;
+    private boolean canDelete;
+
+    public boolean getCanRead() {
+        return canRead;
+    }
+
+    public boolean getCanWrite() {
+        return canWrite;
+    }
+
+    public boolean getCanDelete() {
+        return canDelete;
+    }
+
+    public void setCanRead(final boolean canRead) {
+        this.canRead = canRead;
+    }
+
+    public void setCanWrite(final boolean canWrite) {
+        this.canWrite = canWrite;
+    }
+
+    public void setCanDelete(final boolean canDelete) {
+        this.canDelete = canDelete;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPreCommitException.java b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPreCommitException.java
new file mode 100644
index 0000000..46e4f43
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowRegistryPreCommitException.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class FlowRegistryPreCommitException extends FlowRegistryException {
+    public FlowRegistryPreCommitException(final String message) {
+        super(message);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java b/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
new file mode 100644
index 0000000..3e4a482
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
@@ -0,0 +1,67 @@
+/*
+ *
+ *  * 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 java.util.Objects;
+
+/**
+ * Information for locating a flow version in a flow registry.
+ */
+public class FlowVersionLocation extends FlowLocation {
+
+    private String version;
+
+    public FlowVersionLocation() {
+
+    }
+
+    public FlowVersionLocation(final String branch, final String bucketId, final String flowId, final String version) {
+        super(branch, bucketId, flowId);
+        this.version = version;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(final String version) {
+        this.version = version;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final FlowVersionLocation that = (FlowVersionLocation) o;
+        return Objects.equals(getBranch(), that.getBranch())
+                && Objects.equals(getBucketId(), that.getBucketId())
+                && Objects.equals(getFlowId(), that.getFlowId())
+                && Objects.equals(version, that.version);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getBranch(), getBucketId(), getFlowId(), version);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/NoSuchBucketException.java b/src/main/java/org/apache/nifi/registry/flow/NoSuchBucketException.java
new file mode 100644
index 0000000..c498332
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/NoSuchBucketException.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.registry.flow;
+
+public class NoSuchBucketException extends FlowRegistryException {
+
+    public NoSuchBucketException(final String message) {
+        super(message);
+    }
+
+    public NoSuchBucketException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowException.java b/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowException.java
new file mode 100644
index 0000000..e36a591
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowException.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.registry.flow;
+
+public class NoSuchFlowException  extends FlowRegistryException {
+
+    public NoSuchFlowException(final String message) {
+        super(message);
+    }
+
+    public NoSuchFlowException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowVersionException.java b/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowVersionException.java
new file mode 100644
index 0000000..94d8f34
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/NoSuchFlowVersionException.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.registry.flow;
+
+public class NoSuchFlowVersionException extends FlowRegistryException {
+
+    public NoSuchFlowVersionException(final String message) {
+        super(message);
+    }
+
+    public NoSuchFlowVersionException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/RegisterAction.java b/src/main/java/org/apache/nifi/registry/flow/RegisterAction.java
new file mode 100644
index 0000000..593a302
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/RegisterAction.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.registry.flow;
+
+public enum RegisterAction {
+    COMMIT,
+    FORCE_COMMIT;
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/RegisteredFlow.java b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlow.java
new file mode 100644
index 0000000..c8d39d9
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlow.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+public class RegisteredFlow {
+    private String identifier;
+    private String name;
+    private String description;
+    private String branch;
+    private String bucketIdentifier;
+    private String bucketName;
+    private long createdTimestamp;
+    private long lastModifiedTimestamp;
+    private FlowRegistryPermissions permissions;
+    private long versionCount;
+    private RegisteredFlowVersionInfo versionInfo;
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getBranch() {
+        return branch;
+    }
+
+    public void setBranch(final String branch) {
+        this.branch = branch;
+    }
+
+    public String getBucketIdentifier() {
+        return bucketIdentifier;
+    }
+
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public long getCreatedTimestamp() {
+        return createdTimestamp;
+    }
+
+    public long getLastModifiedTimestamp() {
+        return lastModifiedTimestamp;
+    }
+
+    public FlowRegistryPermissions getPermissions() {
+        return permissions;
+    }
+
+    public long getVersionCount() {
+        return versionCount;
+    }
+
+    public RegisteredFlowVersionInfo getVersionInfo() {
+        return versionInfo;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public void setBucketIdentifier(String bucketIdentifier) {
+        this.bucketIdentifier = bucketIdentifier;
+    }
+
+    public void setBucketName(String bucketName) {
+        this.bucketName = bucketName;
+    }
+
+    public void setCreatedTimestamp(long createdTimestamp) {
+        this.createdTimestamp = createdTimestamp;
+    }
+
+    public void setLastModifiedTimestamp(long lastModifiedTimestamp) {
+        this.lastModifiedTimestamp = lastModifiedTimestamp;
+    }
+
+    public void setPermissions(FlowRegistryPermissions permissions) {
+        this.permissions = permissions;
+    }
+
+    public void setVersionCount(long versionCount) {
+        this.versionCount = versionCount;
+    }
+
+    public void setVersionInfo(RegisteredFlowVersionInfo versionInfo) {
+        this.versionInfo = versionInfo;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshot.java b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshot.java
new file mode 100644
index 0000000..79bf1a8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshot.java
@@ -0,0 +1,108 @@
+/*
+ * 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.apache.nifi.flow.ExternalControllerServiceReference;
+import org.apache.nifi.flow.ParameterProviderReference;
+import org.apache.nifi.flow.VersionedParameterContext;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+import java.util.Map;
+
+public class RegisteredFlowSnapshot {
+    private RegisteredFlowSnapshotMetadata snapshotMetadata;
+    private RegisteredFlow flow;
+    private FlowRegistryBucket bucket;
+    private VersionedProcessGroup flowContents;
+    private Map<String, ExternalControllerServiceReference> externalControllerServices;
+    private Map<String, VersionedParameterContext> parameterContexts;
+    private String flowEncodingVersion;
+    private Map<String, ParameterProviderReference> parameterProviders;
+    private boolean latest;
+
+    public RegisteredFlowSnapshotMetadata getSnapshotMetadata() {
+        return snapshotMetadata;
+    }
+
+    public RegisteredFlow getFlow() {
+        return flow;
+    }
+
+    public FlowRegistryBucket getBucket() {
+        return bucket;
+    }
+
+    public VersionedProcessGroup getFlowContents() {
+        return flowContents;
+    }
+
+    public Map<String, ExternalControllerServiceReference> getExternalControllerServices() {
+        return externalControllerServices;
+    }
+
+    public Map<String, VersionedParameterContext> getParameterContexts() {
+        return parameterContexts;
+    }
+
+    public String getFlowEncodingVersion() {
+        return flowEncodingVersion;
+    }
+
+    public boolean isLatest() {
+        return latest;
+    }
+
+    public void setSnapshotMetadata(final RegisteredFlowSnapshotMetadata snapshotMetadata) {
+        this.snapshotMetadata = snapshotMetadata;
+    }
+
+    public void setFlow(RegisteredFlow flow) {
+        this.flow = flow;
+    }
+
+    public void setBucket(FlowRegistryBucket bucket) {
+        this.bucket = bucket;
+    }
+
+    public void setFlowContents(VersionedProcessGroup flowContents) {
+        this.flowContents = flowContents;
+    }
+
+    public void setExternalControllerServices(final Map<String, ExternalControllerServiceReference> externalControllerServices) {
+        this.externalControllerServices = externalControllerServices;
+    }
+
+    public void setParameterContexts(final Map<String, VersionedParameterContext> parameterContexts) {
+        this.parameterContexts = parameterContexts;
+    }
+
+    public void setFlowEncodingVersion(String flowEncodingVersion) {
+        this.flowEncodingVersion = flowEncodingVersion;
+    }
+
+    public Map<String, ParameterProviderReference> getParameterProviders() {
+        return parameterProviders;
+    }
+
+    public void setParameterProviders(final Map<String, ParameterProviderReference> parameterProviders) {
+        this.parameterProviders = parameterProviders;
+    }
+
+    public void setLatest(final boolean latest) {
+        this.latest = latest;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshotMetadata.java b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshotMetadata.java
new file mode 100644
index 0000000..5f7864d
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowSnapshotMetadata.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+public class RegisteredFlowSnapshotMetadata {
+    private String branch;
+    private String bucketIdentifier;
+    private String flowIdentifier;
+    private String version;
+    private long timestamp;
+    private String author;
+    private String comments;
+
+    public String getBranch() {
+        return branch;
+    }
+
+    public String getBucketIdentifier() {
+        return bucketIdentifier;
+    }
+
+    public String getFlowIdentifier() {
+        return flowIdentifier;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public String getAuthor() {
+        return author;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setBranch(final String branch) {
+        this.branch = branch;
+    }
+
+    public void setBucketIdentifier(String bucketIdentifier) {
+        this.bucketIdentifier = bucketIdentifier;
+    }
+
+    public void setFlowIdentifier(String flowIdentifier) {
+        this.flowIdentifier = flowIdentifier;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public void setAuthor(String author) {
+        this.author = author;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowVersionInfo.java b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowVersionInfo.java
new file mode 100644
index 0000000..58e058a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/RegisteredFlowVersionInfo.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.registry.flow;
+
+public class RegisteredFlowVersionInfo {
+
+    private long version;
+
+    public long getVersion() {
+        return version;
+    }
+
+    public void setVersion(long version) {
+        this.version = version;
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java b/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
new file mode 100644
index 0000000..152dcd8
--- /dev/null
+++ b/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.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.registry.flow;
+
+public enum VersionedFlowState {
+
+    /**
+     * We are unable to communicate with the Flow Registry in order to determine the appropriate state
+     */
+    SYNC_FAILURE("Failed to communicate with Flow Registry"),
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * is on the latest version of the Versioned Flow, but is different than the Versioned Flow that is
+     * stored in the Flow Registry.
+     */
+    LOCALLY_MODIFIED("Local changes have been made"),
+
+    /**
+     * This Process Group has not been modified since it was last synchronized with the Flow Registry, but
+     * the Flow Registry has a newer version of the flow than what is contained in this Process Group.
+     */
+    STALE("A newer version of this flow is available"),
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * has been modified since it was last synchronized with the Flow Registry, and the Flow Registry has
+     * a newer version of the flow than what is contained in this Process Group.
+     */
+    LOCALLY_MODIFIED_AND_STALE("Local changes have been made and a newer version of this flow is available"),
+
+    /**
+     * This Process Group and all child/descendant Process Groups are on the latest version of the flow in
+     * the Flow Registry and have no local modifications.
+     */
+    UP_TO_DATE("Flow version is current");
+
+
+    private final String description;
+
+    VersionedFlowState(final String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/AbstractReportingTask.java b/src/main/java/org/apache/nifi/reporting/AbstractReportingTask.java
new file mode 100644
index 0000000..1cf214e
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/AbstractReportingTask.java
@@ -0,0 +1,117 @@
+/*
+ * 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.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+
+import java.util.concurrent.TimeUnit;
+
+public abstract class AbstractReportingTask extends AbstractConfigurableComponent implements ReportingTask {
+
+    private String identifier;
+    private String name;
+    private long schedulingNanos;
+    private ControllerServiceLookup serviceLookup;
+    private ComponentLog logger;
+    private NodeTypeProvider nodeTypeProvider;
+
+    @Override
+    public final void initialize(final ReportingInitializationContext config) throws InitializationException {
+        identifier = config.getIdentifier();
+        logger = config.getLogger();
+        name = config.getName();
+        schedulingNanos = config.getSchedulingPeriod(TimeUnit.NANOSECONDS);
+        serviceLookup = config.getControllerServiceLookup();
+        nodeTypeProvider = config.getNodeTypeProvider();
+
+        init(config);
+    }
+
+    /**
+     * @return the {@link ControllerServiceLookup} that was passed to the
+     * {@link #initialize(ReportingInitializationContext)} method
+     */
+    protected final ControllerServiceLookup getControllerServiceLookup() {
+        return serviceLookup;
+    }
+
+    /**
+     * @return the {@link NodeTypeProvider} that was passed to the
+     * {@link #initialize(ReportingInitializationContext)} method
+     */
+    protected final NodeTypeProvider getNodeTypeProvider() {
+        return nodeTypeProvider;
+    }
+
+    /**
+     * @return the identifier of this Reporting Task
+     */
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the name of this Reporting Task
+     */
+    protected String getName() {
+        return name;
+    }
+
+    /**
+     * Sets various component information using the given context
+     * @param context the context to use for this reporting task
+     */
+    @OnScheduled
+    public void setComponentInfo(ConfigurationContext context) {
+        this.name = context.getName();
+    }
+
+    /**
+     * @param timeUnit of scheduling period
+     * @return the amount of times that elapses between the moment that this
+     * ReportingTask finishes its invocation of
+     * {@link #onTrigger(ReportingContext)} and the next time that
+     * {@link #onTrigger(ReportingContext)} is called.
+     */
+    protected long getSchedulingPeriod(final TimeUnit timeUnit) {
+        return timeUnit.convert(schedulingNanos, TimeUnit.NANOSECONDS);
+    }
+
+    /**
+     * Provides a mechanism by which subclasses can perform initialization of
+     * the Reporting Task before it is scheduled to be run
+     *
+     * @param config context
+     * @throws InitializationException if failure to init
+     */
+    protected void init(final ReportingInitializationContext config) throws InitializationException {
+    }
+
+    /**
+     * @return the logger that has been provided to the component by the
+     * framework in its initialize method
+     */
+    protected ComponentLog getLogger() {
+        return logger;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/Bulletin.java b/src/main/java/org/apache/nifi/reporting/Bulletin.java
new file mode 100644
index 0000000..e00ad3b
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/Bulletin.java
@@ -0,0 +1,156 @@
+/*
+ * 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 java.util.Date;
+
+/**
+ * A Bulletin is a construct that represents a message that is to be displayed
+ * to users to notify of a specific (usually fleeting) event.
+ */
+public abstract class Bulletin implements Comparable<Bulletin> {
+
+    private final Date timestamp;
+    private final long id;
+    private String nodeAddress;
+    private String level;
+    private String category;
+    private String message;
+
+    private String groupId;
+    private String groupName;
+    private String groupPath;
+    private String sourceId;
+    private String sourceName;
+    private ComponentType sourceType;
+    private String flowFileUuid;
+
+    protected Bulletin(final long id) {
+        this.timestamp = new Date();
+        this.id = id;
+    }
+
+    public String getNodeAddress() {
+        return nodeAddress;
+    }
+
+    public void setNodeAddress(String nodeAddress) {
+        this.nodeAddress = nodeAddress;
+    }
+
+    public String getCategory() {
+        return category;
+    }
+
+    public void setCategory(String category) {
+        this.category = category;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public String getLevel() {
+        return level;
+    }
+
+    public void setLevel(String level) {
+        this.level = level;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public Date getTimestamp() {
+        return timestamp;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getGroupName() {
+        return groupName;
+    }
+
+    public void setGroupName(String groupName) {
+        this.groupName = groupName;
+    }
+
+    public String getGroupPath() {
+        return groupPath;
+    }
+
+    public void setGroupPath(String groupPath) {
+        this.groupPath = groupPath;
+    }
+
+    public String getSourceId() {
+        return sourceId;
+    }
+
+    public void setSourceId(String sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    public void setSourceName(String sourceName) {
+        this.sourceName = sourceName;
+    }
+
+    public ComponentType getSourceType() {
+        return sourceType;
+    }
+
+    public void setSourceType(ComponentType sourceType) {
+        this.sourceType = sourceType;
+    }
+
+    public String getFlowFileUuid() {
+        return flowFileUuid;
+    }
+
+    public void setFlowFileUuid(String flowFileUuid) {
+        this.flowFileUuid = flowFileUuid;
+    }
+
+    @Override
+    public String toString() {
+        return "Bulletin{" + "id=" + id + ", message=" + message + ", sourceName=" + sourceName + ", sourceType=" + sourceType + '}';
+    }
+
+    @Override
+    public int compareTo(Bulletin b) {
+        if (b == null) {
+            return -1;
+        }
+
+        return -Long.compare(getId(), b.getId());
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/BulletinQuery.java b/src/main/java/org/apache/nifi/reporting/BulletinQuery.java
new file mode 100644
index 0000000..aae2eff
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/BulletinQuery.java
@@ -0,0 +1,118 @@
+/*
+ * 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 java.util.regex.Pattern;
+
+public class BulletinQuery {
+
+    private final ComponentType sourceType;
+    private final Pattern sourceIdPattern;
+    private final Pattern groupIdPattern;
+    private final Pattern namePattern;
+    private final Pattern messagePattern;
+    private final Long after;
+    private final Integer limit;
+
+    private BulletinQuery(final Builder builder) {
+        this.sourceType = builder.sourceType;
+        this.sourceIdPattern = builder.sourceIdPattern == null ? null : Pattern.compile(builder.sourceIdPattern);
+        this.groupIdPattern = builder.groupIdPattern == null ? null : Pattern.compile(builder.groupIdPattern);
+        this.namePattern = builder.namePattern == null ? null : Pattern.compile(builder.namePattern);
+        this.messagePattern = builder.messagePattern == null ? null : Pattern.compile(builder.messagePattern);
+        this.after = builder.after;
+        this.limit = builder.limit;
+    }
+
+    public ComponentType getSourceType() {
+        return sourceType;
+    }
+
+    public Pattern getSourceIdPattern() {
+        return sourceIdPattern;
+    }
+
+    public Pattern getGroupIdPattern() {
+        return groupIdPattern;
+    }
+
+    public Pattern getNamePattern() {
+        return namePattern;
+    }
+
+    public Pattern getMessagePattern() {
+        return messagePattern;
+    }
+
+    public Long getAfter() {
+        return after;
+    }
+
+    public Integer getLimit() {
+        return limit;
+    }
+
+    public static class Builder {
+
+        private ComponentType sourceType;
+        private String sourceIdPattern;
+        private String groupIdPattern;
+        private String namePattern;
+        private String messagePattern;
+        private Long after;
+        private Integer limit;
+
+        public Builder after(Long after) {
+            this.after = after;
+            return this;
+        }
+
+        public Builder groupIdMatches(String groupId) {
+            this.groupIdPattern = groupId;
+            return this;
+        }
+
+        public Builder sourceIdMatches(String sourceId) {
+            this.sourceIdPattern = sourceId;
+            return this;
+        }
+
+        public Builder sourceType(ComponentType sourceType) {
+            this.sourceType = sourceType;
+            return this;
+        }
+
+        public Builder nameMatches(String name) {
+            this.namePattern = name;
+            return this;
+        }
+
+        public Builder messageMatches(String message) {
+            this.messagePattern = message;
+            return this;
+        }
+
+        public Builder limit(Integer limit) {
+            this.limit = limit;
+            return this;
+        }
+
+        public BulletinQuery build() {
+            return new BulletinQuery(this);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/BulletinRepository.java b/src/main/java/org/apache/nifi/reporting/BulletinRepository.java
new file mode 100644
index 0000000..1bf5768
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/BulletinRepository.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.reporting;
+
+import java.util.List;
+
+/**
+ * The BulletinRepository provides a place to store and retrieve
+ * {@link Bulletin}s that have been created by the NiFi Framework and the
+ * Components that are running within the Framework.
+ */
+public interface BulletinRepository {
+
+    int MAX_BULLETINS_PER_COMPONENT = 5;
+    int MAX_BULLETINS_FOR_CONTROLLER = 10;
+
+    /**
+     * Adds a Bulletin to the repository.
+     *
+     * @param bulletin to add
+     */
+    void addBulletin(Bulletin bulletin);
+
+    /**
+     * @return the capacity for the number of bulletins for the controller
+     */
+    int getControllerBulletinCapacity();
+
+    /**
+     * @return the capacity for the number of bulletins per component
+     */
+    int getComponentBulletinCapacity();
+
+    /**
+     * Finds Bulletin's that meet the specified query.
+     *
+     * @param bulletinQuery indicates which bulletins are of interest
+     * @return bulletins that met the query
+     */
+    List<Bulletin> findBulletins(BulletinQuery bulletinQuery);
+
+    /**
+     * Finds all bulletins for the specified source component.
+     *
+     * @param sourceId id of the source
+     * @return bulletins for the given source
+     */
+    List<Bulletin> findBulletinsForSource(String sourceId);
+
+    /**
+     * Finds all bulletins for the specified source component that resides in the given group. While the
+     * {@link #findBulletinsForSource(String)} method may be used, this method is preferred when the ID of the group
+     * is known, as this is far more efficient.
+     *
+     * @param sourceId the id of the source component
+     * @param groupId the id of the process group
+     * @return bulletins for the given source
+     */
+    List<Bulletin> findBulletinsForSource(String sourceId, String groupId);
+
+    /**
+     * Finds all bulletins for the specified group.
+     *
+     * @param groupId id of the group
+     * @return bulletins for the given group
+     */
+    List<Bulletin> findBulletinsForGroupBySource(String groupId);
+
+    /**
+     * Finds all bulletins for the specified group.
+     *
+     * @param groupId id of the group
+     * @param maxPerComponent max responses wanted
+     * @return bulletins found
+     */
+    List<Bulletin> findBulletinsForGroupBySource(String groupId, int maxPerComponent);
+
+    /**
+     * @return all bulletins for the controller
+     */
+    List<Bulletin> findBulletinsForController();
+
+    /**
+     * Finds all bulletins for the controller;
+     *
+     * @param max limits the number of responses
+     * @return all bulletins for the controller
+     */
+    List<Bulletin> findBulletinsForController(int max);
+
+    /**
+     * @return the max ID of any bulletin that has been added, or -1 if no bulletins have been added
+     */
+    long getMaxBulletinId();
+}
diff --git a/src/main/java/org/apache/nifi/reporting/ComponentType.java b/src/main/java/org/apache/nifi/reporting/ComponentType.java
new file mode 100644
index 0000000..95cec81
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/ComponentType.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+/**
+ * An Enumeration for indicating which type of component a Bulletin is associated with
+ */
+public enum ComponentType {
+
+    /**
+     * Bulletin is associated with a Processor
+     */
+    PROCESSOR,
+
+    /**
+     * Bulletin is associated with a Remote Process Group
+     */
+    REMOTE_PROCESS_GROUP,
+
+    /**
+     * Bulletin is associated with an Input Port
+     */
+    INPUT_PORT,
+
+    /**
+     * Bulletin is associated with an Output Port
+     */
+    OUTPUT_PORT,
+
+    /**
+     * Bulletin is associated with a Reporting Task
+     */
+    REPORTING_TASK,
+
+    /**
+     * Bulletin is associated with a Flow Analysis Rule
+     */
+    FLOW_ANALYSIS_RULE,
+
+    /**
+     * Bulletin is associated with a Process Group
+     */
+    PROCESS_GROUP,
+
+    /**
+     * Bulletin is associated with a Parameter Provider
+     */
+    PARAMETER_PROVIDER,
+
+    /**
+     * Bulletin is associated with a Controller Service
+     */
+    CONTROLLER_SERVICE,
+
+    /**
+     * Bulletin is a system-level bulletin, associated with the Flow Controller
+     */
+    FLOW_CONTROLLER,
+
+    /**
+     * Bulletin is associated with a Flow Registry Client
+     */
+    FLOW_REGISTRY_CLIENT;
+}
diff --git a/src/main/java/org/apache/nifi/reporting/EventAccess.java b/src/main/java/org/apache/nifi/reporting/EventAccess.java
new file mode 100644
index 0000000..3e2c907
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/EventAccess.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.reporting;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.nifi.action.Action;
+import org.apache.nifi.controller.status.ProcessGroupStatus;
+import org.apache.nifi.diagnostics.StorageUsage;
+import org.apache.nifi.provenance.ProvenanceEventRecord;
+import org.apache.nifi.provenance.ProvenanceEventRepository;
+
+public interface EventAccess {
+
+    /**
+     * @return the status for all components in this Controller
+     */
+    ProcessGroupStatus getControllerStatus();
+
+    /**
+     * @param groupId Process Group Identifier
+     * @return the status of all components in the specified group.
+     */
+    ProcessGroupStatus getGroupStatus(final String groupId);
+
+    /**
+     * Convenience method to obtain Provenance Events starting with (and
+     * including) the given ID. If no event exists with that ID, the first event
+     * to be returned will be have an ID greater than <code>firstEventId</code>.
+     *
+     * @param firstEventId the ID of the first event to obtain
+     * @param maxRecords the maximum number of records to obtain
+     * @return event records matching query
+     * @throws java.io.IOException if unable to get records
+     */
+    List<ProvenanceEventRecord> getProvenanceEvents(long firstEventId, final int maxRecords) throws IOException;
+
+    /**
+     * @return the Provenance Event Repository
+     */
+    ProvenanceEventRepository getProvenanceRepository();
+
+    /**
+     * Obtains flow changes starting with (and including) the given action ID. If no action
+     * exists with that ID, the first action to be returned will have an ID greater than
+     * <code>firstActionId</code>.
+     *
+     * @param firstActionId the id of the first action to obtain
+     * @param maxActions the maximum number of actions to obtain
+     * @return actions with ids greater than or equal to firstActionID, up to the max number of actions
+     */
+    List<Action> getFlowChanges(int firstActionId, final int maxActions);
+
+    /**
+     * Returns the total number of bytes read by this instance (at the root process group level, i.e. all events) since the instance started
+     *
+     * @return the total number of bytes read by this instance
+     */
+    long getTotalBytesRead();
+
+    /**
+     * Returns the total number of bytes written by this instance (at the root process group level, i.e. all events) since the instance started
+     *
+     * @return the total number of bytes written by this instance
+     */
+    long getTotalBytesWritten();
+
+    /**
+     * Returns the total number of bytes sent by this instance (at the root process group level) since the instance started
+     *
+     * @return the total number of bytes sent by this instance
+     */
+    long getTotalBytesSent();
+
+    /**
+     * Returns the total number of bytes received by this instance (at the root process group level) since the instance started
+     *
+     * @return the total number of bytes received by this instance
+     */
+    long getTotalBytesReceived();
+
+    /**
+     * Returns the storage usage of all provenance repositories
+     * @return the map of all the storage usage
+     */
+    Map<String, StorageUsage> getProvenanceRepositoryStorageUsage();
+
+    /**
+     * Returns the storage usage of all content repositories
+     * @return the map of all the storage usage
+     */
+    Map<String, StorageUsage> getContentRepositoryStorageUsage();
+
+    /**
+     * Returns the storage usage of the flow file repository
+     * @return the storage usage
+     */
+    StorageUsage getFlowFileRepositoryStorageUsage();
+
+}
diff --git a/src/main/java/org/apache/nifi/reporting/InitializationException.java b/src/main/java/org/apache/nifi/reporting/InitializationException.java
new file mode 100644
index 0000000..c2f21b4
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/InitializationException.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+public class InitializationException extends Exception {
+
+    public InitializationException(String explanation) {
+        super(explanation);
+    }
+
+    public InitializationException(Throwable cause) {
+        super(cause);
+    }
+
+    public InitializationException(String explanation, Throwable cause) {
+        super(explanation, cause);
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/ReportingContext.java b/src/main/java/org/apache/nifi/reporting/ReportingContext.java
new file mode 100644
index 0000000..85cf844
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/ReportingContext.java
@@ -0,0 +1,107 @@
+/*
+ * 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 java.util.Map;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.state.StateManager;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+
+/**
+ * This interface provides a bridge between the NiFi Framework and a
+ * {@link ReportingTask}. This context allows a ReportingTask to access
+ * statistics, metrics, and monitoring information, as well as configuration
+ * supplied by the user.
+ */
+public interface ReportingContext extends PropertyContext {
+
+    /**
+     * @return a Map of all known {@link PropertyDescriptor}s to their
+     * configured properties. This Map will contain a <code>null</code> for any
+     * Property that has not been configured by the user, even if the
+     * PropertyDescriptor has a default value
+     */
+    Map<PropertyDescriptor, String> getProperties();
+
+    /**
+     * @return the {@link EventAccess} object that can be used to obtain
+     * information about specific events and reports that have happened
+     */
+    EventAccess getEventAccess();
+
+    /**
+     * @return the {@link BulletinRepository} that can be used to analyze
+     * Bulletins that have been emitted and register new Bulletins
+     */
+    BulletinRepository getBulletinRepository();
+
+    /**
+     * Creates a controller-level {@link Bulletin} with the given category, severity
+     * level, and message, so that the Bulletin can be added to the
+     * {@link BulletinRepository}. Access to this bulletin will be enforce through
+     * permissions on the controller.
+     *
+     * @param category of bulletin
+     * @param severity of bulletin
+     * @param message of bulletin
+     * @return new bulletin
+     */
+    Bulletin createBulletin(String category, Severity severity, String message);
+
+    /**
+     * Creates a {@link Bulletin} for the component with the specified
+     * identifier.
+     *
+     * @param componentId the ID of the component
+     * @param category the name of the bulletin's category
+     * @param severity the severity level of the bulletin
+     * @param message the bulletin's message
+     * @return new bulletin
+     */
+    Bulletin createBulletin(String componentId, String category, Severity severity, String message);
+
+    /**
+     * @return the {@link ControllerServiceLookup} which can be used to obtain
+     * Controller Services
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @return the StateManager that can be used to store and retrieve state for this component
+     */
+    StateManager getStateManager();
+
+    /**
+     * @return <code>true</code> if this instance of NiFi is configured to be part of a cluster, <code>false</code>
+     *         if this instance of NiFi is a standalone instance
+     */
+    boolean isClustered();
+
+    /**
+     * @return the ID of this node in the cluster, or <code>null</code> if either this node is not clustered or the Node Identifier
+     *         has not yet been established
+     */
+    String getClusterNodeIdentifier();
+
+    /**
+     * @return true if reporting analytics (connection status predictions, e.g.) are enabled, false otherwise
+     */
+    default boolean isAnalyticsEnabled() {
+        return false;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/reporting/ReportingInitializationContext.java b/src/main/java/org/apache/nifi/reporting/ReportingInitializationContext.java
new file mode 100644
index 0000000..978b421
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/ReportingInitializationContext.java
@@ -0,0 +1,86 @@
+/*
+ * 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 java.util.concurrent.TimeUnit;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+
+/**
+ * A ReportingConfiguration provides configuration information to a
+ * ReportingTask at the time of initialization
+ */
+public interface ReportingInitializationContext extends KerberosContext {
+
+    /**
+     * @return the identifier for this ReportingTask
+     */
+    String getIdentifier();
+
+    /**
+     * @return the configured name for this ReportingTask
+     */
+    String getName();
+
+    /**
+     * Returns the amount of time, in the given {@link TimeUnit} that will
+     * elapsed between the return of one execution of the
+     * {@link ReportingTask}'s
+     * {@link ReportingTask#onTrigger(ReportingContext) onTrigger} method and
+     * the time at which the method is invoked again. This method will return
+     * <code>-1L</code> if the Scheduling Strategy is not set to
+     * {@link SchedulingStrategy#TIMER_DRIVEN}
+     *
+     * @param timeUnit unit of time for scheduling
+     * @return period of time
+     */
+    long getSchedulingPeriod(TimeUnit timeUnit);
+
+    /**
+     * @return the {@link ControllerServiceLookup} which can be used to obtain
+     * Controller Services
+     */
+    ControllerServiceLookup getControllerServiceLookup();
+
+    /**
+     * @return a String representation of the scheduling period
+     */
+    String getSchedulingPeriod();
+
+    /**
+     * @return the {@link SchedulingStrategy} that is used to trigger the task
+     * to run
+     */
+    SchedulingStrategy getSchedulingStrategy();
+
+    /**
+     * @return a logger that can be used to log important events in a standard
+     * way and generate bulletins when appropriate
+     */
+    ComponentLog getLogger();
+
+    /**
+     * @return the {@link NodeTypeProvider} which can be used to detect the node
+     * type of this NiFi instance.
+     * @since Apache NiFi 1.5.0
+     */
+    NodeTypeProvider getNodeTypeProvider();
+
+}
diff --git a/src/main/java/org/apache/nifi/reporting/ReportingTask.java b/src/main/java/org/apache/nifi/reporting/ReportingTask.java
new file mode 100644
index 0000000..27855f7
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/ReportingTask.java
@@ -0,0 +1,104 @@
+/*
+ * 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.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.migration.PropertyConfiguration;
+
+/**
+ * Defines a task that is responsible for reporting status information to
+ * external destinations. All implementations of this class must have a default
+ * constructor.
+ *
+ * <p>
+ * <code>ReportingTask</code>s are discovered using Java's
+ * <code>ServiceLoader</code> mechanism. As a result, all implementations must
+ * follow these rules:
+ *
+ * <ul>
+ * <li>The implementation must implement this interface.</li>
+ * <li>The implementation must have a file named
+ * org.apache.nifi.reporting.ReportingTask located within the jar's
+ * <code>META-INF/services</code> directory. This file contains a list of
+ * fully-qualified class names of all <code>ReportingTask</code>s in the jar,
+ * one-per-line.
+ * <li>The implementation must support a default constructor.</li>
+ * </ul>
+ *
+ * <p>
+ * ReportingTasks are scheduled on a delayed interval with a single thread.
+ * Therefore, implementations are not required to be thread-safe.
+ * </p>
+ *
+ * <p>
+ * ReportingTasks may choose to annotate a method with the
+ * {@link OnConfigurationRestored @OnConfigurationRestored} annotation. If this is done, that method
+ * will be invoked after all properties have been set for the ReportingTask and
+ * before it is scheduled to run. If the method throws an Exception, the
+ * ReportingTask will be Administratively yielded and will not run for the
+ * configured period of time.
+ * </p>
+ */
+public interface ReportingTask extends ConfigurableComponent {
+
+    /**
+     * Provides the Reporting Task with access to objects that may be of use
+     * throughout the life of the service
+     *
+     * @param config of initialization context
+     * @throws org.apache.nifi.reporting.InitializationException if unable to init
+     */
+    void initialize(ReportingInitializationContext config) throws InitializationException;
+
+    /**
+     * This method is called on a scheduled interval to allow the Reporting Task
+     * to perform its tasks.
+     *
+     * @param context reporting context
+     */
+    void onTrigger(ReportingContext context);
+
+    /**
+     * Indicates whether this reporting task, configured with the given {@link ReportingContext}, stores state.
+     * @param context provides access to convenience methods for obtaining property values
+     * @return True if this reporting task stores state
+     */
+    default boolean isStateful(ReportingContext context) {
+        return this.getClass().isAnnotationPresent(Stateful.class);
+    }
+
+    /**
+     * <p>
+     * Allows for the migration of an old property configuration to a new configuration. This allows the Reporting Task to evolve over time,
+     * as it allows properties to be renamed, removed, or reconfigured.
+     * </p>
+     *
+     * <p>
+     * This method is called only when a Reporting Task is restored from a previous configuration. For example, when NiFi is restarted and the
+     * flow is restored from disk, when a previously configured flow is imported (e.g., from a JSON file that was exported or a NiFi Registry),
+     * or when a node joins a cluster and inherits a flow that has a new Reporting Task. Once called, the method will not be invoked again for this
+     * Reporting Task until NiFi is restarted.
+     * </p>
+     *
+     * @param config the current property configuration
+     */
+    default void migrateProperties(PropertyConfiguration config) {
+    }
+
+}
diff --git a/src/main/java/org/apache/nifi/reporting/Severity.java b/src/main/java/org/apache/nifi/reporting/Severity.java
new file mode 100644
index 0000000..8c2b27f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/Severity.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+public enum Severity {
+
+    INFO,
+    WARNING,
+    ERROR;
+}
diff --git a/src/main/java/org/apache/nifi/reporting/VerifiableReportingTask.java b/src/main/java/org/apache/nifi/reporting/VerifiableReportingTask.java
new file mode 100644
index 0000000..fe674be
--- /dev/null
+++ b/src/main/java/org/apache/nifi/reporting/VerifiableReportingTask.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.reporting;
+
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.components.ConfigVerificationResult;
+
+import java.util.List;
+
+/**
+ * <p>
+ *     Any Reporting Task that implements this interface will be provided the opportunity to verify
+ *     a given configuration of the Reporting Task. This allows the Reporting Task to provide meaningful feedback
+ *     to users when configuring the dataflow.
+ * </p>
+ *
+ * <p>
+ *     Generally speaking, verification differs from validation in that validation is expected to be very
+ *     quick and run often. If a Reporting Task is not valid, it cannot be started. However, verification may be
+ *     more expensive or time-consuming to complete. For example, validation may ensure that a username is
+ *     provided for connecting to an external service but should not perform any sort of network connection
+ *     in order to verify that the username is accurate. Verification, on the other hand, may create resources
+ *     such as network connections, may be more expensive to complete, and may be run only when a user invokes
+ *     the action (though verification may later occur at other stages, such as when starting a component).
+ * </p>
+ *
+ * <p>
+ *     Verification is allowed to be run only when a Reporting Task is fully stopped. I.e., it has no active threads
+ *     and currently has a state of STOPPED. Therefore, any initialization logic that may need to be performed
+ *     before the Reporting Task is triggered may also be required for verification. However, the framework is not responsible
+ *     for triggering the Lifecycle management stages, such as @OnScheduled before triggering the verification. Such
+ *     methods should be handled by the {@link #verify(ConfigurationContext, ComponentLog)} itself.
+ *     The {@link #verify(ConfigurationContext, ComponentLog)} method will only be called if the configuration is valid according to the
+ *     validation rules (i.e., all Property Descriptors' validators and customValidate methods have indicated that the configuration is valid).
+ * </p>
+ */
+public interface VerifiableReportingTask {
+
+    /**
+     * Verifies that the configuration defined by the given ConfigurationContext is valid.
+     *
+     * @param context the Configuration Context that contains the necessary configuration
+     * @param verificationLogger a logger that can be used during verification. While the typical logger can be used, doing so may result
+     * in producing bulletins, which can be confusing.
+     *
+     * @return a List of ConfigVerificationResults, each illustrating one step of the verification process that was completed
+     */
+    List<ConfigVerificationResult> verify(ConfigurationContext context, ComponentLog verificationLogger);
+
+}
diff --git a/src/main/java/org/apache/nifi/scheduling/ExecutionNode.java b/src/main/java/org/apache/nifi/scheduling/ExecutionNode.java
new file mode 100644
index 0000000..e41f703
--- /dev/null
+++ b/src/main/java/org/apache/nifi/scheduling/ExecutionNode.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.scheduling;
+
+/**
+ * Defines the Nodes where a given Component will be scheduled to run.
+ */
+public enum ExecutionNode {
+    /**
+     * A Component will be scheduled to run on all nodes.
+     */
+    ALL,
+    /**
+     * A Component will be scheduled to run on the primary node only.
+     */
+    PRIMARY;
+}
diff --git a/src/main/java/org/apache/nifi/scheduling/SchedulingStrategy.java b/src/main/java/org/apache/nifi/scheduling/SchedulingStrategy.java
new file mode 100644
index 0000000..40375cc
--- /dev/null
+++ b/src/main/java/org/apache/nifi/scheduling/SchedulingStrategy.java
@@ -0,0 +1,52 @@
+/*
+ * 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.scheduling;
+
+/**
+ * Defines a Scheduling Strategy to use when scheduling Components (Ports,
+ * Funnels, Processors) to run
+ */
+public enum SchedulingStrategy {
+
+    /**
+     * Components should be scheduled to run on a periodic interval that is
+     * user-defined with a user-defined number of concurrent tasks. All
+     * Components support Timer-Driven mode.
+     */
+    TIMER_DRIVEN(1, "0 sec"),
+    /**
+     * Indicates that the component will be scheduled to run according to a
+     * Cron-style expression
+     */
+    CRON_DRIVEN(1, "* * * * * ?");
+
+    private final int defaultConcurrentTasks;
+    private final String defaultSchedulingPeriod;
+
+    SchedulingStrategy(final int defaultConcurrentTasks, final String defaultSchedulingPeriod) {
+        this.defaultConcurrentTasks = defaultConcurrentTasks;
+        this.defaultSchedulingPeriod = defaultSchedulingPeriod;
+    }
+
+    public int getDefaultConcurrentTasks() {
+        return defaultConcurrentTasks;
+    }
+
+    public String getDefaultSchedulingPeriod() {
+        return defaultSchedulingPeriod;
+    }
+}
diff --git a/src/main/java/org/apache/nifi/search/SearchContext.java b/src/main/java/org/apache/nifi/search/SearchContext.java
new file mode 100644
index 0000000..24b0c2a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/search/SearchContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.search;
+
+import java.util.Map;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+
+public interface SearchContext {
+
+    /**
+     * @return the search term
+     */
+    String getSearchTerm();
+
+    /**
+     * @return the annotation data
+     */
+    String getAnnotationData();
+
+    /**
+     * @param property to get value of
+     * @return a PropertyValue that encapsulates the value configured for the
+     * given PropertyDescriptor
+     */
+    PropertyValue getProperty(PropertyDescriptor property);
+
+    /**
+     * @return a Map of all configured Properties
+     */
+    Map<PropertyDescriptor, String> getProperties();
+}
diff --git a/src/main/java/org/apache/nifi/search/SearchResult.java b/src/main/java/org/apache/nifi/search/SearchResult.java
new file mode 100644
index 0000000..7bc32a5
--- /dev/null
+++ b/src/main/java/org/apache/nifi/search/SearchResult.java
@@ -0,0 +1,75 @@
+/*
+ * 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.search;
+
+public class SearchResult {
+
+    private final String label;
+    private final String match;
+
+    private SearchResult(final Builder builder) {
+        this.label = builder.label;
+        this.match = builder.match;
+    }
+
+    /**
+     * @return the label for this search result
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * @return the matching string for this search result
+     */
+    public String getMatch() {
+        return match;
+    }
+
+    public static final class Builder {
+
+        private String label;
+        private String match;
+
+        /**
+         * Set the label for the search result.
+         *
+         * @param label to set
+         * @return the builder
+         */
+        public Builder label(final String label) {
+            this.label = label;
+            return this;
+        }
+
+        /**
+         * Set the matching string for the search result.
+         *
+         * @param match string
+         * @return the builder
+         */
+        public Builder match(final String match) {
+            this.match = match;
+            return this;
+        }
+
+        public SearchResult build() {
+            return new SearchResult(this);
+        }
+
+    }
+}
diff --git a/src/main/java/org/apache/nifi/search/Searchable.java b/src/main/java/org/apache/nifi/search/Searchable.java
new file mode 100644
index 0000000..c1a0a34
--- /dev/null
+++ b/src/main/java/org/apache/nifi/search/Searchable.java
@@ -0,0 +1,24 @@
+/*
+ * 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.search;
+
+import java.util.Collection;
+
+public interface Searchable {
+
+    Collection<SearchResult> search(SearchContext context);
+}
diff --git a/src/main/java/org/apache/nifi/time/DurationFormat.java b/src/main/java/org/apache/nifi/time/DurationFormat.java
new file mode 100644
index 0000000..6de336a
--- /dev/null
+++ b/src/main/java/org/apache/nifi/time/DurationFormat.java
@@ -0,0 +1,236 @@
+/*
+ * 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.time;
+
+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 DurationFormat {
+    private static final String UNION = "|";
+
+
+    // for Time Durations
+    private static final String NANOS = String.join(UNION, "ns", "nano", "nanos", "nanosecond", "nanoseconds");
+    private static final String MILLIS = String.join(UNION, "ms", "milli", "millis", "millisecond", "milliseconds");
+    private static final String SECS = String.join(UNION, "s", "sec", "secs", "second", "seconds");
+    private static final String MINS = String.join(UNION, "m", "min", "mins", "minute", "minutes");
+    private static final String HOURS = String.join(UNION, "h", "hr", "hrs", "hour", "hours");
+    private static final String DAYS = String.join(UNION, "d", "day", "days");
+    private static final String WEEKS = String.join(UNION, "w", "wk", "wks", "week", "weeks");
+
+    private static final String VALID_TIME_UNITS = String.join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS);
+    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<Long> TIME_UNIT_MULTIPLIERS = Arrays.asList(1000L, 1000L, 1000L, 60L, 60L, 24L);
+
+    private DurationFormat() {
+    }
+
+    /**
+     * 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.
+     * Use {@link #getPreciseTimeDuration(String, TimeUnit)} if fractional values are desirable
+     *
+     * @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
+     * @see #getPreciseTimeDuration(String, TimeUnit)
+     */
+    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.
+     * <p>
+     * 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.
+     * <p>
+     * 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).
+     * <p>
+     * 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).
+     * <p>
+     * This method handles decimal values over {@code 1 ns}, but {@code < 1 ns} will return {@code 0} in any other unit.
+     * <p>
+     * Examples:
+     * <p>
+     * "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");
+        }
+
+        final String duration = matcher.group(1);
+        final String units = matcher.group(2);
+
+        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.
+     * <p>
+     * Examples:
+     * <p>
+     * 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
+     */
+    static List<Object> 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)) {
+            final long rounded = Math.round(decimal);
+            return Arrays.asList(new Object[]{rounded, 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
+     */
+    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<Long> 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
+     */
+    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
+     */
+    private static boolean isWeek(final String rawUnit) {
+        return switch (rawUnit) {
+            case "w", "wk", "wks", "week", "weeks" -> true;
+            default -> 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) {
+        return switch (rawUnit.toLowerCase()) {
+            case "ns", "nano", "nanos", "nanoseconds" -> TimeUnit.NANOSECONDS;
+            case "µs", "micro", "micros", "microseconds" -> TimeUnit.MICROSECONDS;
+            case "ms", "milli", "millis", "milliseconds" -> TimeUnit.MILLISECONDS;
+            case "s", "sec", "secs", "second", "seconds" -> TimeUnit.SECONDS;
+            case "m", "min", "mins", "minute", "minutes" -> TimeUnit.MINUTES;
+            case "h", "hr", "hrs", "hour", "hours" -> TimeUnit.HOURS;
+            case "d", "day", "days" -> TimeUnit.DAYS;
+            default -> throw new IllegalArgumentException("Could not parse '" + rawUnit + "' to TimeUnit");
+        };
+    }
+}
diff --git a/src/test/java/org/apache/nifi/components/EnumDescribedValue.java b/src/test/java/org/apache/nifi/components/EnumDescribedValue.java
new file mode 100644
index 0000000..ede70c9
--- /dev/null
+++ b/src/test/java/org/apache/nifi/components/EnumDescribedValue.java
@@ -0,0 +1,61 @@
+/*
+ * 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.components;
+
+public enum EnumDescribedValue implements DescribedValue {
+
+    GREEN {
+        @Override
+        public String getDisplayName() {
+            return "GreenDisplayName";
+        }
+
+        @Override
+        public String getDescription() {
+            return "GreenDescription";
+        }
+    },
+
+    RED {
+        @Override
+        public String getDisplayName() {
+            return "RedDisplayName";
+        }
+
+        @Override
+        public String getDescription() {
+            return "RedDescription";
+        }
+    },
+
+    BLUE {
+        @Override
+        public String getDisplayName() {
+            return "BlueDisplayName";
+        }
+
+        @Override
+        public String getDescription() {
+            return "BlueDescription";
+        }
+    };
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+}
diff --git a/src/test/java/org/apache/nifi/components/EnumNotDescribedValue.java b/src/test/java/org/apache/nifi/components/EnumNotDescribedValue.java
new file mode 100644
index 0000000..a280c9c
--- /dev/null
+++ b/src/test/java/org/apache/nifi/components/EnumNotDescribedValue.java
@@ -0,0 +1,21 @@
+/*
+ * 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.components;
+
+public enum EnumNotDescribedValue {
+    GREEN, RED, BLUE;
+}
diff --git a/src/test/java/org/apache/nifi/components/TestPropertyDescriptor.java b/src/test/java/org/apache/nifi/components/TestPropertyDescriptor.java
new file mode 100644
index 0000000..c3288e8
--- /dev/null
+++ b/src/test/java/org/apache/nifi/components/TestPropertyDescriptor.java
@@ -0,0 +1,345 @@
+/*
+ * 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.components;
+
+import org.apache.nifi.components.PropertyDescriptor.Builder;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+
+public class TestPropertyDescriptor {
+
+    private static final String DEFAULT_VALUE = "Default Value";
+    private static final String DEPENDENT_PROPERTY_NAME = "dependentProperty";
+
+    @Nested
+    class RegardingDefaultValue {
+        @Test
+        void supportsStringValues() {
+            final PropertyDescriptor descriptor = builder().defaultValue(DEFAULT_VALUE).build();
+
+            assertEquals(DEFAULT_VALUE, descriptor.getDefaultValue());
+        }
+
+        @Test
+        void supportsDescribedValuesValues() {
+            final PropertyDescriptor descriptor = builder().defaultValue(EnumDescribedValue.GREEN).build();
+
+            assertEquals(EnumDescribedValue.GREEN.getValue(), descriptor.getDefaultValue());
+        }
+
+        /**
+         * Regression test for issue NIFI-49, to ensure that if a Processor's Property's
+         * Default Value is not allowed, the Exception thrown should indicate what the default value is
+         */
+        @Test
+        void throwsIllegalStateExceptionWhenDefaultValueNotInAllowableValues() {
+            IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
+                builder().allowableValues("NOT DEFAULT", "OTHER NOT DEFAULT").defaultValue(DEFAULT_VALUE).build();
+            });
+            assertTrue(exception.getMessage().contains("[" + DEFAULT_VALUE + "]"));
+        }
+
+        @Test
+        void canBeCleared() {
+            final PropertyDescriptor descriptorWithDefault = builder().defaultValue(DEFAULT_VALUE).build();
+            final PropertyDescriptor resetDescriptor = builder(descriptorWithDefault).clearDefaultValue().build();
+
+            assertNull(resetDescriptor.getDefaultValue());
+        }
+    }
+
+    @Nested
+    class RegardingAllowableValues {
+
+        private static final Comparator<AllowableValue> allowableValueComparator = Comparator.comparing(AllowableValue::getValue);
+        private final List<AllowableValue> expectedMinimalAllowableValues =
+                List.of(new AllowableValue("GREEN"), new AllowableValue("RED"), new AllowableValue("BLUE"));
+        private final List<AllowableValue> expectedAllowableValuesWithDescription =
+                Arrays.stream(EnumDescribedValue.values()).map(AllowableValue::fromDescribedValue).toList();
+
+        @Test
+        void supportsStringVarArgValues() {
+            final List<AllowableValue> expected = expectedMinimalAllowableValues;
+
+            final PropertyDescriptor descriptor = builder().allowableValues("GREEN", "RED", "BLUE").build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsStringSetValues() {
+            final List<AllowableValue> expected = sort(expectedMinimalAllowableValues);
+
+            final PropertyDescriptor descriptor = builder().allowableValues(Set.of("GREEN", "RED", "BLUE")).build();
+            // the iteration order of sets is not guaranteed by all implementations, thus we unify the order here
+            final List<AllowableValue> actual = sort(descriptor.getAllowableValues());
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsEnumArrayValues() {
+            final List<AllowableValue> expected = expectedMinimalAllowableValues;
+
+            final PropertyDescriptor descriptor = builder().allowableValues(EnumNotDescribedValue.values()).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        void supportsDescribedValueEnumArrayValues() {
+            final List<AllowableValue> expected = expectedAllowableValuesWithDescription;
+
+            final Enum[] enumArray = EnumDescribedValue.values();
+            final PropertyDescriptor descriptor = builder().allowableValues(enumArray).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsEnumClassValues() {
+            final List<AllowableValue> expected = expectedMinimalAllowableValues;
+
+            final PropertyDescriptor descriptor = builder().allowableValues(EnumNotDescribedValue.class).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsDescribedValueEnumClassValues() {
+            final List<AllowableValue> expected = expectedAllowableValuesWithDescription;
+
+            final PropertyDescriptor descriptor = builder().allowableValues(EnumDescribedValue.class).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsEnumSetValues() {
+            final List<AllowableValue> expected = expectedMinimalAllowableValues;
+
+            final PropertyDescriptor descriptor = builder().allowableValues(EnumSet.allOf(EnumNotDescribedValue.class)).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsDescribedValueEnumSetValues() {
+            final List<AllowableValue> expected = expectedAllowableValuesWithDescription;
+
+            final PropertyDescriptor descriptor = builder().allowableValues(EnumSet.allOf(EnumDescribedValue.class)).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        @Test
+        void supportsDescribedValueVarArgValues() {
+            final List<AllowableValue> expected = expectedAllowableValuesWithDescription;
+
+            final PropertyDescriptor descriptor = builder()
+                    .allowableValues(EnumDescribedValue.GREEN, EnumDescribedValue.RED, EnumDescribedValue.BLUE).build();
+            final List<AllowableValue> actual = descriptor.getAllowableValues();
+
+            assertEquals(expected, actual); // equals only compares getValue()
+            assertEquals(displayNamesOf(expected), displayNamesOf(actual));
+            assertEquals(descriptionsOf(expected), descriptionsOf(actual));
+        }
+
+        private List<AllowableValue> sort(final List<AllowableValue> allowableValues) {
+            return allowableValues.stream().sorted(allowableValueComparator).toList();
+        }
+
+        private List<String> displayNamesOf(final List<AllowableValue> allowableValues) {
+            return allowableValues.stream().map(AllowableValue::getDisplayName).toList();
+        }
+
+        private List<String> descriptionsOf(final List<AllowableValue> allowableValues) {
+            return allowableValues.stream().map(AllowableValue::getDescription).toList();
+        }
+    }
+
+    @Test
+    void testDependsOnWithEnumValue() {
+        final PropertyDescriptor dependentPropertyDescriptor = new PropertyDescriptor.Builder()
+                .name(DEPENDENT_PROPERTY_NAME)
+                .build();
+
+        final PropertyDescriptor propertyDescriptor = new PropertyDescriptor.Builder()
+                .name("enumDependsOnDescriptor")
+                .dependsOn(dependentPropertyDescriptor, EnumDescribedValue.RED)
+                .build();
+
+        assertNotNull(propertyDescriptor);
+
+        final Set<PropertyDependency> dependencies = propertyDescriptor.getDependencies();
+        assertEquals(1, dependencies.size());
+        final PropertyDependency dependency = dependencies.iterator().next();
+        assertEquals(DEPENDENT_PROPERTY_NAME, dependency.getPropertyName());
+        final Set<String> dependentValues = dependency.getDependentValues();
+        assertEquals(1, dependentValues.size());
+        final String dependentValue = dependentValues.iterator().next();
+        assertEquals(EnumDescribedValue.RED.getValue(), dependentValue);
+    }
+
+    @Test
+    void testExternalResourceIgnoredIfELWithAttributesPresent() {
+        final PropertyDescriptor descriptor = new PropertyDescriptor.Builder()
+                .name("dir")
+                .identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE)
+                .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+                .required(false)
+                .build();
+
+        final ValidationContext validationContext = Mockito.mock(ValidationContext.class);
+        Mockito.when(validationContext.isExpressionLanguagePresent(anyString())).thenReturn(true);
+        Mockito.when(validationContext.isExpressionLanguageSupported(anyString())).thenReturn(true);
+        Mockito.when(validationContext.newPropertyValue(anyString())).thenAnswer(invocation -> {
+            final String inputArg = invocation.getArgument(0);
+            return inputArg.replace("${TestPropertyDescriptor.Var1}", "__my_var__").replaceAll("\\$\\{.*}", "");
+        });
+
+        assertTrue(descriptor.validate("${TestPropertyDescriptor.Var1}", validationContext).isValid());
+    }
+
+    @Test
+    void testExternalResourceConsideredIfELVarRegistryPresent() {
+        final PropertyDescriptor descriptor = new PropertyDescriptor.Builder()
+                .name("dir")
+                .identifiesExternalResource(ResourceCardinality.SINGLE, ResourceType.FILE, ResourceType.DIRECTORY)
+                .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
+                .required(false)
+                .build();
+
+        final AtomicReference<String> variable = new AtomicReference<>("__my_var__");
+        final ValidationContext validationContext = Mockito.mock(ValidationContext.class);
+        Mockito.when(validationContext.isExpressionLanguagePresent(anyString())).thenReturn(true);
+        Mockito.when(validationContext.isExpressionLanguageSupported(anyString())).thenReturn(true);
+        Mockito.when(validationContext.newPropertyValue(anyString())).thenAnswer(invocation -> {
+            final String inputArg = invocation.getArgument(0);
+            final String evaluatedValue = inputArg.replace("${TestPropertyDescriptor.Var1}", variable.get().replaceAll("\\$\\{.*}", ""));
+
+            final PropertyValue propertyValue = Mockito.mock(PropertyValue.class);
+            Mockito.when(propertyValue.getValue()).thenReturn(evaluatedValue);
+            Mockito.when(propertyValue.evaluateAttributeExpressions()).thenReturn(propertyValue);
+            return propertyValue;
+        });
+
+        // Should not be valid because Expression Language scope is ENVIRONMENT, so the ${TestPropertyDescriptor.Var1} will be replaced with
+        // __my_var__, and __my_var__ does not exist.
+        assertFalse(descriptor.validate("${TestPropertyDescriptor.Var1}", validationContext).isValid());
+
+        // Will now be valid because variable changed to 'target', which does exist.
+        variable.set("target");
+        assertTrue(descriptor.validate("${TestPropertyDescriptor.Var1}", validationContext).isValid());
+
+        // Consider if Expression Language is not supported.
+        Mockito.when(validationContext.isExpressionLanguageSupported(anyString())).thenReturn(false);
+        final PropertyDescriptor withElNotAllowed = new PropertyDescriptor.Builder()
+                .fromPropertyDescriptor(descriptor)
+                .expressionLanguageSupported(ExpressionLanguageScope.NONE)
+                .build();
+
+        // Expression will not be evaluated, so the directory being looked at will literally be ${TestPropertyDescriptor.Var1}
+        assertFalse(withElNotAllowed.validate("${TestPropertyDescriptor.Var1}", validationContext).isValid());
+
+        // Test the literal value 'target'
+        assertTrue(withElNotAllowed.validate("target", validationContext).isValid());
+    }
+
+    @Test
+    void testClearingValues() {
+        final PropertyDescriptor dep1 = new PropertyDescriptor.Builder()
+                .name("dep1")
+                .allowableValues("delVal1")
+                .build();
+
+        final PropertyDescriptor pd1 = new PropertyDescriptor.Builder()
+                .name("test")
+                .addValidator(Validator.VALID)
+                .allowableValues("val1")
+                .dependsOn(dep1, "depVal1")
+                .build();
+
+        final PropertyDescriptor pd2 = new PropertyDescriptor.Builder()
+                .fromPropertyDescriptor(pd1)
+                .clearValidators()
+                .clearAllowableValues()
+                .clearDependsOn()
+                .build();
+
+        assertEquals("test", pd1.getName());
+        assertFalse(pd1.getValidators().isEmpty());
+        assertFalse(pd1.getDependencies().isEmpty());
+        assertNotNull(pd1.getAllowableValues());
+
+        assertEquals("test", pd2.getName());
+        assertTrue(pd2.getValidators().isEmpty());
+        assertTrue(pd2.getDependencies().isEmpty());
+        assertNull(pd2.getAllowableValues());
+    }
+
+    private Builder builder() {
+        return new PropertyDescriptor.Builder().name("propertyName");
+    }
+
+    private Builder builder(final PropertyDescriptor propertyDescriptor) {
+        return new PropertyDescriptor.Builder().fromPropertyDescriptor(propertyDescriptor);
+    }
+}
diff --git a/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferenceFactory.java b/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferenceFactory.java
new file mode 100644
index 0000000..5a297d9
--- /dev/null
+++ b/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferenceFactory.java
@@ -0,0 +1,94 @@
+/*
+ * 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.components.resource;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestStandardResourceReferenceFactory {
+
+    private final StandardResourceReferenceFactory subject = new StandardResourceReferenceFactory();
+
+    @Test
+    public void testCreateResourceReferences() {
+        String value = "/dir1/test1.jar,/dir2/test2.jar";
+        ResourceDefinition resourceDefinition = createResourceDefinition();
+
+        ResourceReferences resourceReferences = subject.createResourceReferences(value, resourceDefinition);
+
+        assertNotNull(resourceReferences);
+
+        List<ResourceReference> resourceReferencesList = resourceReferences.asList();
+        assertNotNull(resourceReferencesList);
+        assertEquals(2, resourceReferencesList.size());
+
+        assertResourceReference(resourceReferencesList.get(0), "/dir1/test1.jar");
+        assertResourceReference(resourceReferencesList.get(1), "/dir2/test2.jar");
+    }
+
+    @Test
+    public void testCreateResourceReferencesWhenValueIsNull() {
+        String value = null;
+        ResourceDefinition resourceDefinition = createResourceDefinition();
+
+        ResourceReferences resourceReferences = subject.createResourceReferences(value, resourceDefinition);
+
+        assertEmptyResourceReferences(resourceReferences);
+    }
+
+    @Test
+    public void testCreateResourceReferencesWhenValueIsEmpty() {
+        String value = "";
+        ResourceDefinition resourceDefinition = createResourceDefinition();
+
+        ResourceReferences resourceReferences = subject.createResourceReferences(value, resourceDefinition);
+
+        assertEmptyResourceReferences(resourceReferences);
+    }
+    @Test
+    public void testCreateResourceReferencesWhenResourceDefinitionIsNull() {
+        String value = "/dir1/test1.jar";
+        ResourceDefinition resourceDefinition = null;
+
+        ResourceReferences resourceReferences = subject.createResourceReferences(value, resourceDefinition);
+
+        assertEmptyResourceReferences(resourceReferences);
+    }
+
+    private StandardResourceDefinition createResourceDefinition() {
+        return new StandardResourceDefinition(ResourceCardinality.SINGLE, Collections.singleton(ResourceType.FILE));
+    }
+
+    private void assertResourceReference(ResourceReference resourceReference, String location) {
+        assertEquals(new File(location).getAbsolutePath(), resourceReference.getLocation());
+        assertEquals(ResourceType.FILE, resourceReference.getResourceType());
+    }
+
+    private void assertEmptyResourceReferences(ResourceReferences resourceReferences) {
+        assertNotNull(resourceReferences);
+        assertNotNull(resourceReferences.asList());
+        assertTrue(resourceReferences.asList().isEmpty());
+    }
+}
diff --git a/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferences.java b/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferences.java
new file mode 100644
index 0000000..ea2a0a5
--- /dev/null
+++ b/src/test/java/org/apache/nifi/components/resource/TestStandardResourceReferences.java
@@ -0,0 +1,163 @@
+/*
+ * 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.components.resource;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestStandardResourceReferences {
+
+    @Test
+    public void testAsURLs() {
+        final ResourceReferences references = new StandardResourceReferences(Arrays.asList(
+            new FileResourceReference(new File("target/1.txt")),
+            new FileResourceReference(new File("target/2.txt"))
+        ));
+
+        final List<URL> urls = references.asURLs();
+        assertEquals(2, urls.size());
+        for (final URL url : urls) {
+            final String filename = url.getFile();
+            assertTrue(filename.endsWith(".txt"));
+
+            final File parentFile = new File(filename).getParentFile();
+            assertEquals("target", parentFile.getName());
+        }
+    }
+
+    @Test
+    public void testFlattenRecursively() throws IOException {
+        // Create directory structure:
+        // target/dirs/
+        // target/dirs/0
+        // target/dirs/0/0
+        // target/dirs/0/0/0.txt
+        // target/dirs/0/0/1.txt
+        // ...
+        // target/dirs/2/2/2.txt
+        final int numDirs = 3;
+        final int numSubDirs = 3;
+        final int numFiles = 3;
+
+        final File baseDir = new File("target/dirs");
+        for (int i = 0; i < numDirs; i++) {
+            final File dir = new File(baseDir, String.valueOf(i));
+            dir.mkdirs();
+
+            for (int j = 0; j < numSubDirs; j++) {
+                final File subdir = new File(dir, String.valueOf(j));
+                subdir.mkdirs();
+
+                for (int k = 0; k < numFiles; k++) {
+                    final File file = new File(subdir, k + ".txt");
+
+                    try (final OutputStream fos = new FileOutputStream(file)) {
+                        fos.write((k + ".txt").getBytes(StandardCharsets.UTF_8));
+                    }
+                }
+            }
+        }
+
+        final ResourceReferences references = new StandardResourceReferences(Collections.singletonList(new FileResourceReference(baseDir)));
+        assertEquals(1, references.getCount());
+        assertEquals(ResourceType.DIRECTORY, references.asList().get(0).getResourceType());
+
+        final ResourceReferences flattened = references.flattenRecursively();
+        assertEquals(numDirs * numSubDirs * numFiles, flattened.getCount());
+
+        final List<ResourceReference> flattenedReferences = flattened.asList();
+        assertEquals(numDirs * numSubDirs * numFiles, flattenedReferences.size());
+
+        // Ensure that each file that was flattened has a unique filename and the file exists.
+        final Set<String> filenames = new HashSet<>();
+        for (final ResourceReference reference : flattenedReferences) {
+            assertEquals(ResourceType.FILE, reference.getResourceType());
+
+            final String filename = reference.getLocation();
+            assertTrue(filename.endsWith(".txt"));
+
+            filenames.add(filename);
+            assertTrue(new File(filename).exists());
+        }
+
+        assertEquals(numDirs * numSubDirs * numFiles, filenames.size());
+    }
+
+    @Test
+    public void testFlatten() throws IOException {
+        // Create directory structure:
+        // target/dir
+        // target/dir/0
+        // target/dir/0/0.txt
+        // target/dir/0/1.txt
+        // ...
+        // target/dir/0.txt
+        // target/dir/1.txt
+        // ...
+        final int numFiles = 3;
+
+        final File baseDir = new File("target/dir");
+        baseDir.mkdirs();
+
+        for (int i = 0; i < numFiles; i++) {
+            final File file = new File(baseDir, i + ".txt");
+
+            try (final OutputStream fos = new FileOutputStream(file)) {
+                fos.write((i + ".txt").getBytes(StandardCharsets.UTF_8));
+            }
+        }
+
+        final ResourceReferences references = new StandardResourceReferences(Collections.singletonList(new FileResourceReference(baseDir)));
+        assertEquals(1, references.getCount());
+        assertEquals(ResourceType.DIRECTORY, references.asList().get(0).getResourceType());
+
+        final ResourceReferences flattened = references.flatten();
+        assertEquals(numFiles, flattened.getCount());
+
+        final List<ResourceReference> flattenedReferences = flattened.asList();
+        assertEquals(numFiles, flattenedReferences.size());
+
+        // Ensure that each file that was flattened has a unique filename and the file exists.
+        final Set<String> filenames = new HashSet<>();
+        for (final ResourceReference reference : flattenedReferences) {
+            assertEquals(ResourceType.FILE, reference.getResourceType());
+
+            final String filename = reference.getLocation();
+            assertTrue(filename.endsWith(".txt"));
+
+            filenames.add(filename);
+            assertTrue(new File(filename).exists());
+        }
+
+        assertEquals(numFiles, filenames.size());
+    }
+}
diff --git a/src/test/java/org/apache/nifi/parameter/TestParameter.java b/src/test/java/org/apache/nifi/parameter/TestParameter.java
new file mode 100644
index 0000000..cc366ff
--- /dev/null
+++ b/src/test/java/org/apache/nifi/parameter/TestParameter.java
@@ -0,0 +1,218 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.asset.Asset;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestParameter {
+
+    @Test
+    public void testCreateParameterWithValue() {
+        final Parameter parameter = new Parameter.Builder()
+            .name("A")
+            .value("value")
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals("value", parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertTrue(parameter.getReferencedAssets().isEmpty());
+    }
+
+    @Test
+    public void testCreateParameterWithSingleReference() {
+        final File file = new File("file");
+        final Asset asset = new MockAsset("id", "parmContext", "name", file, "asset-digest");
+
+        final Parameter parameter = new Parameter.Builder()
+            .name("A")
+            .referencedAssets(List.of(asset))
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals(file.getAbsolutePath(), parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(1, parameter.getReferencedAssets().size());
+        assertEquals(asset, parameter.getReferencedAssets().getFirst());
+    }
+
+    @Test
+    public void testCreateParameterWithMultipleReferences() {
+        final File file1 = new File("file1");
+        final File file2 = new File("file2");
+        final File file3 = new File("file3");
+        final Asset asset1 = new MockAsset("id1", "parmContext", "name1", file1, "asset-digest");
+        final Asset asset2 = new MockAsset("id2", "parmContext", "name2", file2, "asset-digest");
+        final Asset asset3 = new MockAsset("id3", "parmContext", "name3", file3, "asset-digest");
+
+        final Parameter parameter = new Parameter.Builder()
+            .name("A")
+            .referencedAssets(List.of(asset1, asset2, asset3))
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals(file1.getAbsolutePath() + "," + file2.getAbsolutePath() + "," + file3.getAbsolutePath(), parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(3, parameter.getReferencedAssets().size());
+    }
+
+    @Test
+    public void testCreateParameterWithValueThenAsset() {
+        final File file = new File("file");
+        final Asset asset = new MockAsset("id", "parmContext", "name", file, "asset-digest");
+
+        final Parameter parameter = new Parameter.Builder()
+            .name("A")
+            .value("value")
+            .referencedAssets(List.of(asset))
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals(file.getAbsolutePath(), parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(1, parameter.getReferencedAssets().size());
+        assertEquals(asset, parameter.getReferencedAssets().getFirst());
+    }
+
+    @Test
+    public void testCreateParameterAssetThenValue() {
+        final File file = new File("file");
+        final Asset asset = new MockAsset("id", "parmContext", "name", file, "asset-digest");
+
+        final Parameter parameter = new Parameter.Builder()
+            .name("A")
+            .referencedAssets(List.of(asset))
+            .value("value")
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals("value", parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(0, parameter.getReferencedAssets().size());
+    }
+
+    @Test
+    public void testCreateParameterFromOtherThenOverrideWithAsset() {
+        final File file = new File("file");
+        final Asset asset = new MockAsset("id", "parmContext", "name", file, "asset-digest");
+
+        final Parameter original = new Parameter.Builder()
+            .name("A")
+            .value("value")
+            .build();
+
+        final Parameter parameter = new Parameter.Builder()
+            .fromParameter(original)
+            .referencedAssets(List.of(asset))
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(1, parameter.getReferencedAssets().size());
+        assertEquals(asset, parameter.getReferencedAssets().getFirst());
+    }
+
+    @Test
+    public void testCreateParameterFromOtherThenOverrideWithValue() {
+        final File file = new File("file");
+        final Asset asset = new MockAsset("id", "parmContext", "name", file, "asset-digest");
+
+        final Parameter original = new Parameter.Builder()
+            .name("A")
+            .referencedAssets(List.of(asset))
+            .build();
+
+        final Parameter parameter = new Parameter.Builder()
+            .fromParameter(original)
+            .value("value")
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals("value", parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(0, parameter.getReferencedAssets().size());
+    }
+
+    @Test
+    public void testCreateParameterFromOtherThenOverrideWithDifferentValue() {
+        final Parameter original = new Parameter.Builder()
+            .name("A")
+            .value("value 1")
+            .build();
+
+        final Parameter parameter = new Parameter.Builder()
+            .fromParameter(original)
+            .value("value 2")
+            .build();
+
+        assertEquals("A", parameter.getDescriptor().getName());
+        assertEquals("value 2", parameter.getValue());
+        assertNotNull(parameter.getReferencedAssets());
+        assertEquals(0, parameter.getReferencedAssets().size());
+    }
+
+    private static class MockAsset implements Asset {
+        private final String identifier;
+        private final String parameterContextIdentifier;
+        private final String name;
+        private final File file;
+        private final String digest;
+
+        public MockAsset(final String identifier, final String parameterContextIdentifier, final String name, final File file, final String digest) {
+            this.identifier = identifier;
+            this.parameterContextIdentifier = parameterContextIdentifier;
+            this.name = name;
+            this.file = file;
+            this.digest = digest;
+        }
+
+        @Override
+        public String getIdentifier() {
+            return identifier;
+        }
+
+        @Override
+        public String getParameterContextIdentifier() {
+            return parameterContextIdentifier;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public File getFile() {
+            return file;
+        }
+
+        @Override
+        public Optional<String> getDigest() {
+            return Optional.ofNullable(digest);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/nifi/processor/TestDataUnit.java b/src/test/java/org/apache/nifi/processor/TestDataUnit.java
new file mode 100644
index 0000000..6df51f7
--- /dev/null
+++ b/src/test/java/org/apache/nifi/processor/TestDataUnit.java
@@ -0,0 +1,41 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TestDataUnit {
+
+    @Test
+    public void testParseWithIntegerValue() {
+        assertEquals(300L, DataUnit.parseDataSize("300 B", DataUnit.B).longValue());
+        assertEquals(300L * 1024L, DataUnit.parseDataSize("300 KB", DataUnit.B).longValue());
+        assertEquals(300L * 1024L * 1024L, DataUnit.parseDataSize("300 MB", DataUnit.B).longValue());
+        assertEquals(300L * 1024L * 1024L * 1024L, DataUnit.parseDataSize("300 GB", DataUnit.B).longValue());
+    }
+
+    @Test
+    public void testParseWithDecimalValue() {
+        assertEquals(300L, DataUnit.parseDataSize("300 B", DataUnit.B).longValue());
+        assertEquals((long) (3.22D * 1024D), DataUnit.parseDataSize("3.22 KB", DataUnit.B).longValue());
+        assertEquals((long) (3.22D * 1024D * 1024D), DataUnit.parseDataSize("3.22 MB", DataUnit.B).longValue());
+        assertEquals((long) (3.22D * 1024D * 1024D * 1024D), DataUnit.parseDataSize("3.22 GB", DataUnit.B).longValue());
+    }
+
+}
diff --git a/src/test/java/org/apache/nifi/time/TestDurationFormat.java b/src/test/java/org/apache/nifi/time/TestDurationFormat.java
new file mode 100644
index 0000000..3259de7
--- /dev/null
+++ b/src/test/java/org/apache/nifi/time/TestDurationFormat.java
@@ -0,0 +1,278 @@
+/*
+ * 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.time;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestDurationFormat {
+
+    @ParameterizedTest
+    @MethodSource("getParse")
+    public void testParse(String value, TimeUnit desiredUnit, Long expected) {
+        assertEquals(expected, DurationFormat.getTimeDuration(value, desiredUnit));
+    }
+
+    private static Stream<Arguments> getParse() {
+        return Stream.of(Arguments.of("3000 ms", TimeUnit.SECONDS, 3L),
+            Arguments.of("3000 s", TimeUnit.SECONDS, 3000L),
+            Arguments.of("999 millis", TimeUnit.SECONDS, 0L),
+            Arguments.of("4 days", TimeUnit.NANOSECONDS, 4L * 24L * 60L * 60L * 1000000000L),
+            Arguments.of("1 DAY", TimeUnit.HOURS, 24L),
+            Arguments.of("1 hr", TimeUnit.MINUTES, 60L),
+            Arguments.of("1 Hrs", TimeUnit.MINUTES, 60L));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"1 week", "1 wk", "1 w", "1 wks", "1 weeks"})
+    public void testGetTimeDurationShouldConvertWeeks(String week) {
+        assertEquals(7L, DurationFormat.getTimeDuration(week, TimeUnit.DAYS));
+    }
+
+
+    @ParameterizedTest
+    @ValueSource(strings = {"-1 week", "-1 wk", "-1 w", "-1 weeks", "- 1 week"})
+    public void testGetTimeDurationShouldHandleNegativeWeeks(String week) {
+        IllegalArgumentException iae =
+            assertThrows(IllegalArgumentException.class, () -> DurationFormat.getTimeDuration(week, TimeUnit.DAYS));
+        assertTrue(iae.getMessage().contains("Value '" + week + "' is not a valid time duration"));
+    }
+
+
+    @ParameterizedTest
+    @ValueSource(strings = {"1 work", "1 wek", "1 k"})
+    public void testGetTimeDurationShouldHandleInvalidAbbreviations(String week) {
+        IllegalArgumentException iae =
+            assertThrows(IllegalArgumentException.class, () -> DurationFormat.getTimeDuration(week, TimeUnit.DAYS));
+        assertTrue(iae.getMessage().contains("Value '" + week + "' is not a valid time duration"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"1week", "1wk", "1w", "1wks", "1weeks"})
+    public void testGetTimeDurationShouldHandleNoSpaceInInput(String week) {
+        assertEquals(7L, DurationFormat.getTimeDuration(week, TimeUnit.DAYS));
+    }
+
+
+    @ParameterizedTest
+    @ValueSource(strings = {"10 ms", "10 millis", "10 milliseconds"})
+    public void testGetTimeDurationWithWholeNumbers(String whole) {
+        assertEquals(10L, DurationFormat.getTimeDuration(whole, TimeUnit.MILLISECONDS));
+    }
+
+
+    @ParameterizedTest
+    @ValueSource(strings = {"0.010 s", "0.010 seconds"})
+    public void testGetTimeDurationWithDecimalNumbers(String decimal) {
+        assertEquals(10L, DurationFormat.getTimeDuration(decimal, TimeUnit.MILLISECONDS));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getOneWeekInOtherUnits")
+    public void testGetPreciseTimeDurationShouldHandleWeeks(TimeUnit timeUnit, long expected) {
+        assertEquals(expected,  DurationFormat.getPreciseTimeDuration("1 week", timeUnit));
+    }
+
+    private static Stream<Arguments> getOneWeekInOtherUnits() {
+        return Stream.of(Arguments.of(TimeUnit.DAYS, 7L),
+            Arguments.of(TimeUnit.HOURS, 7 * 24L),
+            Arguments.of(TimeUnit.MINUTES, 7 * 24 * 60L),
+            Arguments.of(TimeUnit.SECONDS, 7 * 24 * 60 * 60L),
+            Arguments.of(TimeUnit.MILLISECONDS, 7 * 24 * 60 * 60 * 1000L),
+            Arguments.of(TimeUnit.MICROSECONDS, 7 * 24 * 60 * 60 * 1000 * 1000L),
+            Arguments.of(TimeUnit.NANOSECONDS, 7 * 24 * 60 * 60 * (1000 * 1000 * 1000L))
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("getOneAndAHalfWeeksInOtherUnits")
+    public void testGetPreciseTimeDurationShouldHandleDecimalWeeks(TimeUnit timeUnit, double expected) {
+        assertEquals(expected, DurationFormat.getPreciseTimeDuration("1.5 week", timeUnit));
+    }
+
+    private static Stream<Arguments> getOneAndAHalfWeeksInOtherUnits() {
+        final double oneAndAHalf = 1.5;
+        return Stream.of(Arguments.of(TimeUnit.HOURS, 7 * 24 * oneAndAHalf),
+            Arguments.of(TimeUnit.MINUTES, 7 * 24 * 60 * oneAndAHalf),
+            Arguments.of(TimeUnit.SECONDS, 7 * 24 * 60 * 60 * oneAndAHalf),
+            Arguments.of(TimeUnit.MILLISECONDS, 7 * 24 * 60 * 60 * 1000 * oneAndAHalf),
+            Arguments.of(TimeUnit.MICROSECONDS, 7 * 24 * 60 * 60 * (1000 * 1000L) * oneAndAHalf),
+            Arguments.of(TimeUnit.NANOSECONDS, 7 * 24 * 60 * 60 * (1000 * 1000 * 1000L) * oneAndAHalf));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"10 ms", "10 millis", "10 milliseconds"})
+    public void testGetPreciseTimeDurationWithWholeNumbers(String whole) {
+        assertEquals(10.0, DurationFormat.getPreciseTimeDuration(whole, TimeUnit.MILLISECONDS));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"0.010 s", "0.010 seconds"})
+    public void testGetPreciseTimeDurationWithDecimalNumbers(String decimal) {
+        assertEquals(10.0, DurationFormat.getPreciseTimeDuration(decimal, TimeUnit.MILLISECONDS));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getTinyDecimalValues")
+    public void testGetPreciseTimeDurationShouldHandleSmallDecimalValues(String originalValue, TimeUnit desiredUnit, double expectedValue) {
+        assertEquals(expectedValue, DurationFormat.getPreciseTimeDuration(originalValue, desiredUnit));
+    }
+
+    private static Stream<Arguments> getTinyDecimalValues() {
+        return Stream.of(Arguments.of("123.4 " + TimeUnit.NANOSECONDS.name(), TimeUnit.NANOSECONDS, 123.0),
+            Arguments.of("0.9 " + TimeUnit.NANOSECONDS.name(), TimeUnit.NANOSECONDS, 1.0),
+            Arguments.of("0.9 " + TimeUnit.NANOSECONDS.name(), TimeUnit.MILLISECONDS, 0.0),
+            Arguments.of("123.4 " + TimeUnit.MILLISECONDS.name(), TimeUnit.NANOSECONDS, 123_400_000.0));
+    }
+
+    @ParameterizedTest
+    @EnumSource(TimeUnit.class)
+    public void testMakeWholeNumberTimeShouldHandleWholeNumbers(TimeUnit timeUnit) {
+        assertEquals(Arrays.asList(10L, timeUnit), DurationFormat.makeWholeNumberTime(10.0, timeUnit));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getNanoSecondsForMakeWholeNumber")
+    public void testMakeWholeNumberTimeShouldHandleNanoseconds(double original, long expected) {
+        assertEquals(Arrays.asList(expected, TimeUnit.NANOSECONDS),
+            DurationFormat.makeWholeNumberTime(original, TimeUnit.NANOSECONDS));
+    }
+
+    private static Stream<Arguments> getNanoSecondsForMakeWholeNumber() {
+        return Stream.of(Arguments.of(1100.0, 1100L),
+            Arguments.of(2.1, 2L),
+            Arguments.of(1.0, 1L),
+            Arguments.of(0.1, 1L));
+    }
+
+    @ParameterizedTest
+    @NullSource
+    public void testGetSmallerTimeUnitWithNull(TimeUnit timeUnit) {
+        IllegalArgumentException iae = assertThrows(IllegalArgumentException.class,
+            () -> DurationFormat.getSmallerTimeUnit(timeUnit));
+
+        assertEquals("Cannot determine a smaller time unit than 'null'", iae.getMessage());
+    }
+
+    @Test
+    public void testGetSmallerTimeUnitWithNanoseconds() {
+        IllegalArgumentException iae = assertThrows(IllegalArgumentException.class,
+            () -> DurationFormat.getSmallerTimeUnit(TimeUnit.NANOSECONDS));
+
+        assertEquals("Cannot determine a smaller time unit than 'NANOSECONDS'", iae.getMessage());
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSmallerUnits")
+    public void testShouldGetSmallerTimeUnit(TimeUnit original, TimeUnit expected) {
+        assertEquals(expected, DurationFormat.getSmallerTimeUnit(original));
+    }
+
+    private static Stream<Arguments> getSmallerUnits() {
+        return Stream.of(Arguments.of(TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS),
+            Arguments.of(TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS),
+            Arguments.of(TimeUnit.SECONDS, TimeUnit.MILLISECONDS),
+            Arguments.of(TimeUnit.MINUTES, TimeUnit.SECONDS),
+            Arguments.of(TimeUnit.HOURS, TimeUnit.MINUTES),
+            Arguments.of(TimeUnit.DAYS, TimeUnit.HOURS));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getMultipliers")
+    void testShouldCalculateMultiplier(TimeUnit original, TimeUnit newTimeUnit, long expected) {
+        assertEquals(expected, DurationFormat.calculateMultiplier(original, newTimeUnit));
+    }
+
+    private static Stream<Arguments> getMultipliers() {
+        return Stream.of(Arguments.of(TimeUnit.DAYS, TimeUnit.NANOSECONDS, 24 * 60 * 60 * 1_000_000_000L),
+            Arguments.of(TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS, 1_000L),
+            Arguments.of(TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS, 1_000_000L),
+            Arguments.of(TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS, 1_000L),
+            Arguments.of(TimeUnit.DAYS, TimeUnit.HOURS, 24L),
+            Arguments.of(TimeUnit.DAYS, TimeUnit.SECONDS, 24 * 60 * 60L));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getIncorrectUnits")
+    public void testCalculateMultiplierShouldHandleIncorrectUnits(TimeUnit original, TimeUnit newTimeUnit) {
+        IllegalArgumentException iae = assertThrows(IllegalArgumentException.class,
+            () -> DurationFormat.calculateMultiplier(original, newTimeUnit));
+        assertTrue(iae.getMessage().matches("The original time unit '.*' must be larger than the new time unit '.*'"));
+    }
+
+    private static Stream<Arguments> getIncorrectUnits() {
+        return Stream.of(Arguments.of(TimeUnit.NANOSECONDS, TimeUnit.DAYS),
+            Arguments.of(TimeUnit.NANOSECONDS, TimeUnit.MICROSECONDS),
+            Arguments.of(TimeUnit.HOURS, TimeUnit.DAYS));
+    }
+
+
+    @ParameterizedTest
+    @MethodSource("getDecimalsForMakeWholeNumber")
+    public void testMakeWholeNumberTimeShouldHandleDecimals(double decimal, TimeUnit timeUnit) {
+        assertEquals(Arrays.asList(10L, TimeUnit.NANOSECONDS), DurationFormat.makeWholeNumberTime(decimal, timeUnit));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getDecimalsForMetricConversions")
+    public void testMakeWholeNumberTimeShouldHandleMetricConversions(double originalValue, TimeUnit originalUnits, long expectedValue, TimeUnit expectedUnits) {
+        assertEquals(Arrays.asList(expectedValue, expectedUnits),
+            DurationFormat.makeWholeNumberTime(originalValue, originalUnits));
+    }
+
+    private static Stream<Arguments> getDecimalsForMetricConversions() {
+        return Stream.of(Arguments.of(123.4, TimeUnit.SECONDS, 123_400L, TimeUnit.MILLISECONDS),
+            Arguments.of(1.000_345, TimeUnit.SECONDS, 1_000_345L, TimeUnit.MICROSECONDS),
+            Arguments.of(0.75, TimeUnit.MILLISECONDS, 750L, TimeUnit.MICROSECONDS),
+            Arguments.of(123.4, TimeUnit.NANOSECONDS, 123L, TimeUnit.NANOSECONDS),
+            Arguments.of(0.123, TimeUnit.NANOSECONDS, 1L, TimeUnit.NANOSECONDS));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getDecimalsForNonMetricConversions")
+    public void testMakeWholeNumberTimeShouldHandleNonMetricConversions(double originalValue, TimeUnit originalUnits, long expectedValue, TimeUnit expectedUnits) {
+        assertEquals(Arrays.asList(expectedValue, expectedUnits),
+            DurationFormat.makeWholeNumberTime(originalValue, originalUnits));
+    }
+
+    private static Stream<Arguments> getDecimalsForNonMetricConversions() {
+        return Stream.of(Arguments.of(1.5, TimeUnit.DAYS, 36L, TimeUnit.HOURS),
+            Arguments.of(1.5, TimeUnit.HOURS, 90L, TimeUnit.MINUTES),
+            Arguments.of(.75, TimeUnit.HOURS, 45L, TimeUnit.MINUTES));
+    }
+
+    private static Stream<Arguments> getDecimalsForMakeWholeNumber() {
+        return Stream.of(Arguments.of(0.000_000_010, TimeUnit.SECONDS),
+            Arguments.of(0.000_010, TimeUnit.MILLISECONDS),
+            Arguments.of(0.010, TimeUnit.MICROSECONDS)
+        );
+    }
+}
diff --git a/src/test/java/org/apache/nifi/util/TestURLValidator.java b/src/test/java/org/apache/nifi/util/TestURLValidator.java
new file mode 100644
index 0000000..6bbac56
--- /dev/null
+++ b/src/test/java/org/apache/nifi/util/TestURLValidator.java
@@ -0,0 +1,41 @@
+/*
+ * 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.apache.nifi.processor.util.URLValidator;
+import org.junit.jupiter.api.Test;
+
+import java.net.MalformedURLException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TestURLValidator {
+
+    @Test
+    void testValidUri() {
+        final String url = "https://en.wikipedia.org/w/api.php?action=query&list=recentchanges&format=json&rcprop=user|comment|parsedcomment|timestamp|title|sizes|tags";
+        assertDoesNotThrow(() -> URLValidator.createURL(url));
+    }
+
+    @Test
+    void testInvalidUri() {
+        final String uri = "http://   _";
+        assertThrows(MalformedURLException.class, () -> URLValidator.createURL(uri));
+    }
+}