2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * Modifications to the original nifi code for the ONAP project are made
18 * available under the Apache License, Version 2.0
20 package org.apache.nifi.web.server;
22 import com.google.common.base.Strings;
23 import com.google.common.collect.Lists;
24 import java.io.BufferedReader;
26 import java.io.FileFilter;
27 import java.io.IOException;
28 import java.io.InputStreamReader;
29 import java.net.InetAddress;
30 import java.net.NetworkInterface;
31 import java.net.SocketException;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.EnumSet;
39 import java.util.Enumeration;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.List;
44 import java.util.Objects;
46 import java.util.concurrent.TimeUnit;
47 import java.util.jar.JarEntry;
48 import java.util.jar.JarFile;
49 import java.util.stream.Collectors;
50 import javax.servlet.DispatcherType;
51 import javax.servlet.Filter;
52 import javax.servlet.ServletContext;
53 import org.apache.commons.collections4.CollectionUtils;
54 import org.apache.commons.lang3.StringUtils;
55 import org.apache.nifi.NiFiServer;
56 import org.apache.nifi.bundle.Bundle;
57 import org.apache.nifi.bundle.BundleDetails;
58 import org.apache.nifi.controller.UninheritableFlowException;
59 import org.apache.nifi.controller.serialization.FlowSerializationException;
60 import org.apache.nifi.controller.serialization.FlowSynchronizationException;
61 import org.apache.nifi.documentation.DocGenerator;
62 import org.apache.nifi.lifecycle.LifeCycleStartException;
63 import org.apache.nifi.nar.ExtensionDiscoveringManager;
64 import org.apache.nifi.nar.ExtensionManagerHolder;
65 import org.apache.nifi.nar.ExtensionMapping;
66 import org.apache.nifi.nar.ExtensionUiLoader;
67 import org.apache.nifi.nar.NarAutoLoader;
68 import org.apache.nifi.nar.DCAEAutoLoader;
69 import org.apache.nifi.nar.NarClassLoadersHolder;
70 import org.apache.nifi.nar.NarLoader;
71 import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
72 import org.apache.nifi.nar.StandardNarLoader;
73 import org.apache.nifi.processor.DataUnit;
74 import org.apache.nifi.security.util.KeyStoreUtils;
75 import org.apache.nifi.services.FlowService;
76 import org.apache.nifi.ui.extension.UiExtension;
77 import org.apache.nifi.ui.extension.UiExtensionMapping;
78 import org.apache.nifi.util.FormatUtils;
79 import org.apache.nifi.util.NiFiProperties;
80 import org.apache.nifi.web.ContentAccess;
81 import org.apache.nifi.web.NiFiWebConfigurationContext;
82 import org.apache.nifi.web.UiExtensionType;
83 import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
84 import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
85 import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
86 import org.apache.nifi.web.security.headers.XSSProtectionFilter;
87 import org.eclipse.jetty.annotations.AnnotationConfiguration;
88 import org.eclipse.jetty.deploy.App;
89 import org.eclipse.jetty.deploy.DeploymentManager;
90 import org.eclipse.jetty.server.Connector;
91 import org.eclipse.jetty.server.Handler;
92 import org.eclipse.jetty.server.HttpConfiguration;
93 import org.eclipse.jetty.server.HttpConnectionFactory;
94 import org.eclipse.jetty.server.SecureRequestCustomizer;
95 import org.eclipse.jetty.server.Server;
96 import org.eclipse.jetty.server.ServerConnector;
97 import org.eclipse.jetty.server.SslConnectionFactory;
98 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
99 import org.eclipse.jetty.server.handler.HandlerCollection;
100 import org.eclipse.jetty.server.handler.HandlerList;
101 import org.eclipse.jetty.server.handler.gzip.GzipHandler;
102 import org.eclipse.jetty.servlet.DefaultServlet;
103 import org.eclipse.jetty.servlet.FilterHolder;
104 import org.eclipse.jetty.servlet.ServletHolder;
105 import org.eclipse.jetty.util.ssl.SslContextFactory;
106 import org.eclipse.jetty.util.thread.QueuedThreadPool;
107 import org.eclipse.jetty.webapp.Configuration;
108 import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
109 import org.eclipse.jetty.webapp.WebAppClassLoader;
110 import org.eclipse.jetty.webapp.WebAppContext;
111 import org.slf4j.Logger;
112 import org.slf4j.LoggerFactory;
113 import org.springframework.beans.BeansException;
114 import org.springframework.context.ApplicationContext;
115 import org.springframework.web.context.WebApplicationContext;
116 import org.springframework.web.context.support.WebApplicationContextUtils;
119 * Encapsulates the Jetty instance.
121 public class JettyServer implements NiFiServer, ExtensionUiLoader {
123 private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
124 private static final String WEB_DEFAULTS_XML = "org/apache/nifi/web/webdefault.xml";
126 private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
127 private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
129 private static final FileFilter WAR_FILTER = new FileFilter() {
131 public boolean accept(File pathname) {
132 final String nameToTest = pathname.getName().toLowerCase();
133 return nameToTest.endsWith(".war") && pathname.isFile();
137 private final Server server;
138 private final NiFiProperties props;
140 private Bundle systemBundle;
141 private Set<Bundle> bundles;
142 private ExtensionMapping extensionMapping;
143 private NarAutoLoader narAutoLoader;
144 private DCAEAutoLoader dcaeAutoLoader;
146 private WebAppContext webApiContext;
147 private WebAppContext webDocsContext;
149 // content viewer and mime type specific extensions
150 private WebAppContext webContentViewerContext;
151 private Collection<WebAppContext> contentViewerWebContexts;
153 // component (processor, controller service, reporting task) ui extensions
154 private UiExtensionMapping componentUiExtensions;
155 private Collection<WebAppContext> componentUiExtensionWebContexts;
157 private DeploymentManager deploymentManager;
159 public JettyServer(final NiFiProperties props, final Set<Bundle> bundles) {
160 final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads());
161 threadPool.setName("NiFi Web Server");
164 this.server = new Server(threadPool);
167 // enable the annotation based configuration to ensure the jsp container is initialized properly
168 final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
169 classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
172 configureConnectors(server);
174 // load wars from the bundle
175 final Handler warHandlers = loadInitialWars(bundles);
177 final HandlerList allHandlers = new HandlerList();
179 // Only restrict the host header if running in HTTPS mode
180 if (props.isHTTPSConfigured()) {
181 // Create a handler for the host header and add it to the server
182 HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
183 logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]");
185 // Add this before the WAR handlers
186 allHandlers.addHandler(hostHeaderHandler);
188 logger.info("Running in HTTP mode; host headers not restricted");
192 final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
193 contextHandlers.addHandler(warHandlers);
194 allHandlers.addHandler(contextHandlers);
195 server.setHandler(allHandlers);
197 deploymentManager = new DeploymentManager();
198 deploymentManager.setContextAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
199 deploymentManager.setContexts(contextHandlers);
200 server.addBean(deploymentManager);
204 * Instantiates this object but does not perform any configuration. Used for unit testing.
206 JettyServer(Server server, NiFiProperties properties) {
207 this.server = server;
208 this.props = properties;
211 private Handler loadInitialWars(final Set<Bundle> bundles) {
214 final Map<File, Bundle> warToBundleLookup = findWars(bundles);
216 // locate each war being deployed
217 File webUiWar = null;
218 File webApiWar = null;
219 File webErrorWar = null;
220 File webDocsWar = null;
221 File webContentViewerWar = null;
222 Map<File, Bundle> otherWars = new HashMap<>();
223 for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
224 final File war = warBundleEntry.getKey();
225 final Bundle warBundle = warBundleEntry.getValue();
227 if (war.getName().toLowerCase().startsWith("nifi-web-api")) {
229 } else if (war.getName().toLowerCase().startsWith("nifi-web-error")) {
231 } else if (war.getName().toLowerCase().startsWith("nifi-web-docs")) {
233 } else if (war.getName().toLowerCase().startsWith("nifi-web-content-viewer")) {
234 webContentViewerWar = war;
235 } else if (war.getName().toLowerCase().startsWith("nifi-web")) {
238 otherWars.put(war, warBundle);
242 // ensure the required wars were found
243 if (webUiWar == null) {
244 throw new RuntimeException("Unable to load nifi-web WAR");
245 } else if (webApiWar == null) {
246 throw new RuntimeException("Unable to load nifi-web-api WAR");
247 } else if (webDocsWar == null) {
248 throw new RuntimeException("Unable to load nifi-web-docs WAR");
249 } else if (webErrorWar == null) {
250 throw new RuntimeException("Unable to load nifi-web-error WAR");
251 } else if (webContentViewerWar == null) {
252 throw new RuntimeException("Unable to load nifi-web-content-viewer WAR");
255 // handlers for each war and init params for the web api
256 final ExtensionUiInfo extensionUiInfo = loadWars(otherWars);
257 componentUiExtensionWebContexts = new ArrayList<>(extensionUiInfo.getComponentUiExtensionWebContexts());
258 contentViewerWebContexts = new ArrayList<>(extensionUiInfo.getContentViewerWebContexts());
259 componentUiExtensions = new UiExtensionMapping(extensionUiInfo.getComponentUiExtensionsByType());
261 final HandlerCollection webAppContextHandlers = new HandlerCollection();
262 final Collection<WebAppContext> extensionUiContexts = extensionUiInfo.getWebAppContexts();
263 extensionUiContexts.stream().forEach(c -> webAppContextHandlers.addHandler(c));
265 final ClassLoader frameworkClassLoader = getClass().getClassLoader();
267 // load the web ui app
268 final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", frameworkClassLoader);
269 webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled()));
270 webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
271 webUiContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
272 webAppContextHandlers.addHandler(webUiContext);
274 // load the web api app
275 webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader);
276 webAppContextHandlers.addHandler(webApiContext);
278 // load the content viewer app
279 webContentViewerContext = loadWar(webContentViewerWar, "/nifi-content-viewer", frameworkClassLoader);
280 webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings());
281 webAppContextHandlers.addHandler(webContentViewerContext);
283 // create a web app for the docs
284 final String docsContextPath = "/nifi-docs";
286 // load the documentation war
287 webDocsContext = loadWar(webDocsWar, docsContextPath, frameworkClassLoader);
289 // add the servlets which serve the HTML documentation within the documentation web app
290 addDocsServlets(webDocsContext);
292 webAppContextHandlers.addHandler(webDocsContext);
294 // load the web error app
295 final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader);
296 webErrorContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
297 webAppContextHandlers.addHandler(webErrorContext);
299 // deploy the web apps
300 return gzip(webAppContextHandlers);
304 public void loadExtensionUis(final Set<Bundle> bundles) {
305 // Find and load any WARs contained within the set of bundles...
306 final Map<File, Bundle> warToBundleLookup = findWars(bundles);
307 final ExtensionUiInfo extensionUiInfo = loadWars(warToBundleLookup);
309 final Collection<WebAppContext> webAppContexts = extensionUiInfo.getWebAppContexts();
310 if (CollectionUtils.isEmpty(webAppContexts)) {
311 logger.debug("No webapp contexts were loaded, returning...");
315 // Deploy each WAR that was loaded...
316 for (final WebAppContext webAppContext : webAppContexts) {
317 final App extensionUiApp = new App(deploymentManager, null, "nifi-jetty-server", webAppContext);
318 deploymentManager.addApp(extensionUiApp);
321 final Collection<WebAppContext> componentUiExtensionWebContexts = extensionUiInfo.getComponentUiExtensionWebContexts();
322 final Collection<WebAppContext> contentViewerWebContexts = extensionUiInfo.getContentViewerWebContexts();
324 // Inject the configuration context and security filter into contexts that need it
325 final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
326 final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
327 final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
328 final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
330 performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
331 performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
333 // Merge results of current loading into previously loaded results...
334 this.componentUiExtensionWebContexts.addAll(componentUiExtensionWebContexts);
335 this.contentViewerWebContexts.addAll(contentViewerWebContexts);
336 this.componentUiExtensions.addUiExtensions(extensionUiInfo.getComponentUiExtensionsByType());
338 for (final WebAppContext webAppContext : webAppContexts) {
339 final Throwable t = webAppContext.getUnavailableException();
341 logger.error("Unable to start context due to " + t.getMessage(), t);
346 private ExtensionUiInfo loadWars(final Map<File, Bundle> warToBundleLookup) {
347 // handlers for each war and init params for the web api
348 final List<WebAppContext> webAppContexts = new ArrayList<>();
349 final Map<String, String> mimeMappings = new HashMap<>();
350 final Collection<WebAppContext> componentUiExtensionWebContexts = new ArrayList<>();
351 final Collection<WebAppContext> contentViewerWebContexts = new ArrayList<>();
352 final Map<String, List<UiExtension>> componentUiExtensionsByType = new HashMap<>();
354 final ClassLoader frameworkClassLoader = getClass().getClassLoader();
355 final ClassLoader jettyClassLoader = frameworkClassLoader.getParent();
357 // deploy the other wars
358 if (!warToBundleLookup.isEmpty()) {
359 // ui extension organized by component type
360 for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
361 final File war = warBundleEntry.getKey();
362 final Bundle warBundle = warBundleEntry.getValue();
364 // identify all known extension types in the war
365 final Map<UiExtensionType, List<String>> uiExtensionInWar = new HashMap<>();
366 identifyUiExtensionsForComponents(uiExtensionInWar, war);
368 // only include wars that are for custom processor ui's
369 if (!uiExtensionInWar.isEmpty()) {
370 // get the context path
371 String warName = StringUtils.substringBeforeLast(war.getName(), ".");
372 String warContextPath = String.format("/%s", warName);
374 // get the classloader for this war
375 ClassLoader narClassLoaderForWar = warBundle.getClassLoader();
377 // this should never be null
378 if (narClassLoaderForWar == null) {
379 narClassLoaderForWar = jettyClassLoader;
382 // create the extension web app context
383 WebAppContext extensionUiContext = loadWar(war, warContextPath, narClassLoaderForWar);
385 // create the ui extensions
386 for (final Map.Entry<UiExtensionType, List<String>> entry : uiExtensionInWar.entrySet()) {
387 final UiExtensionType extensionType = entry.getKey();
388 final List<String> types = entry.getValue();
390 if (UiExtensionType.ContentViewer.equals(extensionType)) {
391 // consider each content type identified
392 for (final String contentType : types) {
393 // map the content type to the context path
394 mimeMappings.put(contentType, warContextPath);
397 // this ui extension provides a content viewer
398 contentViewerWebContexts.add(extensionUiContext);
400 // consider each component type identified
401 for (final String componentTypeCoordinates : types) {
402 logger.info(String.format("Loading UI extension [%s, %s] for %s", extensionType, warContextPath, componentTypeCoordinates));
404 // record the extension definition
405 final UiExtension uiExtension = new UiExtension(extensionType, warContextPath);
407 // create if this is the first extension for this component type
408 List<UiExtension> componentUiExtensionsForType = componentUiExtensionsByType.get(componentTypeCoordinates);
409 if (componentUiExtensionsForType == null) {
410 componentUiExtensionsForType = new ArrayList<>();
411 componentUiExtensionsByType.put(componentTypeCoordinates, componentUiExtensionsForType);
414 // see if there is already a ui extension of this same time
415 if (containsUiExtensionType(componentUiExtensionsForType, extensionType)) {
416 throw new IllegalStateException(String.format("Encountered duplicate UI for %s", componentTypeCoordinates));
419 // record this extension
420 componentUiExtensionsForType.add(uiExtension);
423 // this ui extension provides a component custom ui
424 componentUiExtensionWebContexts.add(extensionUiContext);
428 // include custom ui web context in the handlers
429 webAppContexts.add(extensionUiContext);
434 return new ExtensionUiInfo(webAppContexts, mimeMappings, componentUiExtensionWebContexts, contentViewerWebContexts, componentUiExtensionsByType);
438 * Returns whether or not the specified ui extensions already contains an extension of the specified type.
440 * @param componentUiExtensionsForType ui extensions for the type
441 * @param extensionType type of ui extension
442 * @return whether or not the specified ui extensions already contains an extension of the specified type
444 private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) {
445 for (final UiExtension uiExtension : componentUiExtensionsForType) {
446 if (extensionType.equals(uiExtension.getExtensionType())) {
455 * Enables compression for the specified handler.
457 * @param handler handler to enable compression for
458 * @return compression enabled handler
460 private Handler gzip(final Handler handler) {
461 final GzipHandler gzip = new GzipHandler();
462 gzip.setIncludedMethods("GET", "POST", "PUT", "DELETE");
463 gzip.setHandler(handler);
467 private Map<File, Bundle> findWars(final Set<Bundle> bundles) {
468 final Map<File, Bundle> wars = new HashMap<>();
470 // consider each nar working directory
471 bundles.forEach(bundle -> {
472 final BundleDetails details = bundle.getBundleDetails();
473 final File narDependencies = new File(details.getWorkingDirectory(), "NAR-INF/bundled-dependencies");
474 if (narDependencies.isDirectory()) {
475 // list the wars from this nar
476 final File[] narDependencyDirs = narDependencies.listFiles(WAR_FILTER);
477 if (narDependencyDirs == null) {
478 throw new IllegalStateException(String.format("Unable to access working directory for NAR dependencies in: %s", narDependencies.getAbsolutePath()));
482 for (final File war : narDependencyDirs) {
483 wars.put(war, bundle);
491 private void readUiExtensions(final Map<UiExtensionType, List<String>> uiExtensions, final UiExtensionType uiExtensionType, final JarFile jarFile, final JarEntry jarEntry) throws IOException {
492 if (jarEntry == null) {
496 // get an input stream for the nifi-processor configuration file
497 try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry)))) {
499 // read in each configured type
500 String rawComponentType;
501 while ((rawComponentType = in.readLine()) != null) {
502 // extract the component type
503 final String componentType = extractComponentType(rawComponentType);
504 if (componentType != null) {
505 List<String> extensions = uiExtensions.get(uiExtensionType);
507 // if there are currently no extensions for this type create it
508 if (extensions == null) {
509 extensions = new ArrayList<>();
510 uiExtensions.put(uiExtensionType, extensions);
513 // add the specified type
514 extensions.add(componentType);
521 * Identifies all known UI extensions and stores them in the specified map.
523 * @param uiExtensions extensions
526 private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) {
527 try (final JarFile jarFile = new JarFile(warFile)) {
528 // locate the ui extensions
529 readUiExtensions(uiExtensions, UiExtensionType.ContentViewer, jarFile, jarFile.getJarEntry("META-INF/nifi-content-viewer"));
530 readUiExtensions(uiExtensions, UiExtensionType.ProcessorConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-processor-configuration"));
531 readUiExtensions(uiExtensions, UiExtensionType.ControllerServiceConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-controller-service-configuration"));
532 readUiExtensions(uiExtensions, UiExtensionType.ReportingTaskConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-reporting-task-configuration"));
533 } catch (IOException ioe) {
534 logger.warn(String.format("Unable to inspect %s for a UI extensions.", warFile));
539 * Extracts the component type. Trims the line and considers comments.
540 * Returns null if no type was found.
545 private String extractComponentType(final String line) {
546 final String trimmedLine = line.trim();
547 if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
548 final int indexOfPound = trimmedLine.indexOf("#");
549 return (indexOfPound > 0) ? trimmedLine.substring(0, indexOfPound) : trimmedLine;
554 private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) {
555 final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
556 webappContext.setContextPath(contextPath);
557 webappContext.setDisplayName(contextPath);
559 // instruction jetty to examine these jars for tlds, web-fragments, etc
560 webappContext.setAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
562 // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
563 List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
564 serverClasses.remove("org.slf4j.");
565 webappContext.setServerClasses(serverClasses.toArray(new String[0]));
566 webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
568 // get the temp directory for this webapp
569 File tempDir = new File(props.getWebWorkingDirectory(), warFile.getName());
570 if (tempDir.exists() && !tempDir.isDirectory()) {
571 throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
572 } else if (!tempDir.exists()) {
573 final boolean made = tempDir.mkdirs();
575 throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
578 if (!(tempDir.canRead() && tempDir.canWrite())) {
579 throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
582 // configure the temp dir
583 webappContext.setTempDirectory(tempDir);
585 // configure the max form size (3x the default)
586 webappContext.setMaxFormContentSize(600000);
588 // add HTTP security headers to all responses
589 final String ALL_PATHS = "/*";
590 ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class));
591 if(props.isHTTPSConfigured()) {
592 filters.add(StrictTransportSecurityFilter.class);
594 filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext));
597 // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
598 webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
599 } catch (final IOException ioe) {
603 logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
604 return webappContext;
607 private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) {
608 FilterHolder holder = new FilterHolder(clazz);
609 holder.setName(clazz.getSimpleName());
610 webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
613 private void addDocsServlets(WebAppContext docsContext) {
615 // Load the nifi/docs directory
616 final File docsDir = getDocsDir("docs");
618 // load the component documentation working directory
619 final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory();
620 final File workingDocsDirectory = getWorkingDocsDirectory(componentDocsDirPath);
623 final File webApiDocsDir = getWebApiDocsDir();
625 // Create the servlet which will serve the static resources
626 ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
627 defaultHolder.setInitParameter("dirAllowed", "false");
629 ServletHolder docs = new ServletHolder("docs", DefaultServlet.class);
630 docs.setInitParameter("resourceBase", docsDir.getPath());
632 ServletHolder components = new ServletHolder("components", DefaultServlet.class);
633 components.setInitParameter("resourceBase", workingDocsDirectory.getPath());
635 ServletHolder restApi = new ServletHolder("rest-api", DefaultServlet.class);
636 restApi.setInitParameter("resourceBase", webApiDocsDir.getPath());
638 docsContext.addServlet(docs, "/html/*");
639 docsContext.addServlet(components, "/components/*");
640 docsContext.addServlet(restApi, "/rest-api/*");
642 docsContext.addServlet(defaultHolder, "/");
644 logger.info("Loading documents web app with context path set to " + docsContext.getContextPath());
646 } catch (Exception ex) {
647 logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage());
654 * Returns a File object for the directory containing NIFI documentation.
656 * Formerly, if the docsDirectory did not exist NIFI would fail to start
657 * with an IllegalStateException and a rather unhelpful log message.
658 * NIFI-2184 updates the process such that if the docsDirectory does not
659 * exist an attempt will be made to create the directory. If that is
660 * successful NIFI will no longer fail and will start successfully barring
661 * any other errors. The side effect of the docsDirectory not being present
662 * is that the documentation links under the 'General' portion of the help
663 * page will not be accessible, but at least the process will be running.
665 * @param docsDirectory Name of documentation directory in installation directory.
666 * @return A File object to the documentation directory; else startUpFailure called.
668 private File getDocsDir(final String docsDirectory) {
671 docsDir = Paths.get(docsDirectory).toRealPath().toFile();
672 } catch (IOException ex) {
673 logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
674 docsDir = new File(docsDirectory).getAbsoluteFile();
675 final boolean made = docsDir.mkdirs();
677 logger.error("Failed to create 'docs' directory!");
678 startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
684 private File getWorkingDocsDirectory(final File componentDocsDirPath) {
685 File workingDocsDirectory = null;
687 workingDocsDirectory = componentDocsDirPath.toPath().toRealPath().getParent().toFile();
688 } catch (IOException ex) {
689 logger.error("Failed to load :" + componentDocsDirPath.getAbsolutePath());
692 return workingDocsDirectory;
695 private File getWebApiDocsDir() {
696 // load the rest documentation
697 final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
698 if (!webApiDocsDir.exists()) {
699 final boolean made = webApiDocsDir.mkdirs();
701 logger.error("Failed to create " + webApiDocsDir.getAbsolutePath());
702 startUpFailure(new IOException(webApiDocsDir.getAbsolutePath() + " could not be created"));
705 return webApiDocsDir;
708 private void configureConnectors(final Server server) throws ServerConfigurationException {
709 // create the http configuration
710 final HttpConfiguration httpConfiguration = new HttpConfiguration();
711 final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue();
712 httpConfiguration.setRequestHeaderSize(headerSize);
713 httpConfiguration.setResponseHeaderSize(headerSize);
715 // Check if both HTTP and HTTPS connectors are configured and fail if both are configured
716 if (bothHttpAndHttpsConnectorsConfigured(props)) {
717 logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " +
718 "Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty");
719 startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time"));
722 if (props.getSslPort() != null) {
723 configureHttpsConnector(server, httpConfiguration);
724 } else if (props.getPort() != null) {
725 configureHttpConnector(server, httpConfiguration);
727 logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties");
728 startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector"));
733 * Configures an HTTPS connector and adds it to the server.
735 * @param server the Jetty server instance
736 * @param httpConfiguration the configuration object for the HTTPS protocol settings
738 private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) {
739 String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
740 final Integer port = props.getSslPort();
741 String connectorLabel = "HTTPS";
742 final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
743 ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
745 configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
749 * Configures an HTTP connector and adds it to the server.
751 * @param server the Jetty server instance
752 * @param httpConfiguration the configuration object for the HTTP protocol settings
754 private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) {
755 String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
756 final Integer port = props.getPort();
757 String connectorLabel = "HTTP";
758 final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
759 ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c));
761 configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc);
765 * Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar.
766 * Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior.
768 * @param server the Jetty server instance
769 * @param configuration the HTTP/HTTPS configuration instance
770 * @param hostname the hostname from the nifi.properties file
771 * @param port the port to expose
772 * @param connectorLabel used for log output (e.g. "HTTP" or "HTTPS")
773 * @param networkInterfaces the map of network interfaces from nifi.properties
774 * @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector}
776 private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces,
777 ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) {
778 if (port < 0 || (int) Math.pow(2, 16) <= port) {
779 throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port);
782 logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port);
784 final List<Connector> serverConnectors = Lists.newArrayList();
786 // Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing,
787 // we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference
788 // in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake.
789 final String autoRefreshInterval = props.getAutoRefreshInterval();
790 final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS);
791 final long idleTimeout = autoRefreshMillis * 2;
793 // If the interfaces collection is empty or each element is empty
794 if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
795 final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
798 if (StringUtils.isNotBlank(hostname)) {
799 serverConnector.setHost(hostname);
801 serverConnector.setPort(port);
802 serverConnector.setIdleTimeout(idleTimeout);
803 serverConnectors.add(serverConnector);
805 // Add connectors for all IPs from network interfaces
806 serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> {
807 NetworkInterface iface = null;
809 iface = NetworkInterface.getByName(ifaceName);
810 } catch (SocketException e) {
811 logger.error("Unable to get network interface by name {}", ifaceName, e);
814 logger.warn("Unable to find network interface named {}", ifaceName);
817 }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
818 .map(inetAddress -> {
819 final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
822 serverConnector.setHost(inetAddress.getHostAddress());
823 serverConnector.setPort(port);
824 serverConnector.setIdleTimeout(idleTimeout);
826 return serverConnector;
827 }).collect(Collectors.toList())));
829 // Add all connectors
830 serverConnectors.forEach(server::addConnector);
834 * Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector).
835 * Prints a warning log message with the relevant properties.
837 * @param props the NiFiProperties
838 * @return true if both ports are present
840 static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) {
841 Integer httpPort = props.getPort();
842 String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
844 Integer httpsPort = props.getSslPort();
845 String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
847 if (httpPort != null && httpsPort != null) {
848 logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details");
849 logger.warn("HTTP connector: http://" + httpHostname + ":" + httpPort);
850 logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort);
857 private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) {
858 // add some secure config
859 final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
860 httpsConfiguration.setSecureScheme("https");
861 httpsConfiguration.setSecurePort(port);
862 httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
864 // build the connector
865 return new ServerConnector(server,
866 new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
867 new HttpConnectionFactory(httpsConfiguration));
870 private SslContextFactory createSslContextFactory() {
871 final SslContextFactory contextFactory = new SslContextFactory();
872 configureSslContextFactory(contextFactory, props);
873 return contextFactory;
876 protected static void configureSslContextFactory(SslContextFactory contextFactory, NiFiProperties props) {
877 // require client auth when not supporting login, Kerberos service, or anonymous access
878 if (props.isClientAuthRequiredForRestApi()) {
879 contextFactory.setNeedClientAuth(true);
881 contextFactory.setWantClientAuth(true);
884 /* below code sets JSSE system properties when values are provided */
885 // keystore properties
886 if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) {
887 contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE));
889 String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE);
890 if (StringUtils.isNotBlank(keyStoreType)) {
891 contextFactory.setKeyStoreType(keyStoreType);
892 String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType);
893 if (StringUtils.isNoneEmpty(keyStoreProvider)) {
894 contextFactory.setKeyStoreProvider(keyStoreProvider);
897 final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD);
898 final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD);
899 if (StringUtils.isNotBlank(keystorePassword)) {
900 // if no key password was provided, then assume the keystore password is the same as the key password.
901 final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
902 contextFactory.setKeyStorePassword(keystorePassword);
903 contextFactory.setKeyManagerPassword(defaultKeyPassword);
904 } else if (StringUtils.isNotBlank(keyPassword)) {
905 // since no keystore password was provided, there will be no keystore integrity check
906 contextFactory.setKeyManagerPassword(keyPassword);
909 // truststore properties
910 if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) {
911 contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE));
913 String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE);
914 if (StringUtils.isNotBlank(trustStoreType)) {
915 contextFactory.setTrustStoreType(trustStoreType);
916 String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType);
917 if (StringUtils.isNoneEmpty(trustStoreProvider)) {
918 contextFactory.setTrustStoreProvider(trustStoreProvider);
921 if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) {
922 contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD));
927 public void start() {
929 // Create a standard extension manager and discover extensions
930 final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
931 extensionManager.discoverExtensions(systemBundle, bundles);
932 extensionManager.logClassLoaderMapping();
934 // Set the extension manager into the holder which makes it available to the Spring context via a factory bean
935 ExtensionManagerHolder.init(extensionManager);
937 // Generate docs for extensions
938 DocGenerator.generate(props, extensionManager, extensionMapping);
943 // ensure everything started successfully
944 for (Handler handler : server.getChildHandlers()) {
945 // see if the handler is a web app
946 if (handler instanceof WebAppContext) {
947 WebAppContext context = (WebAppContext) handler;
949 // see if this webapp had any exceptions that would
950 // cause it to be unavailable
951 if (context.getUnavailableException() != null) {
952 startUpFailure(context.getUnavailableException());
957 // ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters
958 // this must be done after starting the server (and ensuring there were no start up failures)
959 if (webApiContext != null) {
960 // give the web api the component ui extensions
961 final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
962 webApiServletContext.setAttribute("nifi-ui-extensions", componentUiExtensions);
964 // get the application context
965 final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
966 final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
967 final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
969 // component ui extensions
970 performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
972 // content viewer extensions
973 performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
975 // content viewer controller
976 if (webContentViewerContext != null) {
977 final ContentAccess contentAccess = webApplicationContext.getBean("contentAccess", ContentAccess.class);
979 // add the content access
980 final ServletContext webContentViewerServletContext = webContentViewerContext.getServletHandler().getServletContext();
981 webContentViewerServletContext.setAttribute("nifi-content-access", contentAccess);
983 if (securityFilter != null) {
984 webContentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
989 // ensure the web document war was loaded and provide the extension mapping
990 if (webDocsContext != null) {
991 final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext();
992 webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping);
995 // if this nifi is a node in a cluster, start the flow service and load the flow - the
996 // flow service is loaded here for clustered nodes because the loading of the flow will
997 // initialize the connection between the node and the NCM. if the node connects (starts
998 // heartbeating, etc), the NCM may issue web requests before the application (wars) have
999 // finished loading. this results in the node being disconnected since its unable to
1000 // successfully respond to the requests. to resolve this, flow loading was moved to here
1001 // (after the wars have been successfully deployed) when this nifi instance is a node
1003 if (props.isNode()) {
1005 FlowService flowService = null;
1008 logger.info("Loading Flow...");
1010 ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext());
1011 flowService = ctx.getBean("flowService", FlowService.class);
1013 // start and load the flow
1014 flowService.start();
1015 flowService.load(null);
1017 logger.info("Flow loaded successfully.");
1019 } catch (BeansException | LifeCycleStartException | IOException | FlowSerializationException | FlowSynchronizationException | UninheritableFlowException e) {
1020 // ensure the flow service is terminated
1021 if (flowService != null && flowService.isRunning()) {
1022 flowService.stop(false);
1024 logger.error("Unable to load flow due to: " + e, e);
1025 throw new Exception("Unable to load flow due to: " + e); // cannot wrap the exception as they are not defined in a classloader accessible to the caller
1029 final NarLoader narLoader = new StandardNarLoader(
1030 props.getExtensionsWorkingDirectory(),
1031 props.getComponentDocumentationWorkingDirectory(),
1032 NarClassLoadersHolder.getInstance(),
1037 narAutoLoader = new NarAutoLoader(props.getNarAutoLoadDirectory(), narLoader);
1038 narAutoLoader.start();
1040 URI jarsIndex = props.getDCAEJarIndexURI();
1042 // REVIEW: Added ability to turn off the loaidng of dcae jars by providing no url
1043 if (jarsIndex == null) {
1044 StringBuilder sb = new StringBuilder();
1045 sb.append("Auto-loading of DCAE jars is turned off.");
1046 sb.append(" You must set the value of \"nifi.dcae.jars.index.url\"");
1047 sb.append(" to the full url to the index JSON of DCAE jars in the nifi.properties file");
1048 sb.append(" in order to activate this feature.");
1049 logger.warn(sb.toString());
1051 this.dcaeAutoLoader = new DCAEAutoLoader();
1052 this.dcaeAutoLoader.start(jarsIndex, extensionManager);
1055 // dump the application url after confirming everything started successfully
1057 } catch (Exception ex) {
1062 private void performInjectionForComponentUis(final Collection<WebAppContext> componentUiExtensionWebContexts,
1063 final NiFiWebConfigurationContext configurationContext, final FilterHolder securityFilter) {
1064 if (CollectionUtils.isNotEmpty(componentUiExtensionWebContexts)) {
1065 for (final WebAppContext customUiContext : componentUiExtensionWebContexts) {
1066 // set the NiFi context in each custom ui servlet context
1067 final ServletContext customUiServletContext = customUiContext.getServletHandler().getServletContext();
1068 customUiServletContext.setAttribute("nifi-web-configuration-context", configurationContext);
1070 // add the security filter to any ui extensions wars
1071 if (securityFilter != null) {
1072 customUiContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
1078 private void performInjectionForContentViewerUis(final Collection<WebAppContext> contentViewerWebContexts,
1079 final FilterHolder securityFilter) {
1080 if (CollectionUtils.isNotEmpty(contentViewerWebContexts)) {
1081 for (final WebAppContext contentViewerContext : contentViewerWebContexts) {
1082 // add the security filter to any content viewer wars
1083 if (securityFilter != null) {
1084 contentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
1090 private void dumpUrls() throws SocketException {
1091 final List<String> urls = new ArrayList<>();
1093 for (Connector connector : server.getConnectors()) {
1094 if (connector instanceof ServerConnector) {
1095 final ServerConnector serverConnector = (ServerConnector) connector;
1097 Set<String> hosts = new HashSet<>();
1099 // determine the hosts
1100 if (StringUtils.isNotBlank(serverConnector.getHost())) {
1101 hosts.add(serverConnector.getHost());
1103 Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
1104 if (networkInterfaces != null) {
1105 for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
1106 for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
1107 hosts.add(inetAddress.getHostAddress());
1113 // ensure some hosts were found
1114 if (!hosts.isEmpty()) {
1115 String scheme = "http";
1116 if (props.getSslPort() != null && serverConnector.getPort() == props.getSslPort()) {
1121 for (String host : hosts) {
1122 urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
1128 if (urls.isEmpty()) {
1129 logger.warn("NiFi has started, but the UI is not available on any hosts. Please verify the host properties.");
1131 // log the ui location
1132 logger.info("NiFi has started. The UI is available at the following URLs:");
1133 for (final String url : urls) {
1134 logger.info(String.format("%s/nifi", url));
1139 private void startUpFailure(Throwable t) {
1140 System.err.println("Failed to start web server: " + t.getMessage());
1141 System.err.println("Shutting down...");
1142 logger.warn("Failed to start web server... shutting down.", t);
1147 public void setExtensionMapping(ExtensionMapping extensionMapping) {
1148 this.extensionMapping = extensionMapping;
1152 public void setBundles(Bundle systemBundle, Set<Bundle> bundles) {
1153 this.systemBundle = systemBundle;
1154 this.bundles = bundles;
1158 public void stop() {
1161 } catch (Exception ex) {
1162 logger.warn("Failed to stop web server", ex);
1166 if (narAutoLoader != null) {
1167 narAutoLoader.stop();
1170 if (dcaeAutoLoader != null) {
1171 dcaeAutoLoader.stop();
1173 } catch (Exception e) {
1174 logger.warn("Failed to stop NAR auto-loader", e);
1179 * Holds the result of loading WARs for custom UIs.
1181 private static class ExtensionUiInfo {
1183 private final Collection<WebAppContext> webAppContexts;
1184 private final Map<String, String> mimeMappings;
1185 private final Collection<WebAppContext> componentUiExtensionWebContexts;
1186 private final Collection<WebAppContext> contentViewerWebContexts;
1187 private final Map<String, List<UiExtension>> componentUiExtensionsByType;
1189 public ExtensionUiInfo(final Collection<WebAppContext> webAppContexts,
1190 final Map<String, String> mimeMappings,
1191 final Collection<WebAppContext> componentUiExtensionWebContexts,
1192 final Collection<WebAppContext> contentViewerWebContexts,
1193 final Map<String, List<UiExtension>> componentUiExtensionsByType) {
1194 this.webAppContexts = webAppContexts;
1195 this.mimeMappings = mimeMappings;
1196 this.componentUiExtensionWebContexts = componentUiExtensionWebContexts;
1197 this.contentViewerWebContexts = contentViewerWebContexts;
1198 this.componentUiExtensionsByType = componentUiExtensionsByType;
1201 public Collection<WebAppContext> getWebAppContexts() {
1202 return webAppContexts;
1205 public Map<String, String> getMimeMappings() {
1206 return mimeMappings;
1209 public Collection<WebAppContext> getComponentUiExtensionWebContexts() {
1210 return componentUiExtensionWebContexts;
1213 public Collection<WebAppContext> getContentViewerWebContexts() {
1214 return contentViewerWebContexts;
1217 public Map<String, List<UiExtension>> getComponentUiExtensionsByType() {
1218 return componentUiExtensionsByType;
1223 @FunctionalInterface
1224 interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> {
1225 ServerConnector create(Server server, HttpConfiguration httpConfiguration);