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();
+ }
+}