You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:25 UTC

[26/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
new file mode 100644
index 0000000..98ad3ce
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+    
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
new file mode 100644
index 0000000..ce1901f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
@@ -0,0 +1,5 @@
+{
+  "header": {
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
new file mode 100644
index 0000000..33d4da3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
@@ -0,0 +1,6 @@
+{
+  "header": {
+    "dataModelVersion": "One"
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot
new file mode 100644
index 0000000..7c1ab49
Binary files /dev/null and b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
new file mode 100644
index 0000000..7f4dfc5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
@@ -0,0 +1,97 @@
+{
+  "header": {
+    "dataModelVersion": 2
+  },
+  "content": {
+    "identifier": "a2c80883-171c-316d-ba25-24df2c352693",
+    "name": "Flow1",
+    "comments": "",
+    "position": {
+      "x": 1549.249149182042,
+      "y": 764.2426186568309
+    },
+    "processGroups": [],
+    "remoteProcessGroups": [],
+    "processors": [
+      {
+        "identifier": "92fe4513-21c0-34f6-a916-2874f46ae864",
+        "name": "GenerateFlowFile",
+        "comments": "",
+        "position": {
+          "x": 488.99999411591034,
+          "y": 114.00000359389122
+        },
+        "bundle": {
+          "group": "org.apache.nifi",
+          "artifact": "nifi-standard-nar",
+          "version": "1.6.0-SNAPSHOT"
+        },
+        "style": {},
+        "type": "org.apache.nifi.processors.standard.GenerateFlowFile",
+        "properties": {
+          "character-set": "UTF-8",
+          "File Size": "0B",
+          "Batch Size": "1",
+          "Unique FlowFiles": "false",
+          "Data Format": "Text"
+        },
+        "propertyDescriptors": {
+          "character-set": {
+            "name": "character-set",
+            "displayName": "Character Set",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "File Size": {
+            "name": "File Size",
+            "displayName": "File Size",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "generate-ff-custom-text": {
+            "name": "generate-ff-custom-text",
+            "displayName": "Custom Text",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Batch Size": {
+            "name": "Batch Size",
+            "displayName": "Batch Size",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Unique FlowFiles": {
+            "name": "Unique FlowFiles",
+            "displayName": "Unique FlowFiles",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Data Format": {
+            "name": "Data Format",
+            "displayName": "Data Format",
+            "identifiesControllerService": false,
+            "sensitive": false
+          }
+        },
+        "schedulingPeriod": "0 sec",
+        "schedulingStrategy": "TIMER_DRIVEN",
+        "executionNode": "ALL",
+        "penaltyDuration": "30 sec",
+        "yieldDuration": "1 sec",
+        "bulletinLevel": "WARN",
+        "runDurationMillis": 0,
+        "concurrentlySchedulableTaskCount": 1,
+        "componentType": "PROCESSOR",
+        "groupIdentifier": "a2c80883-171c-316d-ba25-24df2c352693"
+      }
+    ],
+    "inputPorts": [],
+    "outputPorts": [],
+    "connections": [],
+    "labels": [],
+    "funnels": [],
+    "controllerServices": [],
+    "variables": {},
+    "componentType": "PROCESS_GROUP"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
new file mode 100644
index 0000000..574fe56
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
@@ -0,0 +1,6 @@
+{
+  "header": {
+    "dataModelVersion": 3
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/pom.xml b/nifi-registry-core/nifi-registry-jetty/pom.xml
new file mode 100644
index 0000000..9c17c11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<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.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-jetty</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-webapp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlets</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>apache-jsp</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>apache-jstl</artifactId>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
new file mode 100644
index 0000000..c202a5b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
@@ -0,0 +1,489 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.jetty;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceCollection;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.webapp.WebAppClassLoader;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+
+public class JettyServer {
+
+    private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
+    private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml";
+    private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb
+
+    private static final FileFilter WAR_FILTER = new FileFilter() {
+        @Override
+        public boolean accept(File pathname) {
+            final String nameToTest = pathname.getName().toLowerCase();
+            return nameToTest.endsWith(".war") && pathname.isFile();
+        }
+    };
+
+    private final NiFiRegistryProperties properties;
+    private final CryptoKeyProvider masterKeyProvider;
+    private final Server server;
+
+    private WebAppContext webUiContext;
+    private WebAppContext webApiContext;
+    private WebAppContext webDocsContext;
+
+    public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider) {
+        final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads());
+        threadPool.setName("NiFi Registry Web Server");
+
+        this.properties = properties;
+        this.masterKeyProvider = cryptoKeyProvider;
+        this.server = new Server(threadPool);
+
+        // enable the annotation based configuration to ensure the jsp container is initialized properly
+        final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
+        classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
+
+        try {
+            configureConnectors();
+            loadWars();
+        } catch (final Throwable t) {
+            startUpFailure(t);
+        }
+    }
+
+    private void configureConnectors() {
+        // create the http configuration
+        final HttpConfiguration httpConfiguration = new HttpConfiguration();
+        httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE);
+        httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE);
+
+        if (properties.getPort() != null) {
+            final Integer port = properties.getPort();
+            if (port < 0 || (int) Math.pow(2, 16) <= port) {
+                throw new IllegalStateException("Invalid HTTP port: " + port);
+            }
+
+            logger.info("Configuring Jetty for HTTP on port: " + port);
+
+            // create the connector
+            final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
+
+            // set host and port
+            if (StringUtils.isNotBlank(properties.getHttpHost())) {
+                http.setHost(properties.getHttpHost());
+            }
+            http.setPort(port);
+
+            // add this connector
+            server.addConnector(http);
+        } else if (properties.getSslPort() != null) {
+            final Integer port = properties.getSslPort();
+            if (port < 0 || (int) Math.pow(2, 16) <= port) {
+                throw new IllegalStateException("Invalid HTTPs port: " + port);
+            }
+
+            if (StringUtils.isBlank(properties.getKeyStorePath())) {
+                throw new IllegalStateException(NiFiRegistryProperties.SECURITY_KEYSTORE
+                        + " must be provided to configure Jetty for HTTPs");
+            }
+
+            logger.info("Configuring Jetty for HTTPs on port: " + port);
+
+            // add some secure config
+            final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
+            httpsConfiguration.setSecureScheme("https");
+            httpsConfiguration.setSecurePort(properties.getSslPort());
+            httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
+
+            // build the connector
+            final ServerConnector https = new ServerConnector(server,
+                    new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
+                    new HttpConnectionFactory(httpsConfiguration));
+
+            // set host and port
+            if (StringUtils.isNotBlank(properties.getHttpsHost())) {
+                https.setHost(properties.getHttpsHost());
+            }
+            https.setPort(port);
+
+            // add this connector
+            server.addConnector(https);
+        }
+    }
+
+    private SslContextFactory createSslContextFactory() {
+        final SslContextFactory contextFactory = new SslContextFactory();
+
+        // if needClientAuth is false then set want to true so we can optionally use certs
+        if (properties.getNeedClientAuth()) {
+            logger.info("Setting Jetty's SSLContextFactory needClientAuth to true");
+            contextFactory.setNeedClientAuth(true);
+        } else {
+            logger.info("Setting Jetty's SSLContextFactory wantClientAuth to true");
+            contextFactory.setWantClientAuth(true);
+        }
+
+        /* below code sets JSSE system properties when values are provided */
+        // keystore properties
+        if (StringUtils.isNotBlank(properties.getKeyStorePath())) {
+            contextFactory.setKeyStorePath(properties.getKeyStorePath());
+        }
+        if (StringUtils.isNotBlank(properties.getKeyStoreType())) {
+            contextFactory.setKeyStoreType(properties.getKeyStoreType());
+        }
+        final String keystorePassword = properties.getKeyStorePassword();
+        final String keyPassword = properties.getKeyPassword();
+        if (StringUtils.isNotBlank(keystorePassword)) {
+            // if no key password was provided, then assume the keystore password is the same as the key password.
+            final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
+            contextFactory.setKeyManagerPassword(keystorePassword);
+            contextFactory.setKeyStorePassword(defaultKeyPassword);
+        } else if (StringUtils.isNotBlank(keyPassword)) {
+            // since no keystore password was provided, there will be no keystore integrity check
+            contextFactory.setKeyStorePassword(keyPassword);
+        }
+
+        // truststore properties
+        if (StringUtils.isNotBlank(properties.getTrustStorePath())) {
+            contextFactory.setTrustStorePath(properties.getTrustStorePath());
+        }
+        if (StringUtils.isNotBlank(properties.getTrustStoreType())) {
+            contextFactory.setTrustStoreType(properties.getTrustStoreType());
+        }
+        if (StringUtils.isNotBlank(properties.getTrustStorePassword())) {
+            contextFactory.setTrustStorePassword(properties.getTrustStorePassword());
+        }
+
+        return contextFactory;
+    }
+
+    private void loadWars() throws IOException {
+        final File warDirectory = properties.getWarLibDirectory();
+        final File[] wars = warDirectory.listFiles(WAR_FILTER);
+
+        if (wars == null) {
+            throw new RuntimeException("Unable to access war lib directory: " + warDirectory);
+        }
+
+        File webUiWar = null;
+        File webApiWar = null;
+        File webDocsWar = null;
+        for (final File war : wars) {
+            if (war.getName().startsWith("nifi-registry-web-ui")) {
+                webUiWar = war;
+            } else if (war.getName().startsWith("nifi-registry-web-api")) {
+                webApiWar = war;
+            } else if (war.getName().startsWith("nifi-registry-web-docs")) {
+                webDocsWar = war;
+            }
+        }
+
+        if (webUiWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web UI");
+        } else if (webApiWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web API");
+        } else if (webDocsWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web Docs");
+        }
+
+        webUiContext = loadWar(webUiWar, "/nifi-registry");
+
+        webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath());
+        logger.info("Adding {} object to ServletContext with key 'nifi-registry.properties'", properties.getClass().getSimpleName());
+        webApiContext.setAttribute("nifi-registry.properties", properties);
+        logger.info("Adding {} object to ServletContext with key 'nifi-registry.key'", masterKeyProvider.getClass().getSimpleName());
+        webApiContext.setAttribute("nifi-registry.key", masterKeyProvider);
+
+        // there is an issue scanning the asm repackaged jar so narrow down what we are scanning
+        webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$");
+
+        final String docsContextPath = "/nifi-registry-docs";
+        webDocsContext = loadWar(webDocsWar, docsContextPath);
+
+        final HandlerCollection handlers = new HandlerCollection();
+        handlers.addHandler(webUiContext);
+        handlers.addHandler(webApiContext);
+        handlers.addHandler(createDocsWebApp(docsContextPath));
+        handlers.addHandler(webDocsContext);
+        server.setHandler(handlers);
+    }
+
+    private WebAppContext loadWar(final File warFile, final String contextPath)
+            throws IOException {
+        return loadWar(warFile, contextPath, new URL[0]);
+    }
+
+    private WebAppContext loadWar(final File warFile, final String contextPath, final URL[] additionalResources)
+            throws IOException {
+        final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
+        webappContext.setContextPath(contextPath);
+        webappContext.setDisplayName(contextPath);
+
+        // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
+        List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
+        serverClasses.remove("org.slf4j.");
+        webappContext.setServerClasses(serverClasses.toArray(new String[0]));
+        webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
+
+        // get the temp directory for this webapp
+        final File webWorkingDirectory = properties.getWebWorkingDirectory();
+        final File tempDir = new File(webWorkingDirectory, warFile.getName());
+        if (tempDir.exists() && !tempDir.isDirectory()) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
+        } else if (!tempDir.exists()) {
+            final boolean made = tempDir.mkdirs();
+            if (!made) {
+                throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!(tempDir.canRead() && tempDir.canWrite())) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
+        }
+
+        // configure the temp dir
+        webappContext.setTempDirectory(tempDir);
+
+        // configure the max form size (3x the default)
+        webappContext.setMaxFormContentSize(600000);
+
+        // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then
+        // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources
+        ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
+        if (additionalResources != null && additionalResources.length > 0) {
+            URLClassLoader additionalClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader());
+            parentClassLoader = additionalClassLoader;
+        }
+
+        webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
+
+        logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
+        return webappContext;
+    }
+
+    private URL[] getWebApiAdditionalClasspath() {
+        final String dbDriverDir = properties.getDatabaseDriverDirectory();
+
+        if (StringUtils.isBlank(dbDriverDir)) {
+            logger.info("No database driver directory was specified");
+            return new URL[0];
+        }
+
+        final File dirFile = new File(dbDriverDir);
+
+        if (!dirFile.exists()) {
+            logger.warn("Skipping database driver directory that does not exist: " + dbDriverDir);
+            return new URL[0];
+        }
+
+        if (!dirFile.canRead()) {
+            logger.warn("Skipping database driver directory that can not be read: " + dbDriverDir);
+            return new URL[0];
+        }
+
+        final List<URL> resources = new LinkedList<>();
+        try {
+            resources.add(dirFile.toURI().toURL());
+        } catch (final MalformedURLException mfe) {
+            logger.warn("Unable to add {} to classpath due to {}", new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe);
+        }
+
+        if (dirFile.isDirectory()) {
+            final File[] files = dirFile.listFiles();
+            if (files != null) {
+                for (final File resource : files) {
+                    if (resource.isDirectory()) {
+                        logger.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath());
+                    } else {
+                        try {
+                            resources.add(resource.toURI().toURL());
+                        } catch (final MalformedURLException mfe) {
+                            logger.warn("Unable to add {} to classpath due to {}", new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!resources.isEmpty()) {
+            logger.info("Added additional resources to nifi-registry-api classpath: [");
+            for (URL resource : resources) {
+                logger.info(" " + resource.toString());
+            }
+            logger.info("]");
+        }
+
+        return resources.toArray(new URL[resources.size()]);
+    }
+
+    private ContextHandler createDocsWebApp(final String contextPath) throws IOException {
+        final ResourceHandler resourceHandler = new ResourceHandler();
+        resourceHandler.setDirectoriesListed(false);
+
+        // load the docs directory
+        final File docsDir = Paths.get("docs").toRealPath().toFile();
+        final Resource docsResource = Resource.newResource(docsDir);
+
+        // load the rest documentation
+        final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
+        if (!webApiDocsDir.exists()) {
+            final boolean made = webApiDocsDir.mkdirs();
+            if (!made) {
+                throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created");
+            }
+        }
+        final Resource webApiDocsResource = Resource.newResource(webApiDocsDir);
+
+        // create resources for both docs locations
+        final ResourceCollection resources = new ResourceCollection(docsResource, webApiDocsResource);
+        resourceHandler.setBaseResource(resources);
+
+        // create the context handler
+        final ContextHandler handler = new ContextHandler(contextPath);
+        handler.setHandler(resourceHandler);
+
+        logger.info("Loading documents web app with context path set to " + contextPath);
+        return handler;
+    }
+
+    public void start() {
+        try {
+            // start the server
+            server.start();
+
+            // ensure everything started successfully
+            for (Handler handler : server.getChildHandlers()) {
+                // see if the handler is a web app
+                if (handler instanceof WebAppContext) {
+                    WebAppContext context = (WebAppContext) handler;
+
+                    // see if this webapp had any exceptions that would
+                    // cause it to be unavailable
+                    if (context.getUnavailableException() != null) {
+                        startUpFailure(context.getUnavailableException());
+                    }
+                }
+            }
+
+            dumpUrls();
+        } catch (final Throwable t) {
+            startUpFailure(t);
+        }
+    }
+
+    private void startUpFailure(Throwable t) {
+        System.err.println("Failed to start web server: " + t.getMessage());
+        System.err.println("Shutting down...");
+        logger.warn("Failed to start web server... shutting down.", t);
+        System.exit(1);
+    }
+
+    private void dumpUrls() throws SocketException {
+        final List<String> urls = new ArrayList<>();
+
+        for (Connector connector : server.getConnectors()) {
+            if (connector instanceof ServerConnector) {
+                final ServerConnector serverConnector = (ServerConnector) connector;
+
+                Set<String> hosts = new HashSet<>();
+
+                // determine the hosts
+                if (StringUtils.isNotBlank(serverConnector.getHost())) {
+                    hosts.add(serverConnector.getHost());
+                } else {
+                    Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
+                    if (networkInterfaces != null) {
+                        for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
+                            for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
+                                hosts.add(inetAddress.getHostAddress());
+                            }
+                        }
+                    }
+                }
+
+                // ensure some hosts were found
+                if (!hosts.isEmpty()) {
+                    String scheme = "http";
+                    if (properties.getSslPort() != null && serverConnector.getPort() == properties.getSslPort()) {
+                        scheme = "https";
+                    }
+
+                    // dump each url
+                    for (String host : hosts) {
+                        urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
+                    }
+                }
+            }
+        }
+
+        if (urls.isEmpty()) {
+            logger.warn("NiFi Registry has started, but the UI is not available on any hosts. Please verify the host properties.");
+        } else {
+            // log the ui location
+            logger.info("NiFi Registry has started. The UI is available at the following URLs:");
+            for (final String url : urls) {
+                logger.info(String.format("%s/nifi-registry", url));
+            }
+        }
+    }
+
+    public void stop() {
+        try {
+            server.stop();
+        } catch (Exception ex) {
+            logger.warn("Failed to stop web server", ex);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
new file mode 100644
index 0000000..814dbd8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
@@ -0,0 +1,556 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<web-app 
+    xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
+    metadata-complete="false"
+    version="3.1"> 
+
+    <!-- ===================================================================== -->
+    <!-- This file contains the default descriptor for web applications.       -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
+    <!-- The intent of this descriptor is to include jetty specific or common  -->
+    <!-- configuration for all webapps.   If a context has a webdefault.xml    -->
+    <!-- descriptor, it is applied before the contexts own web.xml file        -->
+    <!--                                                                       -->
+    <!-- A context may be assigned a default descriptor by:                    -->
+    <!--  + Calling WebApplicationContext.setDefaultsDescriptor                -->
+    <!--  + Passed an arg to addWebApplications                                -->
+    <!--                                                                       -->
+    <!-- This file is used both as the resource within the jetty.jar (which is -->
+    <!-- used as the default if no explicit defaults descriptor is set) and it -->
+    <!-- is copied to the etc directory of the Jetty distro and explicitly     -->
+    <!-- by the jetty.xml file.                                                -->
+    <!--                                                                       -->
+    <!-- ===================================================================== -->
+
+    <description>
+        Default web.xml file.  
+        This file is applied to a Web application before it's own WEB_INF/web.xml file
+    </description>
+
+    <!-- ==================================================================== -->
+    <!-- Removes static references to beans from javax.el.BeanELResolver to   -->
+    <!-- ensure webapp classloader can be released on undeploy                -->
+    <!-- ==================================================================== -->
+    <listener>
+        <listener-class>org.eclipse.jetty.servlet.listener.ELContextCleaner</listener-class>
+    </listener>
+  
+    <!-- ==================================================================== -->
+    <!-- Removes static cache of Methods from java.beans.Introspector to      -->
+    <!-- ensure webapp classloader can be released on undeploy                -->
+    <!-- ==================================================================== -->  
+    <listener>
+        <listener-class>org.eclipse.jetty.servlet.listener.IntrospectorCleaner</listener-class>
+    </listener>
+  
+
+    <!-- ==================================================================== -->
+    <!-- Context params to control Session Cookies                            -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!--
+      UNCOMMENT TO ACTIVATE 
+      <context-param> 
+        <param-name>org.eclipse.jetty.servlet.SessionDomain</param-name> 
+        <param-value>127.0.0.1</param-value> 
+      </context-param> 
+      <context-param>
+        <param-name>org.eclipse.jetty.servlet.SessionPath</param-name>
+        <param-value>/</param-value>
+      </context-param>
+      <context-param>
+        <param-name>org.eclipse.jetty.servlet.MaxAge</param-name>
+        <param-value>-1</param-value>
+      </context-param>
+    -->
+
+    <!-- ==================================================================== -->
+    <!-- The default servlet.                                                 -->
+    <!-- This servlet, normally mapped to /, provides the handling for static -->
+    <!-- content, OPTIONS and TRACE methods for the context.                  -->
+    <!-- The following initParameters are supported:                          -->
+    <!--  
+    *  acceptRanges      If true, range requests and responses are
+    *                    supported
+    *
+    *  dirAllowed        If true, directory listings are returned if no
+    *                    welcome file is found. Else 403 Forbidden.
+    *
+    *  welcomeServlets   If true, attempt to dispatch to welcome files
+    *                    that are servlets, but only after no matching static
+    *                    resources could be found. If false, then a welcome
+    *                    file must exist on disk. If "exact", then exact
+    *                    servlet matches are supported without an existing file.
+    *                    Default is true.
+    *
+    *                    This must be false if you want directory listings,
+    *                    but have index.jsp in your welcome file list.
+    *
+    *  redirectWelcome   If true, welcome files are redirected rather than
+    *                    forwarded to.
+    *
+    *  gzip              If set to true, then static content will be served as
+    *                    gzip content encoded if a matching resource is
+    *                    found ending with ".gz"
+    *
+    *  resourceBase      Set to replace the context resource base
+    *
+    *  resourceCache     If set, this is a context attribute name, which the servlet
+    *                    will use to look for a shared ResourceCache instance.
+    *
+    *  relativeResourceBase
+    *                    Set with a pathname relative to the base of the
+    *                    servlet context root. Useful for only serving static content out
+    *                    of only specific subdirectories.
+    *
+    *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
+    *
+    *  stylesheet        Set with the location of an optional stylesheet that will be used
+    *                    to decorate the directory listing html.
+    *
+    *  aliases           If True, aliases of resources are allowed (eg. symbolic
+    *                    links and caps variations). May bypass security constraints.
+    *                    
+    *  etags             If True, weak etags will be generated and handled.
+    *
+    *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
+    *  maxCachedFileSize The maximum size of a file to cache
+    *  maxCachedFiles    The maximum number of files to cache
+    *
+    *  useFileMappedBuffer
+    *                    If set to true, it will use mapped file buffer to serve static content
+    *                    when using NIO connector. Setting this value to false means that
+    *                    a direct buffer will be used instead of a mapped file buffer.
+    *                    By default, this is set to true.
+    *
+    *  cacheControl      If set, all static content will have this value set as the cache-control
+    *                    header.
+    *
+    -->
+ 
+ 
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <servlet>
+        <servlet-name>default</servlet-name>
+        <servlet-class>org.eclipse.jetty.servlet.DefaultServlet</servlet-class>
+        <init-param>
+            <param-name>aliases</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>acceptRanges</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>dirAllowed</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>welcomeServlets</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>redirectWelcome</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCacheSize</param-name>
+            <param-value>256000000</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCachedFileSize</param-name>
+            <param-value>200000000</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCachedFiles</param-name>
+            <param-value>2048</param-value>
+        </init-param>
+        <init-param>
+            <param-name>gzip</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>etags</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>useFileMappedBuffer</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <!--
+        <init-param>
+          <param-name>resourceCache</param-name>
+          <param-value>resourceCache</param-value>
+        </init-param>
+        -->
+        <!--
+        <init-param>
+          <param-name>cacheControl</param-name>
+          <param-value>max-age=3600,public</param-value>
+        </init-param>
+        -->
+        <load-on-startup>0</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>default</servlet-name>
+        <url-pattern>/</url-pattern>
+    </servlet-mapping>
+
+
+    <!-- ==================================================================== -->
+    <!-- JSP Servlet                                                          -->
+    <!-- This is the jasper JSP servlet from the jakarta project              -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!-- The JSP page compiler and execution servlet, which is the mechanism  -->
+    <!-- used by Glassfish to support JSP pages.  Traditionally, this servlet -->
+    <!-- is mapped to URL patterh "*.jsp".  This servlet supports the         -->
+    <!-- following initialization parameters (default values are in square    -->
+    <!-- brackets):                                                           -->
+    <!--                                                                      -->
+    <!--   checkInterval       If development is false and reloading is true, -->
+    <!--                       background compiles are enabled. checkInterval -->
+    <!--                       is the time in seconds between checks to see   -->
+    <!--                       if a JSP page needs to be recompiled. [300]    -->
+    <!--                                                                      -->
+    <!--   compiler            Which compiler Ant should use to compile JSP   -->
+    <!--                       pages.  See the Ant documenation for more      -->
+    <!--                       information. [javac]                           -->
+    <!--                                                                      -->
+    <!--   classdebuginfo      Should the class file be compiled with         -->
+    <!--                       debugging information?  [true]                 -->
+    <!--                                                                      -->
+    <!--   classpath           What class path should I use while compiling   -->
+    <!--                       generated servlets?  [Created dynamically      -->
+    <!--                       based on the current web application]          -->
+    <!--                       Set to ? to make the container explicitly set  -->
+    <!--                       this parameter.                                -->
+    <!--                                                                      -->
+    <!--   development         Is Jasper used in development mode (will check -->
+    <!--                       for JSP modification on every access)?  [true] -->
+    <!--                                                                      -->
+    <!--   enablePooling       Determines whether tag handler pooling is      -->
+    <!--                       enabled  [true]                                -->
+    <!--                                                                      -->
+    <!--   fork                Tell Ant to fork compiles of JSP pages so that -->
+    <!--                       a separate JVM is used for JSP page compiles   -->
+    <!--                       from the one Tomcat is running in. [true]      -->
+    <!--                                                                      -->
+    <!--   ieClassId           The class-id value to be sent to Internet      -->
+    <!--                       Explorer when using <jsp:plugin> tags.         -->
+    <!--                       [clsid:8AD9C840-044E-11D1-B3E9-00805F499D93]   -->
+    <!--                                                                      -->
+    <!--   javaEncoding        Java file encoding to use for generating java  -->
+    <!--                       source files. [UTF-8]                          -->
+    <!--                                                                      -->
+    <!--   keepgenerated       Should we keep the generated Java source code  -->
+    <!--                       for each page instead of deleting it? [true]   -->
+    <!--                                                                      -->
+    <!--   logVerbosityLevel   The level of detailed messages to be produced  -->
+    <!--                       by this servlet.  Increasing levels cause the  -->
+    <!--                       generation of more messages.  Valid values are -->
+    <!--                       FATAL, ERROR, WARNING, INFORMATION, and DEBUG. -->
+    <!--                       [WARNING]                                      -->
+    <!--                                                                      -->
+    <!--   mappedfile          Should we generate static content with one     -->
+    <!--                       print statement per input line, to ease        -->
+    <!--                       debugging?  [false]                            -->
+    <!--                                                                      -->
+    <!--                                                                      -->
+    <!--   reloading           Should Jasper check for modified JSPs?  [true] -->
+    <!--                                                                      -->
+    <!--   suppressSmap        Should the generation of SMAP info for JSR45   -->
+    <!--                       debugging be suppressed?  [false]              -->
+    <!--                                                                      -->
+    <!--   dumpSmap            Should the SMAP info for JSR45 debugging be    -->
+    <!--                       dumped to a file? [false]                      -->
+    <!--                       False if suppressSmap is true                  -->
+    <!--                                                                      -->
+    <!--   scratchdir          What scratch directory should we use when      -->
+    <!--                       compiling JSP pages?  [default work directory  -->
+    <!--                       for the current web application]               -->
+    <!--                                                                      -->
+    <!--   tagpoolMaxSize      The maximum tag handler pool size  [5]         -->
+    <!--                                                                      -->
+    <!--   xpoweredBy          Determines whether X-Powered-By response       -->
+    <!--                       header is added by generated servlet  [false]  -->
+    <!--                                                                      -->
+    <!-- If you wish to use Jikes to compile JSP pages:                       -->
+    <!--   Set the init parameter "compiler" to "jikes".  Define              -->
+    <!--   the property "-Dbuild.compiler.emacs=true" when starting Jetty     -->
+    <!--   to cause Jikes to emit error messages in a format compatible with  -->
+    <!--   Jasper.                                                            -->
+    <!--   If you get an error reporting that jikes can't use UTF-8 encoding, -->
+    <!--   try setting the init parameter "javaEncoding" to "ISO-8859-1".     -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <servlet id="jsp">
+        <servlet-name>jsp</servlet-name>
+        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
+        <init-param>
+            <param-name>logVerbosityLevel</param-name>
+            <param-value>DEBUG</param-value>
+        </init-param>
+        <init-param>
+            <param-name>fork</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>keepgenerated</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>development</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>xpoweredBy</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>compilerTargetVM</param-name>
+            <param-value>1.7</param-value>
+        </init-param>
+        <init-param>
+            <param-name>compilerSourceVM</param-name>
+            <param-value>1.7</param-value>
+        </init-param>
+        <!--  
+        <init-param>
+            <param-name>classpath</param-name>
+            <param-value>?</param-value>
+        </init-param>
+        -->
+        <load-on-startup>0</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>jsp</servlet-name>
+        <url-pattern>*.jsp</url-pattern>
+        <url-pattern>*.jspf</url-pattern>
+        <url-pattern>*.jspx</url-pattern>
+        <url-pattern>*.xsp</url-pattern>
+        <url-pattern>*.JSP</url-pattern>
+        <url-pattern>*.JSPF</url-pattern>
+        <url-pattern>*.JSPX</url-pattern>
+        <url-pattern>*.XSP</url-pattern>
+    </servlet-mapping>
+
+
+    <!-- ==================================================================== -->
+    <session-config>
+        <session-timeout>30</session-timeout>
+    </session-config>
+
+    <!-- ==================================================================== -->
+    <!-- Default MIME mappings                                                -->
+    <!-- The default MIME mappings are provided by the mime.properties        -->
+    <!-- resource in the org.eclipse.jetty.server.jar file.  Additional or modified  -->
+    <!-- mappings may be specified here                                       -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!-- UNCOMMENT TO ACTIVATE
+    <mime-mapping>
+      <extension>mysuffix</extension>
+      <mime-type>mymime/type</mime-type>
+    </mime-mapping>
+    -->
+
+    <!-- ==================================================================== -->
+    <welcome-file-list>
+        <welcome-file>index.html</welcome-file>
+        <welcome-file>index.htm</welcome-file>
+        <welcome-file>index.jsp</welcome-file>
+    </welcome-file-list>
+
+    <!-- ==================================================================== -->
+    <locale-encoding-mapping-list>
+        <locale-encoding-mapping>
+            <locale>ar</locale>
+            <encoding>ISO-8859-6</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>be</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>bg</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ca</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>cs</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>da</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>de</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>el</locale>
+            <encoding>ISO-8859-7</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>en</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>es</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>et</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>fi</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>fr</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>hr</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>hu</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>is</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>it</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>iw</locale>
+            <encoding>ISO-8859-8</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ja</locale>
+            <encoding>Shift_JIS</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ko</locale>
+            <encoding>EUC-KR</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>lt</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>lv</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>mk</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>nl</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>no</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>pl</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>pt</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ro</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ru</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sh</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sk</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sl</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sq</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sr</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sv</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>tr</locale>
+            <encoding>ISO-8859-9</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>uk</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>zh</locale>
+            <encoding>GB2312</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>zh_TW</locale>
+            <encoding>Big5</encoding>
+        </locale-encoding-mapping>
+    </locale-encoding-mapping-list>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Disable TRACE</web-resource-name>
+            <url-pattern>/</url-pattern>
+            <http-method>TRACE</http-method>
+        </web-resource-collection>
+        <auth-constraint/>
+    </security-constraint>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Enable everything but TRACE</web-resource-name>
+            <url-pattern>/</url-pattern>
+            <http-method-omission>TRACE</http-method-omission>
+        </web-resource-collection>
+    </security-constraint>
+
+</web-app>
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-registry-core/nifi-registry-properties/pom.xml
new file mode 100644
index 0000000..a6d6422
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<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.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-properties</artifactId>
+    <packaging>jar</packaging>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>addTestSources</goal>
+                            <goal>testCompile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.7.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
new file mode 100644
index 0000000..b7d1d2e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+import org.bouncycastle.util.encoders.DecoderException;
+import org.bouncycastle.util.encoders.EncoderException;
+import org.bouncycastle.util.encoders.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class);
+
+    private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider";
+    private static final String IMPLEMENTATION_KEY = "aes/gcm/";
+    private static final String ALGORITHM = "AES/GCM/NoPadding";
+    private static final String PROVIDER = "BC";
+    private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text
+    private static final int IV_LENGTH = 12;
+    private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1;
+
+    private Cipher cipher;
+    private final SecretKey key;
+
+    public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
+        byte[] key = validateKey(keyHex);
+
+        try {
+            Security.addProvider(new BouncyCastleProvider());
+            cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
+            // Only store the key if the cipher was initialized successfully
+            this.key = new SecretKeySpec(key, "AES");
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
+            logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage());
+            throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e);
+        }
+    }
+
+    private byte[] validateKey(String keyHex) {
+        if (keyHex == null || StringUtils.isBlank(keyHex)) {
+            throw new SensitivePropertyProtectionException("The key cannot be empty");
+        }
+        keyHex = formatHexKey(keyHex);
+        if (!isHexKeyValid(keyHex)) {
+            throw new SensitivePropertyProtectionException("The key must be a valid hexadecimal key");
+        }
+        byte[] key = Hex.decode(keyHex);
+        final List<Integer> validKeyLengths = getValidKeyLengths();
+        if (!validKeyLengths.contains(key.length * 8)) {
+            List<String> validKeyLengthsAsStrings = validKeyLengths.stream().map(i -> Integer.toString(i)).collect(Collectors.toList());
+            throw new SensitivePropertyProtectionException("The key (" + key.length * 8 + " bits) must be a valid length: " + StringUtils.join(validKeyLengthsAsStrings, ", "));
+        }
+        return key;
+    }
+
+    public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
+        this(key == null ? "" : Hex.toHexString(key));
+    }
+
+    private static String formatHexKey(String input) {
+        if (input == null || StringUtils.isBlank(input)) {
+            return "";
+        }
+        return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase();
+    }
+
+    private static boolean isHexKeyValid(String key) {
+        if (key == null || StringUtils.isBlank(key)) {
+            return false;
+        }
+        // Key length is in "nibbles" (i.e. one hex char = 4 bits)
+        return getValidKeyLengths().contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$");
+    }
+
+    private static List<Integer> getValidKeyLengths() {
+        List<Integer> validLengths = new ArrayList<>();
+        validLengths.add(128);
+
+        try {
+            if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
+                validLengths.add(192);
+                validLengths.add(256);
+            } else {
+                logger.warn("JCE Unlimited Strength Cryptography Jurisdiction policies are not available, so the max key length is 128 bits");
+            }
+        } catch (NoSuchAlgorithmException e) {
+            logger.warn("Encountered an error determining the max key length", e);
+        }
+
+        return validLengths;
+    }
+
+    /**
+     * Returns the name of the underlying implementation.
+     *
+     * @return the name of this sensitive property provider
+     */
+    @Override
+    public String getName() {
+        return IMPLEMENTATION_NAME;
+    }
+
+    /**
+     * Returns the key used to identify the provider implementation in {@code nifi.properties}.
+     *
+     * @return the key to persist in the sibling property
+     */
+    @Override
+    public String getIdentifierKey() {
+        return IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded()));
+    }
+
+    private int getKeySize(String key) {
+        if (StringUtils.isBlank(key)) {
+            return 0;
+        } else {
+            // A key in hexadecimal format has one char per nibble (4 bits)
+            return formatHexKey(key).length() * 4;
+        }
+    }
+
+    /**
+     * Returns the encrypted cipher text.
+     *
+     * @param unprotectedValue the sensitive value
+     * @return the value to persist in the {@code nifi.properties} file
+     * @throws SensitivePropertyProtectionException if there is an exception encrypting the value
+     */
+    @Override
+    public String protect(String unprotectedValue) throws SensitivePropertyProtectionException {
+        if (unprotectedValue == null || unprotectedValue.trim().length() == 0) {
+            throw new IllegalArgumentException("Cannot encrypt an empty value");
+        }
+
+        // Generate IV
+        byte[] iv = generateIV();
+        if (iv.length < IV_LENGTH) {
+            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
+        }
+
+        try {
+            // Initialize cipher for encryption
+            cipher.init(Cipher.ENCRYPT_MODE, this.key, new IvParameterSpec(iv));
+
+            byte[] plainBytes = unprotectedValue.getBytes(StandardCharsets.UTF_8);
+            byte[] cipherBytes = cipher.doFinal(plainBytes);
+            logger.info(getName() + " encrypted a sensitive value successfully");
+            return base64Encode(iv) + DELIMITER + base64Encode(cipherBytes);
+            // return Base64.toBase64String(iv) + DELIMITER + Base64.toBase64String(cipherBytes);
+        } catch (BadPaddingException | IllegalBlockSizeException | EncoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
+            final String msg = "Error encrypting a protected value";
+            logger.error(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    private String base64Encode(byte[] input) {
+        return Base64.toBase64String(input).replaceAll("=", "");
+    }
+
+    /**
+     * Generates a new random IV of 12 bytes using {@link SecureRandom}.
+     *
+     * @return the IV
+     */
+    private byte[] generateIV() {
+        byte[] iv = new byte[IV_LENGTH];
+        new SecureRandom().nextBytes(iv);
+        return iv;
+    }
+
+    /**
+     * Returns the decrypted plaintext.
+     *
+     * @param protectedValue the cipher text read from the {@code nifi.properties} file
+     * @return the raw value to be used by the application
+     * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text
+     */
+    @Override
+    public String unprotect(String protectedValue) throws SensitivePropertyProtectionException {
+        if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) {
+            throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars");
+        }
+
+        if (!protectedValue.contains(DELIMITER)) {
+            throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)");
+        }
+
+        protectedValue = protectedValue.trim();
+
+        final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER));
+        byte[] iv = Base64.decode(IV_B64);
+        if (iv.length < IV_LENGTH) {
+            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
+        }
+
+        String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2);
+
+        // Restore the = padding if necessary to reconstitute the GCM MAC check
+        if (CIPHERTEXT_B64.length() % 4 != 0) {
+            final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4);
+            CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '=');
+        }
+
+        try {
+            byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64);
+
+            cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv));
+            byte[] plainBytes = cipher.doFinal(cipherBytes);
+            logger.debug(getName() + " decrypted a sensitive value successfully");
+            return new String(plainBytes, StandardCharsets.UTF_8);
+        } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
+            final String msg = "Error decrypting a protected value";
+            logger.error(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    public static int getIvLength() {
+        return IV_LENGTH;
+    }
+
+    public static int getMinCipherTextLength() {
+        return MIN_CIPHER_TEXT_LENGTH;
+    }
+
+    public static String getDelimiter() {
+        return DELIMITER;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..5c24a73
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.NoSuchPaddingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class);
+
+    private String keyHex;
+
+    public AESSensitivePropertyProviderFactory(String keyHex) {
+        this.keyHex = keyHex;
+    }
+
+    public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException {
+        try {
+            if (keyHex != null && !StringUtils.isBlank(keyHex)) {
+                return new AESSensitivePropertyProvider(keyHex);
+            } else {
+                throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key");
+            }
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
+            String msg = "Error creating AES Sensitive Property Provider";
+            logger.warn(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
new file mode 100644
index 0000000..df4047f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MultipleSensitivePropertyProtectionException extends SensitivePropertyProtectionException {
+
+    private Set<String> failedKeys;
+
+    /**
+     * Constructs a new throwable with {@code null} as its detail message.
+     * The cause is not initialized, and may subsequently be initialized by a
+     * call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     */
+    public MultipleSensitivePropertyProtectionException() {
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public MultipleSensitivePropertyProtectionException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * {@code cause} is <i>not</i> automatically incorporated in
+     * this throwable's detail message.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A {@code null} value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     * @since 1.4
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new throwable with the specified cause and a detail
+     * message of {@code (cause==null ? null : cause.toString())} (which
+     * typically contains the class and detail message of {@code cause}).
+     * This constructor is useful for throwables that are little more than
+     * wrappers for other throwables (for example, PrivilegedActionException).
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param cause the cause (which is saved for later retrieval by the
+     *              {@link #getCause()} method).  (A {@code null} value is
+     *              permitted, and indicates that the cause is nonexistent or
+     *              unknown.)
+     * @since 1.4
+     */
+    public MultipleSensitivePropertyProtectionException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
+     *
+     * @param message    the message
+     * @param failedKeys any failed keys
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys) {
+        this(message, failedKeys, null);
+    }
+
+    /**
+     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
+     *
+     * @param message    the message
+     * @param failedKeys any failed keys
+     * @param cause      the cause (which is saved for later retrieval by the
+     *                   {@link #getCause()} method).  (A {@code null} value is
+     *                   permitted, and indicates that the cause is nonexistent or
+     *                   unknown.)
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys, Throwable cause) {
+        super(message, cause);
+        this.failedKeys = new HashSet<>(failedKeys);
+    }
+
+    public Set<String> getFailedKeys() {
+        return this.failedKeys;
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProtectionException for [" + StringUtils.join(this.failedKeys, ", ") + "]: " + getLocalizedMessage();
+    }
+}