From 5885f77aa2999e19a706a6c9a16b94b5f3eb6587 Mon Sep 17 00:00:00 2001 From: "satendra.sahu" Date: Thu, 27 Sep 2018 22:16:43 +0530 Subject: [PATCH 01/20] secured kafka changes --- pom.xml | 665 +++---- .../java/com/homeadvisor/kafdrop/KafDrop.java | 225 +-- .../kafdrop/config/CuratorConfiguration.java | 217 ++- .../kafdrop/config/KafkaConfiguration.java | 18 + .../config/ServiceDiscoveryConfiguration.java | 2 +- .../kafdrop/controller/ClusterController.java | 141 +- .../homeadvisor/kafdrop/model/BrokerVO.java | 178 +- .../kafdrop/model/ConsumerTopicVO.java | 2 +- .../homeadvisor/kafdrop/model/TopicVO.java | 257 +-- .../kafdrop/service/CuratorKafkaMonitor.java | 1631 ++++++++--------- .../service/KafkaHighLevelConsumer.java | 157 ++ .../kafdrop/service/KafkaMonitor.java | 11 +- .../kafdrop/service/MessageInspector.java | 37 +- src/main/resources/application.yml | 15 +- src/main/resources/log4j.properties | 2 +- .../resources/static/css/baseless.min.css | 1 + src/main/resources/static/css/global.css | 43 +- src/main/resources/static/js/global.js | 2 +- src/main/resources/static/js/jquery.min.js | 9 +- .../resources/templates/broker-detail.ftl | 21 +- .../resources/templates/cluster-overview.ftl | 119 +- .../resources/templates/consumer-detail.ftl | 23 +- .../resources/templates/includes/header.ftl | 21 +- src/main/resources/templates/lib/template.ftl | 20 +- .../resources/templates/message-inspector.ftl | 68 +- .../resources/templates/not-initialized.ftl | 15 - src/main/resources/templates/topic-detail.ftl | 59 +- 27 files changed, 1814 insertions(+), 2145 deletions(-) create mode 100644 src/main/java/com/homeadvisor/kafdrop/config/KafkaConfiguration.java create mode 100644 src/main/java/com/homeadvisor/kafdrop/service/KafkaHighLevelConsumer.java create mode 100644 src/main/resources/static/css/baseless.min.css diff --git a/pom.xml b/pom.xml index cc226b5..35e4030 100644 --- a/pom.xml +++ b/pom.xml @@ -1,449 +1,256 @@ - - 4.0.0 + + 4.0.0 - com.homeadvisor.kafka - kafdrop - 2.0.6 + com.ola.dataplatform + kafka-dp-dashboard + 2.0.2-SNAPSHOT - For when you have a Kaf(ka) cluster to monitor + + com.olacabs + ola-libs-master-pom + 1.0 + - - 1.3.6.RELEASE - -Xdoclint:none - 2.10.0 + For when you have a Kaf(ka) cluster to monitor - - -Xdoclint:none + + 1.3.6.RELEASE + -Xdoclint:none + 2.10.0 + - - - scm:git:git@github.com:HomeAdvisor/Kafdrop.git - scm:git:git@github.com:HomeAdvisor/Kafdrop.git - HEAD - - - - - - org.springframework.boot - spring-boot-starter-parent - ${spring.boot.version} - pom - import - - - - - - - commons-lang - commons-lang - 2.6 - - - org.apache.curator - curator-recipes - ${curator.version} - - - org.apache.curator - curator-x-discovery - ${curator.version} - - - org.apache.zookeeper - zookeeper - 3.4.8 - - - org.apache.kafka - kafka_2.9.2 - 0.8.2.2 - - - org.freemarker - freemarker - 2.3.23 - - - org.springframework.retry - spring-retry - 1.1.3.RELEASE - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-logging - - - - - org.springframework.boot - spring-boot-starter-freemarker - - - org.springframework.boot - spring-boot-starter-log4j - - - org.springframework.boot - spring-boot-starter-actuator - - - - - org.springframework - spring-context - - - org.springframework - spring-core - - - org.springframework - spring-beans - + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + - - - io.springfox - springfox-swagger2 - 2.7.0 - + + + commons-lang + commons-lang + 2.6 + + + org.apache.curator + curator-recipes + ${curator.version} + + + org.apache.curator + curator-x-discovery + ${curator.version} + + + org.apache.zookeeper + zookeeper + 3.4.8 + + + org.apache.kafka + kafka_2.9.2 + 0.8.2.2 + - - - junit - junit - 4.11 - test - - - org.easymock - easymock - 3.3.1 - test - + + org.projectlombok + lombok + 1.16.6 + - - - org.codehaus.groovy - groovy-all - 2.4.6 - test - - - org.spockframework - spock-core - 1.0-groovy-2.4 - test - - - org.codehaus.groovy.modules.http-builder - http-builder - 0.7.1 - test - - - - org.apache.kafka - kafka-clients - 0.8.2.2 - test - - + + org.apache.kafka + kafka-clients + 0.11.0.2 + + + org.freemarker + freemarker + 2.3.23 + + + org.springframework.retry + spring-retry + 1.1.3.RELEASE + - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.8 - 1.8 - - - - maven-assembly-plugin - - - src/main/assembly/bin.xml - - - 2.2.1 - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - true - lib/ - com.homeadvisor.kafdrop.KafDrop - - - - - - com.spotify - docker-maven-plugin - 0.4.13 - - kafdrop - true - ${project.build.directory}/docker-ready - - ${project.version} - latest - - - - / - ${project.build.directory} - ${project.build.finalName}-bin.tar.gz - - - - - - maven-resources-plugin - 2.7 - - - prepare-dockerfile - validate - - copy-resources - - - ${project.build.directory}/docker-ready - - - src/main/docker - true - - - - - - - + + org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - - - repackage - - - - - - - - src/main/resources - true - - static/** - - - - src/main/resources - false - - static/** - - - - - - - - api-test - - api-test - api-test-classes - ${project.build.directory}/api-surefire-reports - - - - - + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework.boot + spring-boot-starter-log4j + + + org.springframework.boot + spring-boot-starter-actuator + - - + + + org.springframework + spring-context + + + org.springframework + spring-core + + + org.springframework + spring-beans + - - - com.github.charithe - kafka-maven-plugin - 1.0.0 - - 2180 - 9092 - + + + io.springfox + springfox-swagger2 + 2.7.0 + - - - - org.apache.zookeeper - zookeeper - 3.4.8 - - - org.apache.kafka - kafka_2.9.2 - 0.8.2.2 - - - org.apache.kafka - kafka-clients - 0.8.2.2 - - + + + junit + junit + 4.11 + test + + + org.easymock + easymock + 3.3.1 + test + + - - - - zk-kafka-pre-integration - pre-integration-test - - start-kafka-broker - - - - zk-kafka-post-integration - post-integration-test - - stop-kafka-broker - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - - ${start-class} - - - false - - --server.port:11017 - --zookeeper.connect=localhost:2180 - - - - - - repackage - - - - kafdrop-pre-integration - integration-test - - start - - - - kafdrop-post-integration - post-integration-test - - stop - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.18 - - ${project.basedir}/src/api-test/java - - **/*Spec.java - - ${api.test.report.dir} - ${test.arguments} - - - - integration-test - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.18 - - ${project.build.directory}/api-test-report - ${api.report.dir} - - - - integration-test - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.20 - - - - integration-test - - - - - - org.codehaus.gmavenplus - gmavenplus-plugin - 1.5 - - - integration-test + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + maven-assembly-plugin + + + src/main/assembly/bin.xml + + + 2.2.1 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + lib/ + com.homeadvisor.kafdrop.KafDrop + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + - testCompile + repackage - - - - - - ${project.basedir}/src/${test.base.dir}/groovy - - **/*.groovy - - - - - - - - - + + + + ${start-class} + + + + + + src/main/resources + true + + static/** + + + + src/main/resources + false + + static/** + + + + + + + docker + + + docker + + + + + + com.spotify + docker-maven-plugin + 1.0.0 + + artifactory.corp.olacabs.com:6001/${project.artifactId}:${project.version} + + docker + ${project.basedir} + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + package + + build + + + + + + + + diff --git a/src/main/java/com/homeadvisor/kafdrop/KafDrop.java b/src/main/java/com/homeadvisor/kafdrop/KafDrop.java index 47ed3de..fac3d18 100644 --- a/src/main/java/com/homeadvisor/kafdrop/KafDrop.java +++ b/src/main/java/com/homeadvisor/kafdrop/KafDrop.java @@ -21,6 +21,7 @@ import com.google.common.base.Throwables; import com.homeadvisor.kafdrop.config.ini.IniFilePropertySource; import com.homeadvisor.kafdrop.config.ini.IniFileReader; +import joptsimple.internal.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.Banner; @@ -29,128 +30,132 @@ import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.util.Objects; import java.util.stream.Stream; @SpringBootApplication(exclude = MetricFilterAutoConfiguration.class) public class KafDrop { - private final static Logger LOG = LoggerFactory.getLogger(KafDrop.class); - - public static void main(String[] args) - { - new SpringApplicationBuilder(KafDrop.class) - .bannerMode(Banner.Mode.OFF) - .listeners(new EnvironmentSetupListener(), - new LoggingConfigurationListener()) - .run(args); - } - - private static class LoggingConfigurationListener implements ApplicationListener, Ordered - { - private static final String PROP_LOGGING_CONFIG = "logging.config"; - private static final String PROP_LOGGING_FILE = "logging.file"; - private static final String PROP_LOGGER = "LOGGER"; - private static final String PROP_SPRING_BOOT_LOG_LEVEL = "logging.level.org.springframework.boot"; - - @Override - public int getOrder() - { - // LoggingApplicationListener runs at HIGHEST_PRECEDENCE + 11. This needs to run before that. - return Ordered.HIGHEST_PRECEDENCE; - } - - @Override - public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) - { - Environment environment = event.getEnvironment(); - final String loggingFile = environment.getProperty(PROP_LOGGING_FILE); - if (loggingFile != null) - { - System.setProperty(PROP_LOGGER, "FILE"); - try - { - System.setProperty("logging.dir", new File(loggingFile).getParent()); + private final static Logger LOG = LoggerFactory.getLogger(KafDrop.class); + + public static void main(String[] args) + { + + new SpringApplicationBuilder(KafDrop.class) + .bannerMode(Banner.Mode.OFF) + .listeners(new EnvironmentSetupListener(), + new LoggingConfigurationListener()) + .run(args); + } + + private static class LoggingConfigurationListener + implements ApplicationListener, Ordered + { + private static final String PROP_LOGGING_CONFIG = "logging.config"; + private static final String PROP_LOGGING_FILE = "logging.file"; + private static final String PROP_LOGGER = "LOGGER"; + private static final String PROP_SPRING_BOOT_LOG_LEVEL = "logging.level.org.springframework.boot"; + + @Override + public int getOrder() + { + // LoggingApplicationListener runs at HIGHEST_PRECEDENCE + 11. This needs to run before that. + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) + { + Environment environment = event.getEnvironment(); + final String loggingFile = environment.getProperty(PROP_LOGGING_FILE); + if (loggingFile != null) { + System.setProperty(PROP_LOGGER, "FILE"); + try { + System.setProperty("logging.dir", new File(loggingFile).getParent()); + } + catch (Exception ex) { + System.err.println("Unable to set up logging.dir from logging.file " + loggingFile + ": " + + Throwables.getStackTraceAsString(ex)); + } } - catch (Exception ex) - { - System.err.println("Unable to set up logging.dir from logging.file " + loggingFile + ": " + - Throwables.getStackTraceAsString(ex)); + if (environment.containsProperty("debug") && + !"false".equalsIgnoreCase(environment.getProperty("debug", String.class))) { + System.setProperty(PROP_SPRING_BOOT_LOG_LEVEL, "DEBUG"); } - } - if (environment.containsProperty("debug") && - !"false".equalsIgnoreCase(environment.getProperty("debug", String.class))) - { - System.setProperty(PROP_SPRING_BOOT_LOG_LEVEL, "DEBUG"); - } - - } - - } - - private static class EnvironmentSetupListener implements ApplicationListener, Ordered - { - private static final String SM_CONFIG_DIR = "sm.config.dir"; - private static final String CONFIG_SUFFIX = "-config.ini"; - - @Override - public int getOrder() - { - return Ordered.HIGHEST_PRECEDENCE + 10; - } - - @Override - public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) - { - final ConfigurableEnvironment environment = event.getEnvironment(); - if (environment.containsProperty(SM_CONFIG_DIR)) - { - Stream.of("kafdrop", "global") - .map(name -> readProperties(environment, name)) - .filter(Objects::nonNull) - .forEach(iniPropSource -> environment.getPropertySources() - .addBefore("applicationConfigurationProperties", iniPropSource)); - } - } - - private IniFilePropertySource readProperties(Environment environment, String name) - { - final File file = new File(environment.getProperty(SM_CONFIG_DIR), name + CONFIG_SUFFIX); - if (file.exists() && file.canRead()) - { - try (InputStream in = new FileInputStream(file); - Reader reader = new InputStreamReader(in, "UTF-8")) - { - return new IniFilePropertySource(name, new IniFileReader().read(reader), environment.getActiveProfiles()); + } + } + + private static class EnvironmentSetupListener + implements ApplicationListener, Ordered + { + private static final String SM_CONFIG_DIR = "sm.config.dir"; + private static final String CONFIG_SUFFIX = "-config.ini"; + + @Override + public int getOrder() + { + return Ordered.HIGHEST_PRECEDENCE + 10; + } + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) + { + final ConfigurableEnvironment environment = event.getEnvironment(); + + LOG.info("Initializing jaas config"); + String env = environment.getProperty("kafka.env"); + Boolean isSecured = environment.getProperty("kafka.isSecured", Boolean.class); + LOG.info("env: {} .Issecured kafka: {}", env, isSecured); + if (isSecured && Strings.isNullOrEmpty(env)) { + throw new RuntimeException("'env' cannot be null if connecting to secured kafka."); + } + + LOG.info("ENV: {}", env); + String path; + + if (isSecured) { + if ((env.equalsIgnoreCase("stage") || env.equalsIgnoreCase("prod") || env.equalsIgnoreCase("local"))) { + path = environment.getProperty("user.dir") + "/kaas_" + env.toLowerCase() + "_jaas.conf"; + LOG.info("PATH: {}", path); + System.setProperty("java.security.auth.login.config", path); + } + else { + throw new RuntimeException("unable to identify env. set 'evn' variable either to 'stage' or 'prod' or local"); + } } - catch (IOException ex) - { - LOG.error("Unable to read configuration file {}", file, ex); + + if (environment.containsProperty(SM_CONFIG_DIR)) { + Stream.of("kafdrop", "global") + .map(name -> readProperties(environment, name)) + .filter(Objects::nonNull) + .forEach(iniPropSource -> environment.getPropertySources() + .addBefore("applicationConfigurationProperties", iniPropSource)); + } + } + + private IniFilePropertySource readProperties(Environment environment, String name) + { + final File file = new File(environment.getProperty(SM_CONFIG_DIR), name + CONFIG_SUFFIX); + if (file.exists() && file.canRead()) { + try (InputStream in = new FileInputStream(file); + Reader reader = new InputStreamReader(in, "UTF-8")) { + return new IniFilePropertySource(name, new IniFileReader().read(reader), environment.getActiveProfiles()); + } + catch (IOException ex) { + LOG.error("Unable to read configuration file {}", file, ex); + } } - } - return null; - } - } - - @Bean - public WebMvcConfigurerAdapter webConfig() - { - return new WebMvcConfigurerAdapter() - { - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) - { - super.configureContentNegotiation(configurer); - configurer.favorPathExtension(false); - } - }; - } + return null; + } + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/config/CuratorConfiguration.java b/src/main/java/com/homeadvisor/kafdrop/config/CuratorConfiguration.java index 8d63e47..875b836 100644 --- a/src/main/java/com/homeadvisor/kafdrop/config/CuratorConfiguration.java +++ b/src/main/java/com/homeadvisor/kafdrop/config/CuratorConfiguration.java @@ -38,113 +38,112 @@ @Configuration public class CuratorConfiguration { - @Bean(initMethod = "start", destroyMethod = "close") - public CuratorFramework curatorFramework(ZookeeperProperties props) - { - return CuratorFrameworkFactory.builder() - .connectString(props.getConnect()) - .connectionTimeoutMs(props.getConnectTimeoutMillis()) - .sessionTimeoutMs(props.getSessionTimeoutMillis()) - .retryPolicy(new RetryNTimes(props.getMaxRetries(), props.getRetryMillis())) - .build(); - } - - @Component - @ConfigurationProperties(prefix = "zookeeper") - public static class ZookeeperProperties - { - public static final Pattern CONNECT_SEPARATOR = Pattern.compile("\\s*,\\s*"); - @NotBlank - private String connect; - - private int sessionTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(5); - private int connectTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(15); - - private int retryMillis = (int) TimeUnit.SECONDS.toMillis(5); - private int maxRetries = Integer.MAX_VALUE; - - - public String getConnect() - { - return connect; - } - - public void setConnect(String connect) - { - this.connect = connect; - } - - public List getConnectList() - { - return CONNECT_SEPARATOR.splitAsStream(this.connect) - .map(String::trim) - .filter(s -> s.length() > 0) - .collect(Collectors.toList()); - } - - public int getRetryMillis() - { - return retryMillis; - } - - public void setRetryMillis(int retryMillis) - { - this.retryMillis = retryMillis; - } - - public int getMaxRetries() - { - return maxRetries; - } - - public void setMaxRetries(int maxRetries) - { - this.maxRetries = maxRetries; - } - - public int getSessionTimeoutMillis() - { - return sessionTimeoutMillis; - } - - public void setSessionTimeoutMillis(int sessionTimeoutMillis) - { - this.sessionTimeoutMillis = sessionTimeoutMillis; - } - - public int getConnectTimeoutMillis() - { - return connectTimeoutMillis; - } - - public void setConnectTimeoutMillis(int connectTimeoutMillis) - { - this.connectTimeoutMillis = connectTimeoutMillis; - } - } - - @Component(value = "curatorConnection") - private static class CuratorHealthIndicator extends AbstractHealthIndicator - { - private final CuratorFramework framework; - - @Autowired - public CuratorHealthIndicator(CuratorFramework framework) - { - this.framework = framework; - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception - { - if (framework.getZookeeperClient().isConnected()) - { - builder.up(); - } - else - { - builder.down(); - } - } - } + @Bean(initMethod = "start", destroyMethod = "close") + public CuratorFramework curatorFramework(ZookeeperProperties props) + { + return CuratorFrameworkFactory.builder() + .connectString(props.getConnect()) + .connectionTimeoutMs(props.getConnectTimeoutMillis()) + .sessionTimeoutMs(props.getSessionTimeoutMillis()) + .retryPolicy(new RetryNTimes(props.getMaxRetries(), props.getRetryMillis())) + .build(); + } + + @Component + @ConfigurationProperties(prefix = "zookeeper") + public static class ZookeeperProperties + { + public static final Pattern CONNECT_SEPARATOR = Pattern.compile("\\s*,\\s*"); + @NotBlank + private String connect; + + private int sessionTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(5); + private int connectTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(15); + + private int retryMillis = (int) TimeUnit.SECONDS.toMillis(5); + private int maxRetries = Integer.MAX_VALUE; + + public String getConnect() + { + return connect; + } + + public void setConnect(String connect) + { + this.connect = connect; + } + + public List getConnectList() + { + return CONNECT_SEPARATOR.splitAsStream(this.connect) + .map(String::trim) + .filter(s -> s.length() > 0) + .collect(Collectors.toList()); + } + + public int getRetryMillis() + { + return retryMillis; + } + + public void setRetryMillis(int retryMillis) + { + this.retryMillis = retryMillis; + } + + public int getMaxRetries() + { + return maxRetries; + } + + public void setMaxRetries(int maxRetries) + { + this.maxRetries = maxRetries; + } + + public int getSessionTimeoutMillis() + { + return sessionTimeoutMillis; + } + + public void setSessionTimeoutMillis(int sessionTimeoutMillis) + { + this.sessionTimeoutMillis = sessionTimeoutMillis; + } + + public int getConnectTimeoutMillis() + { + return connectTimeoutMillis; + } + + public void setConnectTimeoutMillis(int connectTimeoutMillis) + { + this.connectTimeoutMillis = connectTimeoutMillis; + } + } + + @Component(value = "curatorConnection") + private static class CuratorHealthIndicator + extends AbstractHealthIndicator + { + private final CuratorFramework framework; + + @Autowired + public CuratorHealthIndicator(CuratorFramework framework) + { + this.framework = framework; + } + + @Override + protected void doHealthCheck(Health.Builder builder) + throws Exception + { + if (framework.getZookeeperClient().isConnected()) { + builder.up(); + } + else { + builder.down(); + } + } + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/config/KafkaConfiguration.java b/src/main/java/com/homeadvisor/kafdrop/config/KafkaConfiguration.java new file mode 100644 index 0000000..d3d8d96 --- /dev/null +++ b/src/main/java/com/homeadvisor/kafdrop/config/KafkaConfiguration.java @@ -0,0 +1,18 @@ +package com.homeadvisor.kafdrop.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Created by Satendra Sahu on 9/26/18 + */ +@Component +@ConfigurationProperties(prefix = "kafka") +@Data +public class KafkaConfiguration +{ + private String env = "local"; + private String brokerConnect; + private Boolean isSecured = false; +} diff --git a/src/main/java/com/homeadvisor/kafdrop/config/ServiceDiscoveryConfiguration.java b/src/main/java/com/homeadvisor/kafdrop/config/ServiceDiscoveryConfiguration.java index 73977dc..c982b17 100644 --- a/src/main/java/com/homeadvisor/kafdrop/config/ServiceDiscoveryConfiguration.java +++ b/src/main/java/com/homeadvisor/kafdrop/config/ServiceDiscoveryConfiguration.java @@ -66,7 +66,7 @@ public ServiceDiscovery curatorServiceDiscovery( @Value("${curator.discovery.basePath:/homeadvisor/services}") String basePath) throws Exception { final Class payloadClass = Object.class; - curatorFramework.createContainers(basePath); + new EnsurePath(basePath).ensure(curatorFramework.getZookeeperClient()); return ServiceDiscoveryBuilder.builder(payloadClass) .client(curatorFramework) .basePath(basePath) diff --git a/src/main/java/com/homeadvisor/kafdrop/controller/ClusterController.java b/src/main/java/com/homeadvisor/kafdrop/controller/ClusterController.java index 6ed7e4b..01f2110 100644 --- a/src/main/java/com/homeadvisor/kafdrop/controller/ClusterController.java +++ b/src/main/java/com/homeadvisor/kafdrop/controller/ClusterController.java @@ -20,7 +20,6 @@ import com.homeadvisor.kafdrop.config.CuratorConfiguration; import com.homeadvisor.kafdrop.model.BrokerVO; -import com.homeadvisor.kafdrop.model.ClusterSummaryVO; import com.homeadvisor.kafdrop.model.TopicVO; import com.homeadvisor.kafdrop.service.BrokerNotFoundException; import com.homeadvisor.kafdrop.service.KafkaMonitor; @@ -28,88 +27,80 @@ import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; @Controller public class ClusterController { - @Autowired - private KafkaMonitor kafkaMonitor; - - @Autowired - private CuratorConfiguration.ZookeeperProperties zookeeperProperties; - - @RequestMapping("/") - public String clusterInfo(Model model, - @RequestParam(value="filter", required=false) String filter) - { - model.addAttribute("zookeeper", zookeeperProperties); - - final List brokers = kafkaMonitor.getBrokers(); - final List topics = kafkaMonitor.getTopics(); - final ClusterSummaryVO clusterSummary = kafkaMonitor.getClusterSummary(topics); - - final List missingBrokerIds = clusterSummary.getExpectedBrokerIds().stream() - .filter(brokerId -> brokers.stream().noneMatch(b -> b.getId() == brokerId)) - .collect(Collectors.toList()); - - model.addAttribute("brokers", brokers); - model.addAttribute("missingBrokerIds", missingBrokerIds); - model.addAttribute("topics", topics); - model.addAttribute("clusterSummary", clusterSummary); - - if (filter != null) - { - model.addAttribute("filter", filter); - } - - return "cluster-overview"; - } - - @ApiOperation(value = "getCluster", notes = "Get high level broker, topic, and partition data for the Kafka cluster") - @ApiResponses(value = { - @ApiResponse(code = 200, message = "Success", response = ClusterInfoVO.class) - }) - @RequestMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) - public @ResponseBody - ClusterInfoVO getCluster() throws Exception - { - ClusterInfoVO vo = new ClusterInfoVO(); - - vo.zookeeper = zookeeperProperties; - vo.brokers = kafkaMonitor.getBrokers(); - vo.topics = kafkaMonitor.getTopics(); - vo.summary = kafkaMonitor.getClusterSummary(vo.topics); - - return vo; - } - - @ExceptionHandler(BrokerNotFoundException.class) - private String brokerNotFound(Model model) - { - model.addAttribute("zookeeper", zookeeperProperties); - model.addAttribute("brokers", Collections.emptyList()); - model.addAttribute("topics", Collections.emptyList()); - return "cluster-overview"; - - } - - /** - * Simple DTO to encapsulate the cluster state: ZK properties, broker list, - * and topic list. - */ - public static class ClusterInfoVO - { - public CuratorConfiguration.ZookeeperProperties zookeeper; - public ClusterSummaryVO summary; - public List brokers; - public List topics; - } + @Autowired + private KafkaMonitor kafkaMonitor; + + @Autowired + private CuratorConfiguration.ZookeeperProperties zookeeperProperties; + + @RequestMapping("/") + public String clusterInfo(Model model) + { + model.addAttribute("zookeeper", zookeeperProperties); + model.addAttribute("brokers", kafkaMonitor.getBrokers()); + model.addAttribute("topics", kafkaMonitor.getTopics()); + return "cluster-overview"; + } + + @ApiOperation(value = "getCluster", notes = "Get high level broker, topic, and partition data for the Kafka cluster") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Success", response = ClusterInfoVO.class) + }) + @RequestMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + public @ResponseBody + ClusterInfoVO getCluster() + throws Exception + { + ClusterInfoVO vo = new ClusterInfoVO(); + + vo.zookeeper = zookeeperProperties; + //vo.brokers = kafkaMonitor.getBrokers(); + vo.brokers = new ArrayList<>(); + vo.topics = kafkaMonitor.getTopics(); + + return vo; + } + + @ExceptionHandler(BrokerNotFoundException.class) + private String brokerNotFound(Model model) + { + model.addAttribute("zookeeper", zookeeperProperties); + model.addAttribute("brokers", Collections.emptyList()); + model.addAttribute("topics", Collections.emptyList()); + return "cluster-overview"; + } + + /** + * Simple DTO to encapsulate the cluster state: ZK properties, broker list, + * and topic list. + */ + public static class ClusterInfoVO + { + public CuratorConfiguration.ZookeeperProperties zookeeper; + public List brokers; + public List topics; + } + + @ResponseStatus(HttpStatus.OK) + @RequestMapping("/health_check") + public void healthCheck() + { + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/model/BrokerVO.java b/src/main/java/com/homeadvisor/kafdrop/model/BrokerVO.java index 7e8ba7b..15b2ccd 100644 --- a/src/main/java/com/homeadvisor/kafdrop/model/BrokerVO.java +++ b/src/main/java/com/homeadvisor/kafdrop/model/BrokerVO.java @@ -18,88 +18,110 @@ package com.homeadvisor.kafdrop.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; +@JsonIgnoreProperties(ignoreUnknown = true) public class BrokerVO { - private int id; - private String host; - private int port; - private int jmxPort; - private int version; - private boolean controller; - private Date timestamp; - - public int getId() - { - return id; - } - - public void setId(int id) - { - this.id = id; - } - - public String getHost() - { - return host; - } - - public void setHost(String host) - { - this.host = host; - } - - public int getPort() - { - return port; - } - - public void setPort(int port) - { - this.port = port; - } - - public int getJmxPort() - { - return jmxPort; - } - - @JsonProperty("jmx_port") - public void setJmxPort(int jmxPort) - { - this.jmxPort = jmxPort; - } - - public int getVersion() - { - return version; - } - - public void setVersion(int version) - { - this.version = version; - } - - public Date getTimestamp() - { - return timestamp; - } - - public void setTimestamp(Date timestamp) - { - this.timestamp = timestamp; - } - - public boolean isController() - { - return controller; - } - - public void setController(boolean controller) - { - this.controller = controller; - } + private int id; + private String host; + private String[] endpoints; + private int port; + private int jmxPort; + private int version; + private boolean controller; + private Date timestamp; + + public void setEndpoints(String[] endpoints) + { + this.endpoints = endpoints; + if (host == null) { + String[] hostPort = endpoints[0].split("://")[1].split(":"); + this.host = hostPort[0]; + this.port = Integer.parseInt(hostPort[1]); + } + } + + public String[] getEndpoints() + { + return this.endpoints; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public String getHost() + { + return host; + } + + public void setHost(String host) + { + if (host != null) { + this.host = host; + } + } + + public int getPort() + { + return port; + } + + public void setPort(int port) + { + if (port > 0) { + this.port = port; + } + } + + public int getJmxPort() + { + return jmxPort; + } + + @JsonProperty("jmx_port") + public void setJmxPort(int jmxPort) + { + this.jmxPort = jmxPort; + } + + public int getVersion() + { + return version; + } + + public void setVersion(int version) + { + this.version = version; + } + + public Date getTimestamp() + { + return timestamp; + } + + public void setTimestamp(Date timestamp) + { + this.timestamp = timestamp; + } + + public boolean isController() + { + return controller; + } + + public void setController(boolean controller) + { + this.controller = controller; + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/model/ConsumerTopicVO.java b/src/main/java/com/homeadvisor/kafdrop/model/ConsumerTopicVO.java index 2c380d9..2926045 100644 --- a/src/main/java/com/homeadvisor/kafdrop/model/ConsumerTopicVO.java +++ b/src/main/java/com/homeadvisor/kafdrop/model/ConsumerTopicVO.java @@ -66,7 +66,7 @@ public Collection getPartitions() public double getCoveragePercent() { - return (offsets.size() > 0) ? ((double)getAssignedPartitionCount()) / offsets.size() : 0.0; + return ((double)getAssignedPartitionCount()) / offsets.size(); } public int getAssignedPartitionCount() diff --git a/src/main/java/com/homeadvisor/kafdrop/model/TopicVO.java b/src/main/java/com/homeadvisor/kafdrop/model/TopicVO.java index e48f0a5..136909a 100644 --- a/src/main/java/com/homeadvisor/kafdrop/model/TopicVO.java +++ b/src/main/java/com/homeadvisor/kafdrop/model/TopicVO.java @@ -18,131 +18,140 @@ package com.homeadvisor.kafdrop.model; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; import java.util.stream.Collectors; -public class TopicVO implements Comparable +public class TopicVO + implements Comparable { - private String name; - private Map partitions = new TreeMap<>(); - private Map config = new TreeMap<>(); - // description? - // partition state - // delete supported? - - - public TopicVO(String name) - { - this.name = name; - } - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } - - public Map getConfig() - { - return config; - } - - public void setConfig(Map config) - { - this.config = config; - } - - public Collection getPartitions() - { - return partitions.values(); - } - - public Optional getPartition(int partitionId) - { - return Optional.ofNullable(partitions.get(partitionId)); - } - - public Collection getLeaderPartitions(int brokerId) - { - return partitions.values().stream() - .filter(tp -> tp.getLeader() != null && tp.getLeader().getId() == brokerId) - .collect(Collectors.toList()); - } - - public Collection getUnderReplicatedPartitions() - { - return partitions.values().stream() - .filter(TopicPartitionVO::isUnderReplicated) - .collect(Collectors.toList()); - } - - public void setPartitions(Map partitions) - { - this.partitions = partitions; - } - - /** - * Returns the total number of messages published to the topic, ever - * @return - */ - public long getTotalSize() - { - return partitions.values().stream() - .map(TopicPartitionVO::getSize) - .reduce(0L, Long::sum); - } - - /** - * Returns the total number of messages available to consume from the topic. - * @return - */ - public long getAvailableSize() - { - return partitions.values().stream() - .map(p -> p.getSize() - p.getFirstOffset()) - .reduce(0L, Long::sum); - } - - public double getPreferredReplicaPercent() - { - long preferredLeaderCount = partitions.values().stream() - .filter(TopicPartitionVO::isLeaderPreferred) - .count(); - return ((double) preferredLeaderCount) / ((double)partitions.size()); - } - - public void addPartition(TopicPartitionVO partition) - { - partitions.put(partition.getId(), partition); - } - - @Override - public int compareTo(TopicVO that) - { - return this.name.compareTo(that.name); - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TopicVO that = (TopicVO) o; - - if (!name.equals(that.name)) return false; - - return true; - } - - @Override - public int hashCode() - { - return name.hashCode(); - } - + private String name; + private Map partitions = new TreeMap<>(); + private Map config = new TreeMap<>(); + // description? + // partition state + // delete supported? + + public TopicVO(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Map getConfig() + { + return config; + } + + public void setConfig(Map config) + { + this.config = config; + } + + public Map getPartitionMap() + { + return partitions; + } + + public Collection getPartitions() + { + return partitions.values(); + } + + public Optional getPartition(int partitionId) + { + return Optional.ofNullable(partitions.get(partitionId)); + } + + public Collection getLeaderPartitions(int brokerId) + { + return partitions.values().stream() + .filter(tp -> tp.getLeader() != null && tp.getLeader().getId() == brokerId) + .collect(Collectors.toList()); + } + + public Collection getUnderReplicatedPartitions() + { + return partitions.values().stream() + .filter(TopicPartitionVO::isUnderReplicated) + .collect(Collectors.toList()); + } + + public void setPartitions(Map partitions) + { + this.partitions = partitions; + } + + /** + * Returns the total number of messages published to the topic, ever + * + * @return + */ + public long getTotalSize() + { + return partitions.values().stream() + .map(TopicPartitionVO::getSize) + .reduce(0L, Long::sum); + } + + /** + * Returns the total number of messages available to consume from the topic. + * + * @return + */ + public long getAvailableSize() + { + return partitions.values().stream() + .map(p -> p.getSize() - p.getFirstOffset()) + .reduce(0L, Long::sum); + } + + public double getPreferredReplicaPercent() + { + long preferredLeaderCount = partitions.values().stream() + .filter(TopicPartitionVO::isLeaderPreferred) + .count(); + return ((double) preferredLeaderCount) / ((double) partitions.size()); + } + + public void addPartition(TopicPartitionVO partition) + { + partitions.put(partition.getId(), partition); + } + + @Override + public int compareTo(TopicVO that) + { + return this.name.compareTo(that.name); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + TopicVO that = (TopicVO) o; + + if (!name.equals(that.name)) { return false; } + + return true; + } + + @Override + public int hashCode() + { + return name.hashCode(); + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/service/CuratorKafkaMonitor.java b/src/main/java/com/homeadvisor/kafdrop/service/CuratorKafkaMonitor.java index 84df01b..cf0ca2d 100644 --- a/src/main/java/com/homeadvisor/kafdrop/service/CuratorKafkaMonitor.java +++ b/src/main/java/com/homeadvisor/kafdrop/service/CuratorKafkaMonitor.java @@ -21,23 +21,44 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; -import com.homeadvisor.kafdrop.model.*; +import com.google.common.collect.Lists; +import com.homeadvisor.kafdrop.model.BrokerVO; +import com.homeadvisor.kafdrop.model.ConsumerPartitionVO; +import com.homeadvisor.kafdrop.model.ConsumerRegistrationVO; +import com.homeadvisor.kafdrop.model.ConsumerTopicVO; +import com.homeadvisor.kafdrop.model.ConsumerVO; +import com.homeadvisor.kafdrop.model.MessageVO; +import com.homeadvisor.kafdrop.model.TopicPartitionStateVO; +import com.homeadvisor.kafdrop.model.TopicPartitionVO; +import com.homeadvisor.kafdrop.model.TopicRegistrationVO; +import com.homeadvisor.kafdrop.model.TopicVO; import com.homeadvisor.kafdrop.util.BrokerChannel; import com.homeadvisor.kafdrop.util.Version; import kafka.api.ConsumerMetadataRequest; -import kafka.api.PartitionOffsetRequestInfo; import kafka.cluster.Broker; import kafka.common.ErrorMapping; import kafka.common.TopicAndPartition; -import kafka.javaapi.*; +import kafka.javaapi.ConsumerMetadataResponse; +import kafka.javaapi.OffsetFetchRequest; +import kafka.javaapi.OffsetFetchResponse; +import kafka.javaapi.PartitionMetadata; +import kafka.javaapi.TopicMetadata; import kafka.network.BlockingChannel; import kafka.utils.ZKGroupDirs; import kafka.utils.ZKGroupTopicDirs; import kafka.utils.ZkUtils; import org.apache.commons.lang.StringUtils; import org.apache.curator.framework.CuratorFramework; -import org.apache.curator.framework.recipes.cache.*; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.NodeCache; +import org.apache.curator.framework.recipes.cache.PathChildrenCache; import org.apache.curator.framework.recipes.cache.PathChildrenCache.StartMode; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; +import org.apache.curator.framework.recipes.cache.TreeCache; +import org.apache.curator.framework.recipes.cache.TreeCacheEvent; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.TopicPartition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -45,885 +66,763 @@ import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Service; - +import com.homeadvisor.kafdrop.config.*; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; + import java.io.IOException; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import java.util.stream.LongStream; import java.util.stream.Stream; @Service -public class CuratorKafkaMonitor implements KafkaMonitor +public class CuratorKafkaMonitor + implements KafkaMonitor { - private final Logger LOG = LoggerFactory.getLogger(getClass()); - - @Autowired - private CuratorFramework curatorFramework; - - @Autowired - private ObjectMapper objectMapper; - - private PathChildrenCache brokerPathCache; - private PathChildrenCache topicConfigPathCache; - private TreeCache topicTreeCache; - private TreeCache consumerTreeCache; - private NodeCache controllerNodeCache; - - private int controllerId = -1; - - private Map brokerCache = new TreeMap<>(); - - private AtomicInteger cacheInitCounter = new AtomicInteger(); - - private ForkJoinPool threadPool; - - @Autowired - private CuratorKafkaMonitorProperties properties; - private Version kafkaVersion; - - private RetryTemplate retryTemplate; - - @PostConstruct - public void start() throws Exception - { - try - { - kafkaVersion = new Version(properties.getKafkaVersion()); - } - catch (Exception ex) - { - throw new IllegalStateException("Invalid kafka version: " + properties.getKafkaVersion(), ex); - } - - threadPool = new ForkJoinPool(properties.getThreadPoolSize()); - - FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); - backOffPolicy.setBackOffPeriod(properties.getRetry().getBackoffMillis()); - - final SimpleRetryPolicy retryPolicy = - new SimpleRetryPolicy(properties.getRetry().getMaxAttempts(), - ImmutableMap.of(InterruptedException.class, false, - Exception.class, true)); - - retryTemplate = new RetryTemplate(); - retryTemplate.setBackOffPolicy(backOffPolicy); - retryTemplate.setRetryPolicy(retryPolicy); - - cacheInitCounter.set(4); - - brokerPathCache = new PathChildrenCache(curatorFramework, ZkUtils.BrokerIdsPath(), true); - brokerPathCache.getListenable().addListener(new BrokerListener()); - brokerPathCache.getListenable().addListener((f, e) -> { - if (e.getType() == PathChildrenCacheEvent.Type.INITIALIZED) - { - cacheInitCounter.decrementAndGet(); - LOG.info("Broker cache initialized"); - } - }); - brokerPathCache.start(StartMode.POST_INITIALIZED_EVENT); - - topicConfigPathCache = new PathChildrenCache(curatorFramework, ZkUtils.TopicConfigPath(), true); - topicConfigPathCache.getListenable().addListener((f, e) -> { - if (e.getType() == PathChildrenCacheEvent.Type.INITIALIZED) - { - cacheInitCounter.decrementAndGet(); - LOG.info("Topic configuration cache initialized"); - } - }); - topicConfigPathCache.start(StartMode.POST_INITIALIZED_EVENT); - - topicTreeCache = new TreeCache(curatorFramework, ZkUtils.BrokerTopicsPath()); - topicTreeCache.getListenable().addListener((client, event) -> { - if (event.getType() == TreeCacheEvent.Type.INITIALIZED) - { - cacheInitCounter.decrementAndGet(); - LOG.info("Topic tree cache initialized"); - } - }); - topicTreeCache.start(); - - consumerTreeCache = new TreeCache(curatorFramework, ZkUtils.ConsumersPath()); - consumerTreeCache.getListenable().addListener((client, event) -> { - if (event.getType() == TreeCacheEvent.Type.INITIALIZED) - { - cacheInitCounter.decrementAndGet(); - LOG.info("Consumer tree cache initialized"); - } - }); - consumerTreeCache.start(); - - controllerNodeCache = new NodeCache(curatorFramework, ZkUtils.ControllerPath()); - controllerNodeCache.getListenable().addListener(this::updateController); - controllerNodeCache.start(true); - updateController(); - } - - private String clientId() - { - return properties.getClientId(); - } - - private void updateController() - { - Optional.ofNullable(controllerNodeCache.getCurrentData()) - .map(data -> { - try - { - Map controllerData = objectMapper.reader(Map.class).readValue(data.getData()); - return (Integer) controllerData.get("brokerid"); + private final Logger LOG = LoggerFactory.getLogger(getClass()); + + @Autowired + private CuratorFramework curatorFramework; + + + @Autowired + private ObjectMapper objectMapper; + + private PathChildrenCache brokerPathCache; + private PathChildrenCache topicConfigPathCache; + private TreeCache topicTreeCache; + private TreeCache consumerTreeCache; + private NodeCache controllerNodeCache; + + private int controllerId = -1; + + private Map brokerCache = new TreeMap<>(); + + private AtomicInteger cacheInitCounter = new AtomicInteger(); + + private ForkJoinPool threadPool; + + @Autowired + private CuratorKafkaMonitorProperties properties; + private Version kafkaVersion; + + private RetryTemplate retryTemplate; + @Autowired + + private KafkaHighLevelConsumer kafkaHighLevelConsumer; + + @PostConstruct + public void start() + throws Exception + { + try { + kafkaVersion = new Version(properties.getKafkaVersion()); + } + catch (Exception ex) { + throw new IllegalStateException("Invalid kafka version: " + properties.getKafkaVersion(), ex); + } + + threadPool = new ForkJoinPool(properties.getThreadPoolSize()); + + FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(properties.getRetry().getBackoffMillis()); + + final SimpleRetryPolicy retryPolicy = + new SimpleRetryPolicy(properties.getRetry().getMaxAttempts(), + ImmutableMap.of(InterruptedException.class, false, + Exception.class, true)); + + retryTemplate = new RetryTemplate(); + retryTemplate.setBackOffPolicy(backOffPolicy); + retryTemplate.setRetryPolicy(retryPolicy); + + cacheInitCounter.set(4); + + brokerPathCache = new PathChildrenCache(curatorFramework, ZkUtils.BrokerIdsPath(), true); + brokerPathCache.getListenable().addListener(new BrokerListener()); + brokerPathCache.getListenable().addListener((f, e) -> { + if (e.getType() == PathChildrenCacheEvent.Type.INITIALIZED) { + cacheInitCounter.decrementAndGet(); + LOG.info("Broker cache initialized"); } - catch (IOException e) - { - LOG.error("Unable to read controller data", e); - return null; + }); + brokerPathCache.start(StartMode.POST_INITIALIZED_EVENT); + + topicConfigPathCache = new PathChildrenCache(curatorFramework, ZkUtils.TopicConfigPath(), true); + topicConfigPathCache.getListenable().addListener((f, e) -> { + if (e.getType() == PathChildrenCacheEvent.Type.INITIALIZED) { + cacheInitCounter.decrementAndGet(); + LOG.info("Topic configuration cache initialized"); } - }) - .ifPresent(this::updateController); - } - - private void updateController(int brokerId) - { - brokerCache.values() - .forEach(broker -> broker.setController(broker.getId() == brokerId)); - } - - private void validateInitialized() - { - if (cacheInitCounter.get() > 0) - { - throw new NotInitializedException(); - } - } - - - @PreDestroy - public void stop() throws IOException - { - consumerTreeCache.close(); - topicConfigPathCache.close(); - brokerPathCache.close(); - controllerNodeCache.close(); - } - - private int brokerId(ChildData input) - { - return Integer.parseInt(StringUtils.substringAfter(input.getPath(), ZkUtils.BrokerIdsPath() + "/")); - } - - private BrokerVO addBroker(BrokerVO broker) - { - final BrokerVO oldBroker = brokerCache.put(broker.getId(), broker); - LOG.info("Kafka broker {} was {}", broker.getId(), oldBroker == null ? "added" : "updated"); - return oldBroker; - } - - private BrokerVO removeBroker(int brokerId) - { - final BrokerVO broker = brokerCache.remove(brokerId); - LOG.info("Kafka broker {} was removed", broker.getId()); - return broker; - } - - @Override - public List getBrokers() - { - validateInitialized(); - return brokerCache.values().stream().collect(Collectors.toList()); - } - - @Override - public Optional getBroker(int id) - { - validateInitialized(); - return Optional.ofNullable(brokerCache.get(id)); - } - - private BrokerChannel brokerChannel(Integer brokerId) - { - if (brokerId == null) - { - brokerId = randomBroker(); - if (brokerId == null) - { - throw new BrokerNotFoundException("No brokers available to select from"); - } - } - - Integer finalBrokerId = brokerId; - BrokerVO broker = getBroker(brokerId) - .orElseThrow(() -> new BrokerNotFoundException("Broker " + finalBrokerId + " is not available")); - - return BrokerChannel.forBroker(broker.getHost(), broker.getPort()); - } - - private Integer randomBroker() - { - if (brokerCache.size() > 0) - { - List brokerIds = brokerCache.keySet().stream().collect(Collectors.toList()); - Collections.shuffle(brokerIds); - return brokerIds.get(0); - } - else - { - return null; - } - } - - public ClusterSummaryVO getClusterSummary() - { - return getClusterSummary(getTopics()); - } - - @Override - public ClusterSummaryVO getClusterSummary(Collection topics) { - final ClusterSummaryVO topicSummary = topics.stream() - .map(topic -> { - ClusterSummaryVO summary = new ClusterSummaryVO(); - summary.setPartitionCount(topic.getPartitions().size()); - summary.setUnderReplicatedCount(topic.getUnderReplicatedPartitions().size()); - summary.setPreferredReplicaPercent(topic.getPreferredReplicaPercent()); - topic.getPartitions() - .forEach(partition -> { - if (partition.getLeader() != null) { - summary.addBrokerLeaderPartition(partition.getLeader().getId()); - } - if (partition.getPreferredLeader() != null) { - summary.addBrokerPreferredLeaderPartition(partition.getPreferredLeader().getId()); - } - partition.getReplicas() - .forEach(replica -> summary.addExpectedBrokerId(replica.getId())); - }); - return summary; - }) - .reduce((s1, s2) -> { - s1.setPartitionCount(s1.getPartitionCount() + s2.getPartitionCount()); - s1.setUnderReplicatedCount(s1.getUnderReplicatedCount() + s2.getUnderReplicatedCount()); - s1.setPreferredReplicaPercent(s1.getPreferredReplicaPercent() + s2.getPreferredReplicaPercent()); - s2.getBrokerLeaderPartitionCount().forEach(s1::addBrokerLeaderPartition); - s2.getBrokerPreferredLeaderPartitionCount().forEach(s1::addBrokerPreferredLeaderPartition); - return s1; - }) - .orElseGet(ClusterSummaryVO::new); - topicSummary.setTopicCount(topics.size()); - topicSummary.setPreferredReplicaPercent(topicSummary.getPreferredReplicaPercent() / topics.size()); - return topicSummary; - } - - @Override - public List getTopics() - { - validateInitialized(); - return getTopicMetadata().values().stream() - .sorted(Comparator.comparing(TopicVO::getName)) - .collect(Collectors.toList()); - } - - @Override - public Optional getTopic(String topic) - { - validateInitialized(); - final Optional topicVO = Optional.ofNullable(getTopicMetadata(topic).get(topic)); - topicVO.ifPresent( - vo -> { - getTopicPartitionSizes(vo, kafka.api.OffsetRequest.LatestTime()) - .entrySet() - .forEach(entry -> vo.getPartition(entry.getKey()).ifPresent(p -> p.setSize(entry.getValue()))); - getTopicPartitionSizes(vo, kafka.api.OffsetRequest.EarliestTime()) - .entrySet() - .forEach(entry -> vo.getPartition(entry.getKey()).ifPresent(p -> p.setFirstOffset(entry.getValue()))); - } - ); - return topicVO; - } - - private Map getTopicMetadata(String... topics) - { - if (kafkaVersion.compareTo(new Version(0, 9, 0)) >= 0) - { - return retryTemplate.execute( - context -> brokerChannel(null) - .execute(channel -> getTopicMetadata(channel, topics))); - } - else - { - Stream topicStream; - if (topics == null || topics.length == 0) - { - topicStream = - Optional.ofNullable( - topicTreeCache.getCurrentChildren(ZkUtils.BrokerTopicsPath())) - .map(Map::keySet) - .map(Collection::stream) - .orElse(Stream.empty()); - } - else - { - topicStream = Stream.of(topics); - } - - return topicStream - .map(this::getTopicZkData) - .filter(Objects::nonNull) - .collect(Collectors.toMap(TopicVO::getName, topic -> topic)); - } - } - - private TopicVO getTopicZkData(String topic) - { - return Optional.ofNullable(topicTreeCache.getCurrentData(ZkUtils.getTopicPath(topic))) - .map(this::parseZkTopic) - .orElse(null); - } - - public TopicVO parseZkTopic(ChildData input) - { - try - { - final TopicVO topic = new TopicVO(StringUtils.substringAfterLast(input.getPath(), "/")); - - final TopicRegistrationVO topicRegistration = - objectMapper.reader(TopicRegistrationVO.class).readValue(input.getData()); - - topic.setConfig( - Optional.ofNullable(topicConfigPathCache.getCurrentData(ZkUtils.TopicConfigPath() + "/" + topic.getName())) - .map(this::readTopicConfig) - .orElse(Collections.emptyMap())); - - for (Map.Entry> entry : topicRegistration.getReplicas().entrySet()) - { - final int partitionId = entry.getKey(); - final List partitionBrokerIds = entry.getValue(); - - final TopicPartitionVO partition = new TopicPartitionVO(partitionId); - - final Optional partitionState = partitionState(topic.getName(), partition.getId()); - - partitionBrokerIds.stream() - .map(brokerId -> { - TopicPartitionVO.PartitionReplica replica = new TopicPartitionVO.PartitionReplica(); - replica.setId(brokerId); - replica.setInService(partitionState.map(ps -> ps.getIsr().contains(brokerId)).orElse(false)); - replica.setLeader(partitionState.map(ps -> brokerId == ps.getLeader()).orElse(false)); - return replica; - }) - .forEach(partition::addReplica); - - topic.addPartition(partition); - } - - // todo: get partition sizes here as single bulk request? - - return topic; - } - catch (IOException e) - { - throw Throwables.propagate(e); - } - } - - private Map getTopicMetadata(BlockingChannel channel, String... topics) - { - final TopicMetadataRequest request = - new TopicMetadataRequest((short) 0, 0, clientId(), Arrays.asList(topics)); - - LOG.debug("Sending topic metadata request: {}", request); - - channel.send(request); - final kafka.api.TopicMetadataResponse underlyingResponse = - kafka.api.TopicMetadataResponse.readFrom(channel.receive().buffer()); - - LOG.debug("Received topic metadata response: {}", underlyingResponse); - - TopicMetadataResponse response = new TopicMetadataResponse(underlyingResponse); - return response.topicsMetadata().stream() - .filter(tmd -> tmd.errorCode() == ErrorMapping.NoError()) - .map(this::processTopicMetadata) - .collect(Collectors.toMap(TopicVO::getName, t -> t)); - } - - private TopicVO processTopicMetadata(TopicMetadata tmd) - { - TopicVO topic = new TopicVO(tmd.topic()); - - topic.setConfig( - Optional.ofNullable(topicConfigPathCache.getCurrentData(ZkUtils.TopicConfigPath() + "/" + topic.getName())) - .map(this::readTopicConfig) - .orElse(Collections.emptyMap())); - - topic.setPartitions( - tmd.partitionsMetadata().stream() - .map((pmd) -> parsePartitionMetadata(tmd.topic(), pmd)) - .collect(Collectors.toMap(TopicPartitionVO::getId, p -> p)) - ); - return topic; - } - - private TopicPartitionVO parsePartitionMetadata(String topic, PartitionMetadata pmd) - { - TopicPartitionVO partition = new TopicPartitionVO(pmd.partitionId()); - if (pmd.leader() != null) - { - partition.addReplica(new TopicPartitionVO.PartitionReplica(pmd.leader().id(), true, true)); - } - - final List isr = getIsr(topic, pmd); - pmd.replicas().stream() - .map(replica -> new TopicPartitionVO.PartitionReplica(replica.id(), isr.contains(replica.id()), false)) - .forEach(partition::addReplica); - return partition; - } - - private List getIsr(String topic, PartitionMetadata pmd) - { - return pmd.isr().stream().map(Broker::id).collect(Collectors.toList()); - } - - private Map readTopicConfig(ChildData d) - { - try - { - final Map configData = objectMapper.reader(Map.class).readValue(d.getData()); - return (Map) configData.get("config"); - } - catch (IOException e) - { - throw Throwables.propagate(e); - } - } - - - private Optional partitionState(String topicName, int partitionId) - throws IOException - { - final Optional partitionData = Optional.ofNullable(topicTreeCache.getCurrentData( - ZkUtils.getTopicPartitionLeaderAndIsrPath(topicName, partitionId))) - .map(ChildData::getData); - if (partitionData.isPresent()) - { - return Optional.ofNullable(objectMapper.reader(TopicPartitionStateVO.class).readValue(partitionData.get())); - } - else - { - return Optional.empty(); - } - } - - @Override - public List getConsumers() - { - validateInitialized(); - return getConsumerStream(null).collect(Collectors.toList()); - } - - @Override - public List getConsumers(final TopicVO topic) - { - validateInitialized(); - return getConsumerStream(topic) - .filter(consumer -> consumer.getTopic(topic.getName()) != null) - .collect(Collectors.toList()); - } - - @Override - public List getConsumers(final String topic) - { - return getConsumers(getTopic(topic).get()); - } - - private Stream getConsumerStream(TopicVO topic) - { - return consumerTreeCache.getCurrentChildren(ZkUtils.ConsumersPath()).keySet().stream() - .map(g -> getConsumerByTopic(g, topic)) - .filter(Optional::isPresent) - .map(Optional::get) - .sorted(Comparator.comparing(ConsumerVO::getGroupId)); - } - - @Override - public Optional getConsumer(String groupId) - { - validateInitialized(); - return getConsumerByTopic(groupId, null); - } - - @Override - public Optional getConsumerByTopicName(String groupId, String topicName) - { - return getConsumerByTopic(groupId, Optional.of(topicName).flatMap(this::getTopic).orElse(null)); - } - - @Override - public Optional getConsumerByTopic(String groupId, TopicVO topic) - { - final ConsumerVO consumer = new ConsumerVO(groupId); - final ZKGroupDirs groupDirs = new ZKGroupDirs(groupId); - - if (consumerTreeCache.getCurrentData(groupDirs.consumerGroupDir()) == null) return Optional.empty(); - - // todo: get number of threads in each instance (subscription -> topic -> # threads) - Optional.ofNullable(consumerTreeCache.getCurrentChildren(groupDirs.consumerRegistryDir())) - .ifPresent( - children -> - children.keySet().stream() - .map(id -> readConsumerRegistration(groupDirs, id)) - .forEach(consumer::addActiveInstance)); - - Stream topicStream = null; - - if (topic != null) - { - if (consumerTreeCache.getCurrentData(groupDirs.consumerGroupDir() + "/owners/" + topic.getName()) != null) - { - topicStream = Stream.of(topic.getName()); - } - else - { - topicStream = Stream.empty(); - } - } - else - { - topicStream = Optional.ofNullable( - consumerTreeCache.getCurrentChildren(groupDirs.consumerGroupDir() + "/owners")) - .map(Map::keySet) - .map(Collection::stream) - .orElse(Stream.empty()); - } - - topicStream - .map(ConsumerTopicVO::new) - .forEach(consumerTopic -> { - getConsumerPartitionStream(groupId, consumerTopic.getTopic(), topic) - .forEach(consumerTopic::addOffset); - consumer.addTopic(consumerTopic); - }); - - return Optional.of(consumer); - } - - private ConsumerRegistrationVO readConsumerRegistration(ZKGroupDirs groupDirs, String id) - { - try - { - ChildData data = consumerTreeCache.getCurrentData(groupDirs.consumerRegistryDir() + "/" + id); - final Map consumerData = objectMapper.reader(Map.class).readValue(data.getData()); - Map subscriptions = (Map) consumerData.get("subscription"); - - ConsumerRegistrationVO vo = new ConsumerRegistrationVO(id); - vo.setSubscriptions(subscriptions); - return vo; - } - catch (IOException ex) - { - throw Throwables.propagate(ex); - } - } - - private Stream getConsumerPartitionStream(String groupId, - String topicName, - TopicVO topicOpt) - { - ZKGroupTopicDirs groupTopicDirs = new ZKGroupTopicDirs(groupId, topicName); - - if (topicOpt == null || topicOpt.getName().equals(topicName)) - { - topicOpt = getTopic(topicName).orElse(null); - } - - if (topicOpt != null) - { - final TopicVO topic = topicOpt; - - Map consumerOffsets = getConsumerOffsets(groupId, topic); - - return topic.getPartitions().stream() - .map(partition -> { - int partitionId = partition.getId(); - - final ConsumerPartitionVO consumerPartition = new ConsumerPartitionVO(groupId, topicName, partitionId); - consumerPartition.setOwner( - Optional.ofNullable( - consumerTreeCache.getCurrentData(groupTopicDirs.consumerOwnerDir() + "/" + partitionId)) - .map(data -> new String(data.getData())) - .orElse(null)); - - consumerPartition.setOffset(consumerOffsets.getOrDefault(partitionId, -1L)); - - final Optional topicPartition = topic.getPartition(partitionId); - consumerPartition.setSize(topicPartition.map(TopicPartitionVO::getSize).orElse(-1L)); - consumerPartition.setFirstOffset(topicPartition.map(TopicPartitionVO::getFirstOffset).orElse(-1L)); - - return consumerPartition; - }); - } - else - { - return Stream.empty(); - } - } - - private Map getConsumerOffsets(String groupId, TopicVO topic) - { - try - { - // Kafka doesn't really give us an indication of whether a consumer is - // using Kafka or Zookeeper based offset tracking. So look up the offsets - // for both and assume that the largest offset is the correct one. - - ForkJoinTask> kafkaTask = - threadPool.submit(() -> getConsumerOffsets(groupId, topic, false)); - - ForkJoinTask> zookeeperTask = - threadPool.submit(() -> getConsumerOffsets(groupId, topic, true)); - - Map zookeeperOffsets = zookeeperTask.get(); - Map kafkaOffsets = kafkaTask.get(); - zookeeperOffsets.entrySet() - .forEach(entry -> kafkaOffsets.merge(entry.getKey(), entry.getValue(), Math::max)); - return kafkaOffsets; - } - catch (InterruptedException ex) - { - Thread.currentThread().interrupt(); - throw Throwables.propagate(ex); - } - catch (ExecutionException ex) - { - throw Throwables.propagate(ex.getCause()); - } - } - - private Map getConsumerOffsets(String groupId, - TopicVO topic, - boolean zookeeperOffsets) - { - return retryTemplate.execute( - context -> brokerChannel(zookeeperOffsets ? null : offsetManagerBroker(groupId)) - .execute(channel -> getConsumerOffsets(channel, groupId, topic, zookeeperOffsets))); - } - - /** - * Returns the map of partitionId to consumer offset for the given group and - * topic. Uses the given blocking channel to execute the offset fetch request. - * - * @param channel The channel to send requests on - * @param groupId Consumer group to use - * @param topic Topic to query - * @param zookeeperOffsets If true, use a version of the API that retrieves - * offsets from Zookeeper. Otherwise use a version - * that pulls the offsets from Kafka itself. - * @return Map where the key is partitionId and the value is the consumer - * offset for that partition. - */ - private Map getConsumerOffsets(BlockingChannel channel, - String groupId, - TopicVO topic, - boolean zookeeperOffsets) - { - - final OffsetFetchRequest request = new OffsetFetchRequest( - groupId, - topic.getPartitions().stream() - .map(p -> new TopicAndPartition(topic.getName(), p.getId())) - .collect(Collectors.toList()), - (short) (zookeeperOffsets ? 0 : 1), 0, // version 0 = zookeeper offsets, 1 = kafka offsets - clientId()); - - LOG.debug("Sending consumer offset request: {}", request); - - channel.send(request.underlying()); - - final kafka.api.OffsetFetchResponse underlyingResponse = - kafka.api.OffsetFetchResponse.readFrom(channel.receive().buffer()); - - LOG.debug("Received consumer offset response: {}", underlyingResponse); - - OffsetFetchResponse response = new OffsetFetchResponse(underlyingResponse); - - return response.offsets().entrySet().stream() - .filter(entry -> entry.getValue().error() == ErrorMapping.NoError()) - .collect(Collectors.toMap(entry -> entry.getKey().partition(), entry -> entry.getValue().offset())); - } - - /** - * Returns the broker Id that is the offset coordinator for the given group id. If not found, returns null - */ - private Integer offsetManagerBroker(String groupId) - { - return retryTemplate.execute( - context -> - brokerChannel(null) - .execute(channel -> offsetManagerBroker(channel, groupId)) - ); - } - - private Integer offsetManagerBroker(BlockingChannel channel, String groupId) - { - final ConsumerMetadataRequest request = - new ConsumerMetadataRequest(groupId, (short) 0, 0, clientId()); - - LOG.debug("Sending consumer metadata request: {}", request); - - channel.send(request); - ConsumerMetadataResponse response = - ConsumerMetadataResponse.readFrom(channel.receive().buffer()); - - LOG.debug("Received consumer metadata response: {}", response); - - return (response.errorCode() == ErrorMapping.NoError()) ? response.coordinator().id() : null; - } - - private Map getTopicPartitionSizes(TopicVO topic) - { - return getTopicPartitionSizes(topic, kafka.api.OffsetRequest.LatestTime()); - } - - private Map getTopicPartitionSizes(TopicVO topic, long time) - { - try - { - PartitionOffsetRequestInfo requestInfo = new PartitionOffsetRequestInfo(time, 1); - - return threadPool.submit(() -> - topic.getPartitions().parallelStream() - .filter(p -> p.getLeader() != null) - .collect(Collectors.groupingBy(p -> p.getLeader().getId())) // Group partitions by leader broker id - .entrySet().parallelStream() - .map(entry -> { - final Integer brokerId = entry.getKey(); - final List brokerPartitions = entry.getValue(); - try - { - // Get the size of the partitions for a topic from the leader. - final OffsetResponse offsetResponse = - sendOffsetRequest(brokerId, topic, requestInfo, brokerPartitions); - - - // Build a map of partitionId -> topic size from the response - return brokerPartitions.stream() - .collect(Collectors.toMap(TopicPartitionVO::getId, - partition -> Optional.ofNullable( - offsetResponse.offsets(topic.getName(), partition.getId())) - .map(Arrays::stream) - .orElse(LongStream.empty()) - .findFirst() - .orElse(-1L))); - } - catch (Exception ex) - { - LOG.error("Unable to get partition log size for topic {} partitions ({})", - topic.getName(), - brokerPartitions.stream() - .map(TopicPartitionVO::getId) - .map(String::valueOf) - .collect(Collectors.joining(",")), - ex); - - // Map each partition to -1, indicating we got an error - return brokerPartitions.stream().collect(Collectors.toMap(TopicPartitionVO::getId, tp -> -1L)); - } - }) - .map(Map::entrySet) - .flatMap(Collection::stream) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) - .get(); - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - throw Throwables.propagate(e); - } - catch (ExecutionException e) - { - throw Throwables.propagate(e.getCause()); - } - } - - private OffsetResponse sendOffsetRequest(Integer brokerId, TopicVO topic, - PartitionOffsetRequestInfo requestInfo, - List brokerPartitions) - { - final OffsetRequest offsetRequest = new OffsetRequest( - brokerPartitions.stream() - .collect(Collectors.toMap( - partition -> new TopicAndPartition(topic.getName(), partition.getId()), - partition -> requestInfo)), - (short) 0, clientId()); - - LOG.debug("Sending offset request: {}", offsetRequest); - - return retryTemplate.execute( - context -> - brokerChannel(brokerId) - .execute(channel -> - { - channel.send(offsetRequest.underlying()); - final kafka.api.OffsetResponse underlyingResponse = - kafka.api.OffsetResponse.readFrom(channel.receive().buffer()); - - LOG.debug("Received offset response: {}", underlyingResponse); - - return new OffsetResponse(underlyingResponse); - })); - } - - private class BrokerListener implements PathChildrenCacheListener - { - @Override - public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception - { - switch (event.getType()) - { - case CHILD_REMOVED: - { - BrokerVO broker = removeBroker(brokerId(event.getData())); - break; + }); + topicConfigPathCache.start(StartMode.POST_INITIALIZED_EVENT); + + topicTreeCache = new TreeCache(curatorFramework, ZkUtils.BrokerTopicsPath()); + topicTreeCache.getListenable().addListener((client, event) -> { + if (event.getType() == TreeCacheEvent.Type.INITIALIZED) { + cacheInitCounter.decrementAndGet(); + LOG.info("Topic tree cache initialized"); } - - case CHILD_ADDED: - case CHILD_UPDATED: - { - addBroker(parseBroker(event.getData())); - break; + }); + topicTreeCache.start(); + + consumerTreeCache = new TreeCache(curatorFramework, ZkUtils.ConsumersPath()); + consumerTreeCache.getListenable().addListener((client, event) -> { + if (event.getType() == TreeCacheEvent.Type.INITIALIZED) { + cacheInitCounter.decrementAndGet(); + LOG.info("Consumer tree cache initialized"); + } + }); + consumerTreeCache.start(); + + controllerNodeCache = new NodeCache(curatorFramework, ZkUtils.ControllerPath()); + controllerNodeCache.getListenable().addListener(this::updateController); + controllerNodeCache.start(true); + updateController(); + } + + private String clientId() + { + return properties.getClientId(); + } + + private void updateController() + { + Optional.ofNullable(controllerNodeCache.getCurrentData()) + .map(data -> { + try { + Map controllerData = objectMapper.reader(Map.class).readValue(data.getData()); + return (Integer) controllerData.get("brokerid"); + } + catch (IOException e) { + LOG.error("Unable to read controller data", e); + return null; + } + }) + .ifPresent(this::updateController); + } + + private void updateController(int brokerId) + { + brokerCache.values() + .forEach(broker -> broker.setController(broker.getId() == brokerId)); + } + + private void validateInitialized() + { + if (cacheInitCounter.get() > 0) { + throw new NotInitializedException(); + } + } + + @PreDestroy + public void stop() + throws IOException + { + consumerTreeCache.close(); + topicConfigPathCache.close(); + brokerPathCache.close(); + controllerNodeCache.close(); + } + + private int brokerId(ChildData input) + { + return Integer.parseInt(StringUtils.substringAfter(input.getPath(), ZkUtils.BrokerIdsPath() + "/")); + } + + private BrokerVO addBroker(BrokerVO broker) + { + final BrokerVO oldBroker = brokerCache.put(broker.getId(), broker); + LOG.info("Kafka broker {} was {}", broker.getId(), oldBroker == null ? "added" : "updated"); + return oldBroker; + } + + private BrokerVO removeBroker(int brokerId) + { + final BrokerVO broker = brokerCache.remove(brokerId); + LOG.info("Kafka broker {} was removed", broker.getId()); + return broker; + } + + @Override + public List getBrokers() + { + validateInitialized(); + List vo = brokerCache.values().stream().collect(Collectors.toList()); + return brokerCache.values().stream().collect(Collectors.toList()); + } + + @Override + public Optional getBroker(int id) + { + validateInitialized(); + return Optional.ofNullable(brokerCache.get(id)); + } + + private BrokerChannel brokerChannel(Integer brokerId) + { + if (brokerId == null) { + brokerId = randomBroker(); + if (brokerId == null) { + throw new BrokerNotFoundException("No brokers available to select from"); + } + } + + Integer finalBrokerId = brokerId; + BrokerVO broker = getBroker(brokerId) + .orElseThrow(() -> new BrokerNotFoundException("Broker " + finalBrokerId + " is not available")); + + return BrokerChannel.forBroker(broker.getHost(), broker.getPort()); + } + + private Integer randomBroker() + { + if (brokerCache.size() > 0) { + List brokerIds = brokerCache.keySet().stream().collect(Collectors.toList()); + Collections.shuffle(brokerIds); + return brokerIds.get(0); + } + else { + return null; + } + } + + @Override + public List getTopics() + { + validateInitialized(); + return getTopicMetadata().values().stream() + .sorted(Comparator.comparing(TopicVO::getName)) + .collect(Collectors.toList()); + } + + @Override + public Optional + getTopic(String topic) + { + validateInitialized(); + Map topicVoMap = getTopicMetadata(topic); + TopicVO topicVO = null; + if (topicVoMap.containsKey(topic)) { + topicVO = topicVoMap.get(topic); + topicVO.setPartitions(getTopicPartitionSizes(topicVO)); + } + + return Optional.of(topicVO); + } + + private Map getTopicMetadata(String... topics) + { + if (kafkaVersion.compareTo(new Version(0, 9, 0)) >= 0) { + return retryTemplate.execute( + context -> brokerChannel(null) + .execute(channel -> getTopicMetadata(channel, topics))); + } + else { + Stream topicStream; + if (topics == null || topics.length == 0) { + topicStream = + Optional.ofNullable( + topicTreeCache.getCurrentChildren(ZkUtils.BrokerTopicsPath())) + .map(Map::keySet) + .map(Collection::stream) + .orElse(Stream.empty()); + } + else { + topicStream = Stream.of(topics); } - case INITIALIZED: - { - brokerPathCache.getCurrentData().stream() - .map(BrokerListener.this::parseBroker) - .forEach(CuratorKafkaMonitor.this::addBroker); - break; + return topicStream + .map(this::getTopicZkData) + .filter(Objects::nonNull) + .collect(Collectors.toMap(TopicVO::getName, topic -> topic)); + } + } + + private TopicVO getTopicZkData(String topic) + { + return Optional.ofNullable(topicTreeCache.getCurrentData(ZkUtils.getTopicPath(topic))) + .map(this::parseZkTopic) + .orElse(null); + } + + public TopicVO parseZkTopic(ChildData input) + { + try { + final TopicVO topic = new TopicVO(StringUtils.substringAfterLast(input.getPath(), "/")); + + final TopicRegistrationVO topicRegistration = + objectMapper.reader(TopicRegistrationVO.class).readValue(input.getData()); + + topic.setConfig( + Optional.ofNullable(topicConfigPathCache.getCurrentData(ZkUtils.TopicConfigPath() + "/" + topic.getName())) + .map(this::readTopicConfig) + .orElse(Collections.emptyMap())); + + for (Map.Entry> entry : topicRegistration.getReplicas().entrySet()) { + final int partitionId = entry.getKey(); + try { + final List partitionBrokerIds = entry.getValue(); + + final TopicPartitionVO partition = new TopicPartitionVO(partitionId); + + final TopicPartitionStateVO partitionState = partitionState(topic.getName(), partition.getId()); + + partitionBrokerIds.stream() + .map(brokerId -> { + TopicPartitionVO.PartitionReplica replica = new TopicPartitionVO.PartitionReplica(); + replica.setId(brokerId); + replica.setInService(partitionState.getIsr().contains(brokerId)); + replica.setLeader(brokerId == partitionState.getLeader()); + return replica; + }) + .forEach(partition::addReplica); + + topic.addPartition(partition); + } + catch (NullPointerException e) { + LOG.error("Unable to get partitionState for partitionId: {} and topic: {}", partitionId, topic.getName()); + } } - } - updateController(); - } - - private int brokerId(ChildData input) - { - return Integer.parseInt(StringUtils.substringAfter(input.getPath(), ZkUtils.BrokerIdsPath() + "/")); - } - - - private BrokerVO parseBroker(ChildData input) - { - try - { - final BrokerVO broker = objectMapper.reader(BrokerVO.class).readValue(input.getData()); - broker.setId(brokerId(input)); - return broker; - } - catch (IOException e) - { - throw Throwables.propagate(e); - } - } - } + // todo: get partition sizes here as single bulk request? + + return topic; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private Map getTopicMetadata(BlockingChannel channel, String... topics) + { + + return kafkaHighLevelConsumer.getTopicsInfo(topics); + + + /*final TopicMetadataRequest request = + new TopicMetadataRequest((short) 0, 0, clientId(), Arrays.asList(topics)); + + LOG.debug("Sending topic metadata request: {}", request); + + channel.send(request); + final kafka.api.TopicMetadataResponse underlyingResponse = + kafka.api.TopicMetadataResponse.readFrom(channel.receive().buffer()); + + LOG.debug("Received topic metadata response: {}", underlyingResponse); + + TopicMetadataResponse response = new TopicMetadataResponse(underlyingResponse); + return response.topicsMetadata().stream() + .filter(tmd -> tmd.errorCode() == ErrorMapping.NoError()) + .map(this::processTopicMetadata) + .collect(Collectors.toMap(TopicVO::getName, t -> t));*/ + } + + private TopicVO processTopicMetadata(TopicMetadata tmd) + { + TopicVO topic = new TopicVO(tmd.topic()); + + topic.setConfig( + Optional.ofNullable(topicConfigPathCache.getCurrentData(ZkUtils.TopicConfigPath() + "/" + topic.getName())) + .map(this::readTopicConfig) + .orElse(Collections.emptyMap())); + + topic.setPartitions( + tmd.partitionsMetadata().stream() + .map((pmd) -> parsePartitionMetadata(tmd.topic(), pmd)) + .collect(Collectors.toMap(TopicPartitionVO::getId, p -> p)) + ); + return topic; + } + + private TopicPartitionVO parsePartitionMetadata(String topic, PartitionMetadata pmd) + { + TopicPartitionVO partition = new TopicPartitionVO(pmd.partitionId()); + if (pmd.leader() != null) { + partition.addReplica(new TopicPartitionVO.PartitionReplica(pmd.leader().id(), true, true)); + } + + final List isr = getIsr(topic, pmd); + pmd.replicas().stream() + .map(replica -> new TopicPartitionVO.PartitionReplica(replica.id(), isr.contains(replica.id()), false)) + .forEach(partition::addReplica); + return partition; + } + + private List getIsr(String topic, PartitionMetadata pmd) + { + return pmd.isr().stream().map(Broker::id).collect(Collectors.toList()); + } + + private Map readTopicConfig(ChildData d) + { + try { + final Map configData = objectMapper.reader(Map.class).readValue(d.getData()); + return (Map) configData.get("config"); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private TopicPartitionStateVO partitionState(String topicName, int partitionId) + throws IOException + { + return objectMapper.reader(TopicPartitionStateVO.class).readValue( + topicTreeCache.getCurrentData( + ZkUtils.getTopicPartitionLeaderAndIsrPath(topicName, partitionId)) + .getData()); + } + + @Override + public List getConsumers() + { + validateInitialized(); + return getConsumerStream(null).collect(Collectors.toList()); + } + + @Override + public List getConsumers(final TopicVO topic) + { + validateInitialized(); + return getConsumerStream(topic) + .filter(consumer -> consumer.getTopic(topic.getName()) != null) + .collect(Collectors.toList()); + } + + @Override + public List getConsumers(final String topic) + { + return getConsumers(getTopic(topic).get()); + } + + private Stream getConsumerStream(TopicVO topic) + { + return consumerTreeCache.getCurrentChildren(ZkUtils.ConsumersPath()).keySet().stream() + .map(g -> getConsumerByTopic(g, topic)) + .filter(Optional::isPresent) + .map(Optional::get) + .sorted(Comparator.comparing(ConsumerVO::getGroupId)); + } + + @Override + public Optional getConsumer(String groupId) + { + validateInitialized(); + return getConsumerByTopic(groupId, null); + } + + @Override + public Optional getConsumerByTopicName(String groupId, String topicName) + { + return getConsumerByTopic(groupId, Optional.of(topicName).flatMap(this::getTopic).orElse(null)); + } + + @Override + public Optional getConsumerByTopic(String groupId, TopicVO topic) + { + final ConsumerVO consumer = new ConsumerVO(groupId); + final ZKGroupDirs groupDirs = new ZKGroupDirs(groupId); + + if (consumerTreeCache.getCurrentData(groupDirs.consumerGroupDir()) == null) { return Optional.empty(); } + + // todo: get number of threads in each instance (subscription -> topic -> # threads) + Optional.ofNullable(consumerTreeCache.getCurrentChildren(groupDirs.consumerRegistryDir())) + .ifPresent( + children -> + children.keySet().stream() + .map(id -> readConsumerRegistration(groupDirs, id)) + .forEach(consumer::addActiveInstance)); + + Stream topicStream = null; + + if (topic != null) { + if (consumerTreeCache.getCurrentData(groupDirs.consumerGroupDir() + "/owners/" + topic.getName()) != null) { + topicStream = Stream.of(topic.getName()); + } + else { + topicStream = Stream.empty(); + } + } + else { + topicStream = Optional.ofNullable( + consumerTreeCache.getCurrentChildren(groupDirs.consumerGroupDir() + "/owners")) + .map(Map::keySet) + .map(Collection::stream) + .orElse(Stream.empty()); + } + + topicStream + .map(ConsumerTopicVO::new) + .forEach(consumerTopic -> { + getConsumerPartitionStream(groupId, consumerTopic.getTopic(), topic) + .forEach(consumerTopic::addOffset); + consumer.addTopic(consumerTopic); + }); + + return Optional.of(consumer); + } + + @Override + public List getMessages(TopicPartition topicPartition, long offset,long count) + { + + List> records = kafkaHighLevelConsumer.getLatestRecords(topicPartition, offset,count); + List messageVOS = Lists.newArrayList(); + for (ConsumerRecord record : records) { + MessageVO messageVo = new MessageVO(); + messageVo.setKey(record.key()); + messageVo.setMessage(record.value()); + messageVo.setChecksum(record.checksum()); + messageVo.setCompressionCodec(record.headers().toString()); + messageVo.setValid(true); + + messageVOS.add(messageVo); + } + return messageVOS; + } + + private ConsumerRegistrationVO readConsumerRegistration(ZKGroupDirs groupDirs, String id) + { + try { + ChildData data = consumerTreeCache.getCurrentData(groupDirs.consumerRegistryDir() + "/" + id); + final Map consumerData = objectMapper.reader(Map.class).readValue(data.getData()); + Map subscriptions = (Map) consumerData.get("subscription"); + + ConsumerRegistrationVO vo = new ConsumerRegistrationVO(id); + vo.setSubscriptions(subscriptions); + return vo; + } + catch (IOException ex) { + throw Throwables.propagate(ex); + } + } + + private Stream getConsumerPartitionStream(String groupId, + String topicName, + TopicVO topicOpt) + { + ZKGroupTopicDirs groupTopicDirs = new ZKGroupTopicDirs(groupId, topicName); + + if (topicOpt == null || topicOpt.getName().equals(topicName)) { + topicOpt = getTopic(topicName).orElse(null); + } + + if (topicOpt != null) { + final TopicVO topic = topicOpt; + + Map consumerOffsets = getConsumerOffsets(groupId, topic); + + return topic.getPartitions().stream() + .map(partition -> { + int partitionId = partition.getId(); + + final ConsumerPartitionVO consumerPartition = new ConsumerPartitionVO(groupId, topicName, partitionId); + consumerPartition.setOwner( + Optional.ofNullable( + consumerTreeCache.getCurrentData(groupTopicDirs.consumerOwnerDir() + "/" + partitionId)) + .map(data -> new String(data.getData())) + .orElse(null)); + + consumerPartition.setOffset(consumerOffsets.getOrDefault(partitionId, -1L)); + + final Optional topicPartition = topic.getPartition(partitionId); + consumerPartition.setSize(topicPartition.map(TopicPartitionVO::getSize).orElse(-1L)); + consumerPartition.setFirstOffset(topicPartition.map(TopicPartitionVO::getFirstOffset).orElse(-1L)); + + return consumerPartition; + }); + } + else { + return Stream.empty(); + } + } + + private Map getConsumerOffsets(String groupId, TopicVO topic) + { + try { + // Kafka doesn't really give us an indication of whether a consumer is + // using Kafka or Zookeeper based offset tracking. So look up the offsets + // for both and assume that the largest offset is the correct one. + + ForkJoinTask> kafkaTask = + threadPool.submit(() -> getConsumerOffsets(groupId, topic, false)); + + ForkJoinTask> zookeeperTask = + threadPool.submit(() -> getConsumerOffsets(groupId, topic, true)); + + Map zookeeperOffsets = zookeeperTask.get(); + Map kafkaOffsets = kafkaTask.get(); + zookeeperOffsets.entrySet() + .forEach(entry -> kafkaOffsets.merge(entry.getKey(), entry.getValue(), Math::max)); + return kafkaOffsets; + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(ex); + } + catch (ExecutionException ex) { + throw Throwables.propagate(ex.getCause()); + } + } + + private Map getConsumerOffsets(String groupId, + TopicVO topic, + boolean zookeeperOffsets) + { + return retryTemplate.execute( + context -> brokerChannel(zookeeperOffsets ? null : offsetManagerBroker(groupId)) + .execute(channel -> getConsumerOffsets(channel, groupId, topic, zookeeperOffsets))); + } + + /** + * Returns the map of partitionId to consumer offset for the given group and + * topic. Uses the given blocking channel to execute the offset fetch request. + * + * @param channel The channel to send requests on + * @param groupId Consumer group to use + * @param topic Topic to query + * @param zookeeperOffsets If true, use a version of the API that retrieves + * offsets from Zookeeper. Otherwise use a version + * that pulls the offsets from Kafka itself. + * @return Map where the key is partitionId and the value is the consumer + * offset for that partition. + */ + private Map getConsumerOffsets(BlockingChannel channel, + String groupId, + TopicVO topic, + boolean zookeeperOffsets) + { + + final OffsetFetchRequest request = new OffsetFetchRequest( + groupId, + topic.getPartitions().stream() + .map(p -> new TopicAndPartition(topic.getName(), p.getId())) + .collect(Collectors.toList()), + (short) (zookeeperOffsets ? 0 : 1), 0, // version 0 = zookeeper offsets, 1 = kafka offsets + clientId()); + + LOG.debug("Sending consumer offset request: {}", request); + + channel.send(request.underlying()); + + final kafka.api.OffsetFetchResponse underlyingResponse = + kafka.api.OffsetFetchResponse.readFrom(channel.receive().buffer()); + + LOG.debug("Received consumer offset response: {}", underlyingResponse); + + OffsetFetchResponse response = new OffsetFetchResponse(underlyingResponse); + + return response.offsets().entrySet().stream() + .filter(entry -> entry.getValue().error() == ErrorMapping.NoError()) + .collect(Collectors.toMap(entry -> entry.getKey().partition(), entry -> entry.getValue().offset())); + } + + /** + * Returns the broker Id that is the offset coordinator for the given group id. If not found, returns null + */ + private Integer offsetManagerBroker(String groupId) + { + return retryTemplate.execute( + context -> + brokerChannel(null) + .execute(channel -> offsetManagerBroker(channel, groupId)) + ); + } + + private Integer offsetManagerBroker(BlockingChannel channel, String groupId) + { + final ConsumerMetadataRequest request = + new ConsumerMetadataRequest(groupId, (short) 0, 0, clientId()); + + LOG.debug("Sending consumer metadata request: {}", request); + + channel.send(request); + ConsumerMetadataResponse response = + ConsumerMetadataResponse.readFrom(channel.receive().buffer()); + + LOG.debug("Received consumer metadata response: {}", response); + + return (response.errorCode() == ErrorMapping.NoError()) ? response.coordinator().id() : null; + } + + private Map getTopicPartitionSizes(TopicVO topic) + { + return kafkaHighLevelConsumer.getPartitionSize(topic.getName()); + } + + + + /*private Map sendOffsetRequest(Integer brokerId, TopicVO topic, + PartitionOffsetRequestInfo requestInfo, + List brokerPartitions) + { + final OffsetRequest offsetRequest = new OffsetRequest( + brokerPartitions.stream() + .collect(Collectors.toMap( + partition -> new TopicAndPartition(topic.getName(), partition.getId()), + partition -> requestInfo)), + (short) 0, clientId()); + + LOG.debug("Sending offset request: {}", offsetRequest); + return kafkaHighLevelConsumer.getPartitionSize(topic.getName()); + }*/ + + private class BrokerListener + implements PathChildrenCacheListener + { + @Override + public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) + throws Exception + { + switch (event.getType()) { + case CHILD_REMOVED: { + BrokerVO broker = removeBroker(brokerId(event.getData())); + break; + } + + case CHILD_ADDED: + case CHILD_UPDATED: { + addBroker(parseBroker(event.getData())); + break; + } + + case INITIALIZED: { + brokerPathCache.getCurrentData().stream() + .map(BrokerListener.this::parseBroker) + .forEach(CuratorKafkaMonitor.this::addBroker); + break; + } + } + updateController(); + } + + private int brokerId(ChildData input) + { + return Integer.parseInt(StringUtils.substringAfter(input.getPath(), ZkUtils.BrokerIdsPath() + "/")); + } + + private BrokerVO parseBroker(ChildData input) + { + try { + final BrokerVO broker = objectMapper.reader(BrokerVO.class).readValue(input.getData()); + broker.setId(brokerId(input)); + return broker; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } } diff --git a/src/main/java/com/homeadvisor/kafdrop/service/KafkaHighLevelConsumer.java b/src/main/java/com/homeadvisor/kafdrop/service/KafkaHighLevelConsumer.java new file mode 100644 index 0000000..87e7d0b --- /dev/null +++ b/src/main/java/com/homeadvisor/kafdrop/service/KafkaHighLevelConsumer.java @@ -0,0 +1,157 @@ +package com.homeadvisor.kafdrop.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; +import com.homeadvisor.kafdrop.config.KafkaConfiguration; +import com.homeadvisor.kafdrop.model.TopicPartitionVO; +import com.homeadvisor.kafdrop.model.TopicVO; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Created by Satendra Sahu on 9/20/18 + */ +@Service +public class KafkaHighLevelConsumer +{ + private final Logger LOG = LoggerFactory.getLogger(getClass()); + private final static ObjectMapper objectMapper = new ObjectMapper(); + private KafkaConsumer kafkaConsumer; + + @Autowired + private KafkaConfiguration kafkaaConfiguration; + + public KafkaHighLevelConsumer() {} + + @PostConstruct + private void initializeClient() + { + if (kafkaConsumer == null) { + + Properties properties = new Properties(); + properties.put("group.id", "kafka-drop-consumer-group"); + properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + properties.put(ConsumerConfig.GROUP_ID_CONFIG, "kafka-drop-consumer-group"); + properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getCanonicalName()); + properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getCanonicalName()); + properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100); + properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + properties.put(ConsumerConfig.CLIENT_ID_CONFIG, "kafka-drop-client"); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaaConfiguration.getBrokerConnect()); + + if (kafkaaConfiguration.getIsSecured() == true) { + properties.put("sasl.mechanism", "PLAIN"); + properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT"); + } + + kafkaConsumer = new KafkaConsumer(properties); + } + } + + public synchronized Map getPartitionSize(String topic) + { + initializeClient(); + + List partitionInfoSet = kafkaConsumer.partitionsFor(topic); + kafkaConsumer.assign(partitionInfoSet.stream().map(partitionInfo -> { + return new TopicPartition(partitionInfo.topic(), partitionInfo.partition()); + }).collect(Collectors.toList()) + ); + + kafkaConsumer.poll(0); + Set assignedPartitionList = kafkaConsumer.assignment(); + TopicVO topicVO = getTopicInfo(topic); + Map partitionsVo = topicVO.getPartitionMap(); + + kafkaConsumer.seekToBeginning(assignedPartitionList); + assignedPartitionList.stream().forEach(topicPartition -> { + TopicPartitionVO topicPartitionVO = partitionsVo.get(topicPartition.partition()); + long startOffset = kafkaConsumer.position(topicPartition); + LOG.debug("topic: {}, partition: {}, startOffset: {}", topicPartition.topic(), topicPartition.partition(), startOffset); + topicPartitionVO.setFirstOffset(startOffset); + }); + + kafkaConsumer.seekToEnd(assignedPartitionList); + assignedPartitionList.stream().forEach(topicPartition -> { + long latestOffset = kafkaConsumer.position(topicPartition); + LOG.debug("topic: {}, partition: {}, latestOffset: {}", topicPartition.topic(), topicPartition.partition(), latestOffset); + TopicPartitionVO partitionVO = partitionsVo.get(topicPartition.partition()); + partitionVO.setSize(latestOffset); + }); + return partitionsVo; + } + + public synchronized List> getLatestRecords(TopicPartition topicPartition, long offset, Long count) + { + initializeClient(); + kafkaConsumer.assign(Arrays.asList(topicPartition)); + kafkaConsumer.seek(topicPartition, offset); + + ConsumerRecords records = null; + + records = kafkaConsumer.poll(10); + if (records.count() > 0) { + return records.records(topicPartition).subList(0, count.intValue()); + } + return null; + } + + public synchronized Map getTopicsInfo(String[] topics) + { + initializeClient(); + if (topics.length == 0) { + Set topicSet = kafkaConsumer.listTopics().keySet(); + topics = Arrays.copyOf(topicSet.toArray(), topicSet.size(), String[].class); + } + Map topicVOMap = Maps.newHashMap(); + + for (String topic : topics) { + topicVOMap.put(topic, getTopicInfo(topic)); + } + + return topicVOMap; + } + + private TopicVO getTopicInfo(String topic) + { + List partitionInfoList = kafkaConsumer.partitionsFor(topic); + TopicVO topicVO = new TopicVO(topic); + Map partitions = new TreeMap<>(); + + for (PartitionInfo partitionInfo : partitionInfoList) { + TopicPartitionVO topicPartitionVO = new TopicPartitionVO(partitionInfo.partition()); + + Node leader = partitionInfo.leader(); + topicPartitionVO.addReplica(new TopicPartitionVO.PartitionReplica(leader.id(), true, true)); + + for (Node node : partitionInfo.replicas()) { + topicPartitionVO.addReplica(new TopicPartitionVO.PartitionReplica(node.id(), true, false)); + } + partitions.put(partitionInfo.partition(), topicPartitionVO); + } + + topicVO.setPartitions(partitions); + return topicVO; + } +} diff --git a/src/main/java/com/homeadvisor/kafdrop/service/KafkaMonitor.java b/src/main/java/com/homeadvisor/kafdrop/service/KafkaMonitor.java index 5346b53..e1b36e8 100644 --- a/src/main/java/com/homeadvisor/kafdrop/service/KafkaMonitor.java +++ b/src/main/java/com/homeadvisor/kafdrop/service/KafkaMonitor.java @@ -20,10 +20,11 @@ import com.homeadvisor.kafdrop.model.BrokerVO; import com.homeadvisor.kafdrop.model.ConsumerVO; -import com.homeadvisor.kafdrop.model.ClusterSummaryVO; +import com.homeadvisor.kafdrop.model.MessageVO; +import com.homeadvisor.kafdrop.model.TopicPartitionVO; import com.homeadvisor.kafdrop.model.TopicVO; +import org.apache.kafka.common.TopicPartition; -import java.util.Collection; import java.util.List; import java.util.Optional; @@ -36,11 +37,9 @@ public interface KafkaMonitor List getTopics(); - Optional getTopic(String topic); - - ClusterSummaryVO getClusterSummary(); + List getMessages(TopicPartition topicPartition, long offset, long count); - ClusterSummaryVO getClusterSummary(Collection topics); + Optional getTopic(String topic); List getConsumers(); diff --git a/src/main/java/com/homeadvisor/kafdrop/service/MessageInspector.java b/src/main/java/com/homeadvisor/kafdrop/service/MessageInspector.java index 5143bda..b9f77de 100644 --- a/src/main/java/com/homeadvisor/kafdrop/service/MessageInspector.java +++ b/src/main/java/com/homeadvisor/kafdrop/service/MessageInspector.java @@ -29,6 +29,7 @@ import kafka.javaapi.message.ByteBufferMessageSet; import kafka.message.Message; import kafka.message.MessageAndOffset; +import org.apache.kafka.common.TopicPartition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -55,41 +56,9 @@ public List getMessages(String topicName, int partitionId, long offse final TopicVO topic = kafkaMonitor.getTopic(topicName).orElseThrow(TopicNotFoundException::new); final TopicPartitionVO partition = topic.getPartition(partitionId).orElseThrow(PartitionNotFoundException::new); - return kafkaMonitor.getBroker(partition.getLeader().getId()) - .map(broker -> { - SimpleConsumer consumer = new SimpleConsumer(broker.getHost(), broker.getPort(), 10000, 100000, ""); + TopicPartition topicPartition = new TopicPartition(topicName, partitionId); + return kafkaMonitor.getMessages(topicPartition, offset, count); - final FetchRequestBuilder fetchRequestBuilder = new FetchRequestBuilder() - .clientId("KafDrop") - .maxWait(5000) // todo: make configurable - .minBytes(1); - - List messages = new ArrayList<>(); - long currentOffset = offset; - while (messages.size() < count) - { - final FetchRequest fetchRequest = - fetchRequestBuilder - .addFetch(topicName, partitionId, currentOffset, 1024 * 1024) - .build(); - - FetchResponse fetchResponse = consumer.fetch(fetchRequest); - - final ByteBufferMessageSet messageSet = fetchResponse.messageSet(topicName, partitionId); - if (messageSet.validBytes() <= 0) break; - - - int oldSize = messages.size(); - StreamSupport.stream(messageSet.spliterator(), false) - .limit(count - messages.size()) - .map(MessageAndOffset::message) - .map(this::createMessage) - .forEach(messages::add); - currentOffset += messages.size() - oldSize; - } - return messages; - }) - .orElseGet(Collections::emptyList); } private MessageVO createMessage(Message message) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0a65a8e..a259e6c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,11 +15,11 @@ metrics.jmx.domain: ${spring.jmx.default_domain}-metrics management.contextPath: /debug kafdrop.monitor: - kafkaVersion: "0.8.2.2" + kafkaVersion: "0.11.0.2" threadPoolSize: 10 retry: - maxAttempts: 3 - backoffMillis: 1000 + maxAttempts: 5 + backoffMillis: 2000 curator.discovery: @@ -39,3 +39,12 @@ project: name: KafDropr (DEV) description: ${project.name} version: DEV + +# env can be 'local', 'stage', 'prod' +kafka: + env: local + brokerConnect: localhost:9092 + isSecured: false + +zookeeper: + connect: localhost:2181 \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties index e9858b9..893d271 100644 --- a/src/main/resources/log4j.properties +++ b/src/main/resources/log4j.properties @@ -23,7 +23,7 @@ log4j.rootCategory=INFO, ${LOGGER} PID=???? LOG_PATH=${java.io.tmpdir} -LOG_FILE=${LOG_PATH}/kafdrop.log +LOG_FILE=${LOG_PATH}/kafka-dashboard.log LOG_PATTERN=[%d{yyyy-MM-dd HH:mm:ss.SSS}] %X{context} - ${PID} %5p [%t] --- %c{1}: %m%n # CONSOLE is set to be a ConsoleAppender using a PatternLayout. diff --git a/src/main/resources/static/css/baseless.min.css b/src/main/resources/static/css/baseless.min.css new file mode 100644 index 0000000..cb6556a --- /dev/null +++ b/src/main/resources/static/css/baseless.min.css @@ -0,0 +1 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */hr,legend{border:0;padding:0}body,h1,h2,h3,h4,h5,h6{font-family:"PT Sans",Helvetica,'Helvetica Neue',Arial,sans-serif}hr,legend,td,th{padding:0}pre,textarea{overflow:auto}.bs-btn,a,ins{text-decoration:none}.row .col.eightcol.no-collapse,.row .col.elevencol.no-collapse,.row .col.fivecol.no-collapse,.row .col.fourcol.no-collapse,.row .col.ninecol.no-collapse,.row .col.no-collapse,.row .col.onecol.no-collapse,.row .col.sixcol.no-collapse,.row .col.tencol.no-collapse,.row .col.threecol.no-collapse,.row .col.twelvecol.no-collapse,.row .col.twocol.no-collapse{min-height:1px}.bs-form-group:after,.bs-list:after,.bs-panel:after,.cf:after,.clearfix:after,.container:after,.row:after,h1:after,h2:after,h3:after,h4:after,h5:after,h6:after{clear:both}.bs-btn,.bs-label,.bs-shape{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;box-sizing:border-box}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}small{font-size:80%}svg:not(:root){overflow:hidden}figure{margin:1em 40px}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.bs-alert .alert-subtext,.bs-alert .alert-title,.bs-btn,.bs-label,.bs-panel .panel-heading,h4,h5,h6,mark{font-weight:700}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}.cf:after,.cf:before,.clearfix:after,.clearfix:before{content:" ";display:table}.pr,.pull-right{float:right}.pl,.pull-left{float:left}.text-primary{color:#2371b1}.text-success{color:#9ebf24}.text-warning{color:#e6781e}.text-notice{color:#cc0c39}.text-info{color:#e3dfba}.bs-btn,.bs-btn:active,.bs-btn:hover,.bs-btn:visited,.text-inverted,blockquote,body,ins,mark{color:#333}a,a:active,a:focus,a:hover,a:visited{color:#2371b1}.text-right{text-align:right}.text-left{text-align:left}.bs-label,.bs-shape,.text-center{text-align:center}.center-block{display:block;margin-left:auto;margin-right:auto}h1,h2,h3,h4,h5,h6,p{margin:17.5px 0 0}hr,ol,ul{margin:17.5px 0}*,:after,:before{box-sizing:inherit}:focus{outline:#b3b3b3 dotted thin}.bs-btn:focus,a:active,a:hover{outline:0}body{font-size:14px}a{background-color:transparent;transition-property:color;transition-duration:.5s}h1,h2,h3,h4,h5,h6{color:14px;line-height:1.15em}h1:after,h1:before,h2:after,h2:before,h3:after,h3:before,h4:after,h4:before,h5:after,h5:before,h6:after,h6:before{content:" ";display:table}h1{font-size:35px}h2{font-size:31.5px}h3{font-size:29.75px}h4{font-size:28px}h5{font-size:26.25px}h6{font-size:24.5px}blockquote{margin:0;padding-left:17.5px;border-left:.5em #ccc solid}hr{box-sizing:content-box;display:block;height:2px;border-top:none;border-bottom:1px solid #e6e6e6}code,kbd,pre,samp{font-family:"PT Mono",Menlo,'Ubuntu Mono','Lucida Console','Courier New',Courier,monospace;color:#999;font-size:1em}pre{white-space:pre}ins{background:#ff9}mark{background:#ff0}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}ol,ul{padding:0 0 0 17.5px}li p:last-child{margin:0}dd{margin:0 0 0 17.5px}img{max-width:100%;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.bs-btn{cursor:pointer;margin:0;padding:3px 10px;border:0;display:inline-block;font-family:"PT Sans",Helvetica,'Helvetica Neue',Arial,sans-serif;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:3px;background:repeat-x #f2f2f2;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0)}.bs-btn:hover{background-color:#ededed;background-image:-moz-linear-gradient(top,#fff,#ededed);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ededed));background-image:-webkit-linear-gradient(top,#fff,#ededed);background-image:-o-linear-gradient(top,#fff,#ededed);background-image:linear-gradient(to bottom,#fff,#ededed);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffededed', GradientType=0)}.bs-btn:active{background:#e0e0e0}.bs-btn.notice,.bs-btn.notice:visited,.bs-btn.primary:visited,.bs-btn.success,.bs-btn.success:visited,.bs-btn.warning,.bs-btn.warning:visited{color:#fff}.bs-btn.disabled,.bs-btn:disabled{cursor:not-allowed;background:#e0e0e0}.bs-btn.large{font-size:17.5px}.bs-btn.small{font-size:11.9px}.bs-btn.mini{font-size:10.5px}.bs-btn.primary{background:repeat-x #1f639c;background-image:-moz-linear-gradient(top,#2371b1,#1f639c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#2371b1),to(#1f639c));background-image:-webkit-linear-gradient(top,#2371b1,#1f639c);background-image:-o-linear-gradient(top,#2371b1,#1f639c);background-image:linear-gradient(to bottom,#2371b1,#1f639c);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2371b1', endColorstr='#ff1f639c', GradientType=0);color:#fff}.bs-btn.primary:hover{background-color:#1d5e93;background-image:-moz-linear-gradient(top,#2371b1,#1d5e93);background-image:-webkit-gradient(linear,0 0,0 100%,from(#2371b1),to(#1d5e93));background-image:-webkit-linear-gradient(top,#2371b1,#1d5e93);background-image:-o-linear-gradient(top,#2371b1,#1d5e93);background-image:linear-gradient(to bottom,#2371b1,#1d5e93);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2371b1', endColorstr='#ff1d5e93', GradientType=0)}.bs-btn.primary:active{background:#19507e}.bs-btn.primary.disabled,.bs-btn.primary:disabled{cursor:not-allowed;background:#19507e}.bs-btn.success{background:repeat-x #8caa20;background-image:-moz-linear-gradient(top,#9ebf24,#8caa20);background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ebf24),to(#8caa20));background-image:-webkit-linear-gradient(top,#9ebf24,#8caa20);background-image:-o-linear-gradient(top,#9ebf24,#8caa20);background-image:linear-gradient(to bottom,#9ebf24,#8caa20);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9ebf24', endColorstr='#ff8caa20', GradientType=0)}.bs-btn.success:hover{background-color:#85a11e;background-image:-moz-linear-gradient(top,#9ebf24,#85a11e);background-image:-webkit-gradient(linear,0 0,0 100%,from(#9ebf24),to(#85a11e));background-image:-webkit-linear-gradient(top,#9ebf24,#85a11e);background-image:-o-linear-gradient(top,#9ebf24,#85a11e);background-image:linear-gradient(to bottom,#9ebf24,#85a11e);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9ebf24', endColorstr='#ff85a11e', GradientType=0)}.bs-btn.success:active{background:#738c1a}.bs-btn.success.disabled,.bs-btn.success:disabled{cursor:not-allowed;background:#738c1a}.bs-btn.warning{background:repeat-x #d36c17;background-image:-moz-linear-gradient(top,#e6781e,#d36c17);background-image:-webkit-gradient(linear,0 0,0 100%,from(#e6781e),to(#d36c17));background-image:-webkit-linear-gradient(top,#e6781e,#d36c17);background-image:-o-linear-gradient(top,#e6781e,#d36c17);background-image:linear-gradient(to bottom,#e6781e,#d36c17);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe6781e', endColorstr='#ffd36c17', GradientType=0)}.bs-btn.warning:hover{background-color:#ca6716;background-image:-moz-linear-gradient(top,#e6781e,#ca6716);background-image:-webkit-gradient(linear,0 0,0 100%,from(#e6781e),to(#ca6716));background-image:-webkit-linear-gradient(top,#e6781e,#ca6716);background-image:-o-linear-gradient(top,#e6781e,#ca6716);background-image:linear-gradient(to bottom,#e6781e,#ca6716);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe6781e', endColorstr='#ffca6716', GradientType=0)}.bs-btn.warning:active{background:#b35b14}.bs-btn.warning.disabled,.bs-btn.warning:disabled{cursor:not-allowed;background:#b35b14}.bs-btn.notice{background:repeat-x #b40b32;background-image:-moz-linear-gradient(top,#cc0c39,#b40b32);background-image:-webkit-gradient(linear,0 0,0 100%,from(#cc0c39),to(#b40b32));background-image:-webkit-linear-gradient(top,#cc0c39,#b40b32);background-image:-o-linear-gradient(top,#cc0c39,#b40b32);background-image:linear-gradient(to bottom,#cc0c39,#b40b32);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcc0c39', endColorstr='#ffb40b32', GradientType=0)}.bs-btn.notice:hover{background-color:#aa0a30;background-image:-moz-linear-gradient(top,#cc0c39,#aa0a30);background-image:-webkit-gradient(linear,0 0,0 100%,from(#cc0c39),to(#aa0a30));background-image:-webkit-linear-gradient(top,#cc0c39,#aa0a30);background-image:-o-linear-gradient(top,#cc0c39,#aa0a30);background-image:linear-gradient(to bottom,#cc0c39,#aa0a30);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcc0c39', endColorstr='#ffaa0a30', GradientType=0)}.bs-btn.notice:active{background:#920929}.bs-btn.notice.disabled,.bs-btn.notice:disabled{cursor:not-allowed;background:#920929}.bs-btn.info{background:repeat-x #e6e3cc;background-image:-moz-linear-gradient(top,#eeecdd,#e6e3cc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#eeecdd),to(#e6e3cc));background-image:-webkit-linear-gradient(top,#eeecdd,#e6e3cc);background-image:-o-linear-gradient(top,#eeecdd,#e6e3cc);background-image:linear-gradient(to bottom,#eeecdd,#e6e3cc);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeecdd', endColorstr='#ffe6e3cc', GradientType=0);color:#333}.bs-btn.info:hover{background-color:#e2e0c5;background-image:-moz-linear-gradient(top,#eeecdd,#e2e0c5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#eeecdd),to(#e2e0c5));background-image:-webkit-linear-gradient(top,#eeecdd,#e2e0c5);background-image:-o-linear-gradient(top,#eeecdd,#e2e0c5);background-image:linear-gradient(to bottom,#eeecdd,#e2e0c5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeecdd', endColorstr='#ffe2e0c5', GradientType=0)}.bs-btn.info:active{background:#dad6b4}.bs-btn.info:visited{color:#333}.bs-btn.info.disabled,.bs-btn.info:disabled{cursor:not-allowed;background:#dad6b4}.bs-btn.inverted{background:repeat-x #262626;background-image:-moz-linear-gradient(top,#333,#262626);background-image:-webkit-gradient(linear,0 0,0 100%,from(#333),to(#262626));background-image:-webkit-linear-gradient(top,#333,#262626);background-image:-o-linear-gradient(top,#333,#262626);background-image:linear-gradient(to bottom,#333,#262626);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff333333', endColorstr='#ff262626', GradientType=0);color:#fff}.bs-btn.inverted:hover{background-color:#212121;background-image:-moz-linear-gradient(top,#333,#212121);background-image:-webkit-gradient(linear,0 0,0 100%,from(#333),to(#212121));background-image:-webkit-linear-gradient(top,#333,#212121);background-image:-o-linear-gradient(top,#333,#212121);background-image:linear-gradient(to bottom,#333,#212121);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff333333', endColorstr='#ff212121', GradientType=0)}.bs-btn.inverted:active{background:#141414}.bs-btn.inverted:visited{color:#fff}.bs-btn.inverted.disabled,.bs-btn.inverted:disabled{cursor:not-allowed;background:#141414}.bs-alert{border-radius:3px;margin:15px 0;padding:0;background:#f2f0de;border:1px solid #dcd7a8;color:#333}.bs-alert .alert-text{margin:0;padding:10px}.bs-alert .alert-title{margin:0;padding:6px;color:#333;text-shadow:1px 1px 0 #d4ce96}.bs-alert .alert-subtext{margin:0;padding:0 10px;color:#b7ad4d}.bs-alert .alert-notice{margin:0;padding:10px;text-shadow:-1px -1px 0 #7d7c6b;background:#8b8976;border-top:1px solid #d4ce96}.bs-alert.primary{background:#318cd6;border:1px solid #1f639c;color:#fff}.bs-alert.primary .alert-title{color:#fff;text-shadow:1px 1px 0 #1b5686}.bs-alert.primary .alert-subtext{color:#0a1f31}.bs-alert.primary .alert-notice{text-shadow:-1px -1px 0 #7eacd1;background:#91b8d8;border-top:1px solid #1b5686}.bs-alert.success{background:#b8da3c;border:1px solid #8caa20;color:#fff}.bs-alert.success .alert-title{color:#fff;text-shadow:1px 1px 0 #7b941c}.bs-alert.success .alert-subtext{color:#343e0c}.bs-alert.success .alert-notice{text-shadow:-1px -1px 0 #c6d97e;background:#cfdf92;border-top:1px solid #7b941c}.bs-alert.warning{background:#eb944c;border:1px solid #d36c17;color:#fff}.bs-alert.warning .alert-title{color:#fff;text-shadow:1px 1px 0 #bc6015}.bs-alert.warning .alert-subtext{color:#60310b}.bs-alert.warning .alert-notice{text-shadow:-1px -1px 0 #f0ae78;background:#f3bb8f;border-top:1px solid #bc6015}.bs-alert.notice{background:#f21a4c;border:1px solid #b40b32;color:#fff}.bs-alert.notice .alert-title{color:#fff;text-shadow:1px 1px 0 #9c092c}.bs-alert.notice .alert-subtext{color:#3c0411}.bs-alert.notice .alert-notice{text-shadow:-1px -1px 0 #e1708b;background:#e6859c;border-top:1px solid #9c092c}.bs-alert.info{background:#f2f0de;border:1px solid #dcd7a8;color:#333}.bs-alert.info .alert-title{color:#333;text-shadow:1px 1px 0 #d4ce96}.bs-alert.info .alert-subtext{color:#b7ad4d}.bs-alert.info .alert-notice{text-shadow:-1px -1px 0 #7d7c6b;background:#8b8976;border-top:1px solid #d4ce96}.bs-alert.inverted{background:#4d4d4d;border:1px solid #262626;color:#fff}.bs-alert.inverted .alert-title{color:#fff;text-shadow:1px 1px 0 #1a1a1a}.bs-alert.inverted .alert-subtext{color:#000}.bs-alert.inverted .alert-notice{text-shadow:-1px -1px 0 #8c8c8c;background:#999;border-top:1px solid #1a1a1a}.bs-form{margin:0;padding:0}.bs-form-group{margin:3px 0;padding:0}.bs-form-group:after,.bs-form-group:before{content:" ";display:table}.bs-form-group label{font-weight:700;display:block}.bs-form-group.inline label{display:inline-block}.bs-form-elem{margin:0;padding:3px 7px;border-radius:3px;border:1px solid #ccc;box-shadow:inset 1px 1px 0 #eee}.bs-form-elem.full-width{width:100%}.bs-form-control{margin:10px 0 0;padding:10px 0 0;border-top:1px solid #ccc}.row .col,.row .col.twelvecol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.container{max-width:1140px;display:block;margin-left:auto;margin-right:auto}.container:after,.container:before,.row:after,.row:before{display:table;content:" "}.row{margin-left:-10px;margin-right:-10px}.row .col{box-sizing:border-box}.row .col.twelvecol{box-sizing:border-box}.row .col.elevencol,.row .col.tencol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.row .col.elevencol{box-sizing:border-box}.row .col.tencol{box-sizing:border-box}.row .col.eightcol,.row .col.ninecol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.row .col.ninecol{box-sizing:border-box}.row .col.eightcol{box-sizing:border-box}.row .col.sevencol,.row .col.sixcol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.row .col.sevencol{box-sizing:border-box}.row .col.sevencol.no-collapse{min-height:1px}.row .col.sixcol{box-sizing:border-box}.row .col.fivecol,.row .col.fourcol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box}.row .col.fivecol{box-sizing:border-box}.row .col.fourcol{box-sizing:border-box}.row .col.threecol,.row .col.twocol{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;padding-left:10px;padding-right:10px}.row .col.threecol{box-sizing:border-box}.row .col.twocol{box-sizing:border-box}.row .col.onecol{padding-left:10px;padding-right:10px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@media (min-width:768px){.row .col,.row .col.twelvecol{float:left;width:100%}.row .col.elevencol{float:left;width:91.66666667%}.row .col.tencol{float:left;width:83.33333333%}.row .col.ninecol{float:left;width:75%}.row .col.eightcol{float:left;width:66.66666667%}.row .col.sevencol{float:left;width:58.33333333%}.row .col.sixcol{float:left;width:50%}.row .col.fivecol{float:left;width:41.66666667%}.row .col.fourcol{float:left;width:33.33333333%}.row .col.threecol{float:left;width:25%}.row .col.twocol{float:left;width:16.66666667%}.row .col.onecol{float:left;width:8.33333333%}}.bs-label{margin:0;padding:0 7px 2px;border-radius:15px;display:inline-block;font-size:14px;cursor:default;background:#eee;color:#333}.bs-list.flat,.bs-list.flat li{margin:0;padding:0}.bs-label.small{font-size:11.9px}.bs-label.large{font-size:17.5px}.bs-label.mini{font-size:10.5px}.bs-label.box{border-radius:3px}.bs-label.primary{background:#2371b1;color:#fff}.bs-label.success{background:#9ebf24;color:#fff}.bs-label.warning{background:#e6781e;color:#fff}.bs-label.notice{background:#cc0c39;color:#fff}.bs-label.info{background:#e3dfba;color:#333}.bs-label.inverted{background:#333;color:#fff}.bs-list:after,.bs-list:before{content:" ";display:table}.bs-list.flat{list-style:none}.bs-list.inline li{display:inline-block}.bs-list.nav li{max-width:100%;border-radius:15px;margin:7px 2px 7px 0;padding:0 10px;line-height:21px;background:#2371b1;color:#fff}.bs-list.nav li:hover{cursor:pointer;background:#2984cf}.bs-list.nav li a{color:#fff}.bs-list.nav.primary li{background:#2371b1;color:#fff}.bs-list.nav.primary li:hover{background:#2984cf}.bs-list.nav.primary li a{color:#fff}.bs-list.nav.success li{background:#9ebf24;color:#fff}.bs-list.nav.success li:hover{background:#b4d82f}.bs-list.nav.success li a{color:#fff}.bs-list.nav.warning li{background:#e6781e;color:#fff}.bs-list.nav.warning li:hover{background:#ea8b3e}.bs-list.nav.warning li a{color:#fff}.bs-list.nav.notice li{background:#cc0c39;color:#fff}.bs-list.nav.notice li:hover{background:#ee0e42}.bs-list.nav.notice li a{color:#fff}.bs-list.nav.info li{background:#e3dfba;color:#fff}.bs-list.nav.info li:hover{background:#edebd3}.bs-list.nav.info li a{color:#fff}.bs-list.nav.inverted li{background:#333;color:#fff}.bs-list.nav.inverted li:hover{background:#454545}.bs-list.nav.inverted li a{color:#fff}.bs-panel .panel-heading,.bs-panel.default .panel-heading{color:#333;background:#eee;border-bottom:1px solid #ccc;text-shadow:1px 1px 0 #d5d5d5}.bs-panel{margin:0 0 10px;padding:0;border:1px solid #ccc}.bs-panel:after,.bs-panel:before{content:" ";display:table}.bs-panel .panel-body,.bs-panel .panel-heading,.bs-panel .panel-list-item,.bs-panel .panel-notice{margin:0;padding:10px}.bs-panel .panel-foot{margin:0;padding:5px 10px;font-size:75%}.bs-panel.round{border-radius:3px}.bs-panel .panel-list .panel-list-item.first,.bs-panel .panel-list .panel-list-item:first-child{border-top:none}.bs-panel .panel-list .panel-list-item,.bs-panel.default .panel-list .panel-list-item{border-top:1px solid #ccc}.bs-panel.default{border:1px solid #ccc}.bs-panel.primary{border:1px solid #1b5686}.bs-panel.primary .panel-heading{color:#fff;background:#2371b1;border-bottom:1px solid #1b5686;text-shadow:1px 1px 0 #1b5686}.bs-panel.primary .panel-list .panel-list-item{border-top:1px solid #1b5686}.bs-panel.success{border:1px solid #7b941c}.bs-panel.success .panel-heading{color:#fff;background:#9ebf24;border-bottom:1px solid #7b941c;text-shadow:1px 1px 0 #7b941c}.bs-panel.success .panel-list .panel-list-item{border-top:1px solid #7b941c}.bs-panel.warning{border:1px solid #bc6015}.bs-panel.warning .panel-heading{color:#fff;background:#e6781e;border-bottom:1px solid #bc6015;text-shadow:1px 1px 0 #bc6015}.bs-panel.warning .panel-list .panel-list-item{border-top:1px solid #bc6015}.bs-panel.notice{border:1px solid #9c092c}.bs-panel.notice .panel-heading{color:#fff;background:#cc0c39;border-bottom:1px solid #9c092c;text-shadow:1px 1px 0 #9c092c}.bs-panel.notice .panel-list .panel-list-item{border-top:1px solid #9c092c}.bs-panel.info{border:1px solid #d4ce96}.bs-panel.info .panel-heading{color:#333;background:#e3dfba;border-bottom:1px solid #d4ce96;text-shadow:1px 1px 0 #d4ce96}.bs-panel.info .panel-list .panel-list-item{border-top:1px solid #d4ce96}.bs-panel.inverted{border:1px solid #1a1a1a}.bs-panel.inverted .panel-heading{color:#fff;background:#333;border-bottom:1px solid #1a1a1a;text-shadow:1px 1px 0 #1a1a1a}.bs-panel.inverted .panel-list .panel-list-item{border-top:1px solid #1a1a1a}.bs-shape{margin:3px auto;padding:0;display:block;cursor:default;width:75px;height:75px;line-height:75px;background:#eee}.bs-shape.small{width:51px;height:51px;line-height:51px}.bs-shape.mini{width:33px;height:33px;line-height:33px}.bs-shape.large{width:100px;height:100px;line-height:100px}.bs-shape.circle{border-radius:100%}.bs-shape.primary{background:#2371b1}.bs-shape.success{background:#9ebf24}.bs-shape.warning{background:#e6781e}.bs-shape.notice{background:#cc0c39}.bs-shape.info{background:#e3dfba}.bs-shape.inverted{background:#333}.bs-shape:after{content:" "}.bs-table{width:100%}.bs-table td,.bs-table th{margin:0;padding:5px;border:1px solid #ccc}.bs-table thead tr td,.bs-table thead tr th{background:#e6e6e6}.bs-table.striped tbody tr{background:#f2f2f2}.bs-table.striped tbody tr:nth-child(odd){background:#fff}.bs-table.hover td:hover,.bs-table.hover th:hover{background:#e6e6e6}.bs-table.info td,.bs-table.info th{border:1px solid #e3dfba}.bs-table.info thead tr td,.bs-table.info thead tr th{background:#f2f0de}.bs-table.info.striped tbody tr{background:#f9f8f0}.bs-table.info.striped tbody tr:nth-child(odd){background:#fff}.bs-table.info.hover td:hover,.bs-table.info.hover th:hover{background:#f2f0de}.bs-table.small td,.bs-table.small th{font-size:11.9px}.bs-table.large td,.bs-table.large th{margin:0;padding:7px 5px;font-size:17.5px}.bs-well{margin:0;padding:15px;background:#eee;border:1px solid #d5d5d5;color:#333}.bs-well.small{margin:0;padding:10px}.bs-well.large{margin:0;padding:25px}.bs-well.round{border-radius:3px}.bs-well.info{background:#e3dfba;border:1px solid #c6bd71;color:#4c481f}@media print{blockquote,img,pre,tr{page-break-inside:avoid}a,a:visited{text-decoration:underline}hr{height:1px;border:0;border-bottom:1px solid #000}a[href]:after{content:" (" attr(href) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999;padding-right:1em}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}} \ No newline at end of file diff --git a/src/main/resources/static/css/global.css b/src/main/resources/static/css/global.css index c42eb0e..42cbb5a 100644 --- a/src/main/resources/static/css/global.css +++ b/src/main/resources/static/css/global.css @@ -16,42 +16,31 @@ * */ -.page-header { - margin-top: 0; +.l-content { + padding-left: 20px; + padding-right: 20px; + margin: 0 auto; } -.page-header h1 { - font-family: "PT Sans", Helvetica, 'Helvetica Neue', Arial, sans-serif; - font-weight: bold; +.clearing { + height: 1px; + line-height: 1px; + font-size: 1px; + clear: both; } -.page-header .small { - font-size: 50% -} - -.page-header a { - text-decoration: none; -} - -.table.overview { +table.overview { width: 33%; } -.container, .container-fluid { - padding-left: 0; - padding-right: 0; +.hidden { + display: none; } -.l-container { - padding-bottom: 25px; +.error { + color: red; } -.table th { - text-align: left; - background-color: #eee; +input[type=text].error { + border-color: red; } - -.table td.profile { - padding-left: 3px; - vertical-align: bottom; -} \ No newline at end of file diff --git a/src/main/resources/static/js/global.js b/src/main/resources/static/js/global.js index f53bd9b..04812e1 100644 --- a/src/main/resources/static/js/global.js +++ b/src/main/resources/static/js/global.js @@ -26,4 +26,4 @@ jQuery(document).ready(function(){ target.slideToggle(); linkText.toggleClass('fa-chevron-circle-down fa-chevron-circle-right'); }); -}); \ No newline at end of file +}); diff --git a/src/main/resources/static/js/jquery.min.js b/src/main/resources/static/js/jquery.min.js index 644d35e..0f60b7b 100644 --- a/src/main/resources/static/js/jquery.min.js +++ b/src/main/resources/static/js/jquery.min.js @@ -1,4 +1,5 @@ -/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), -a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), -null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" - - - <#setting number_format="0">

Kafka Cluster Overview

-
- - - - - - - - - - - - - - - - - - - - - - - -
Zookeeper Host Configuration<#list zookeeper.connectList as z>${z}<#if z_has_next>,
Total Topics${clusterSummary.topicCount}
Total Partitions${clusterSummary.partitionCount}
Total Preferred Partition Leaderclass="warning">${clusterSummary.preferredReplicaPercent?string.percent}
Total Under Replicated Partitionsclass="warning">${clusterSummary.underReplicatedCount}
+
+ Zookeeper Hosts: <#list zookeeper.connectList as z>${z}<#if z_has_next>,

Brokers

- +
@@ -61,29 +19,14 @@ - + - <#if brokers?size == 0> - - - <#elseif missingBrokerIds?size gt 0> - - + <#list brokers as b> @@ -95,7 +38,6 @@ - @@ -104,35 +46,13 @@

Topics

-
IDPort JMX Port Version - Start Time - - Start Time Controller? - # Partitions (% of Total) - -
No brokers available
Missing brokers: <#list missingBrokerIds as b>${b}<#if b_has_next>, No brokers available!
${b.version} ${b.timestamp?string["yyyy-MM-dd HH:mm:ss.SSSZ"]} <@template.yn b.controller/>${(clusterSummary.getBrokerLeaderPartitionCount(b.id))!0} (${(((clusterSummary.getBrokerLeaderPartitionCount(b.id))!0)/clusterSummary.partitionCount)?string.percent})
+
- - - - + + + + <#----> @@ -144,11 +64,11 @@ <#list topics as t> - + - - + + <#----> @@ -158,15 +78,4 @@ -<@template.footer/> - - +<@template.footer/> \ No newline at end of file diff --git a/src/main/resources/templates/consumer-detail.ftl b/src/main/resources/templates/consumer-detail.ftl index bb07c7e..c205a82 100644 --- a/src/main/resources/templates/consumer-detail.ftl +++ b/src/main/resources/templates/consumer-detail.ftl @@ -1,18 +1,3 @@ -<#-- - Copyright 2016 HomeAdvisor, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> <#import "lib/template.ftl" as template> <@template.header "Consumer: ${consumer.groupId}"/> @@ -22,7 +7,7 @@

Overview

-
- Name - - -    - - - - Partitions - - - % Preferred - - - # Under Replicated - - NamePartitions% Preferred# Under Replicated Custom Config?Consumers
${t.name} ${t.partitions?size}class="warning">${t.preferredReplicaPercent?string.percent}class="warning">${t.underReplicatedPartitions?size}class="warn">${t.preferredReplicaPercent?string.percent}class="warn">${t.underReplicatedPartitions?size} <@template.yn t.config?size gt 0/>${t.consumers![]?size}
+
@@ -46,7 +31,7 @@

<@template.toggleLink target="#${tableId}" anchor='${tableId}' /> Topic: ${consumerTopic.topic}

-

Active Instances
+
@@ -69,7 +54,7 @@
Total Threads

- +
@@ -99,4 +84,4 @@ -<@template.footer/> +<@template.footer/> \ No newline at end of file diff --git a/src/main/resources/templates/includes/header.ftl b/src/main/resources/templates/includes/header.ftl index 37d3672..6802cdc 100644 --- a/src/main/resources/templates/includes/header.ftl +++ b/src/main/resources/templates/includes/header.ftl @@ -1,21 +1,6 @@ -<#-- - Copyright 2016 HomeAdvisor, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -
Partition