entry: ifaces.entrySet()) {
NetworkInterface iface = entry.getKey();
InetAddress inetAddr = entry.getValue();
if (iface.isVirtual()) {
continue;
}
byte[] macAddr;
try {
macAddr = iface.getHardwareAddress();
} catch (SocketException e) {
logger.debug("Failed to get the hardware address of a network interface: {}", iface, e);
continue;
}
boolean replace = false;
int res = compareAddresses(bestMacAddr, macAddr);
if (res < 0) {
// Found a better MAC address.
replace = true;
} else if (res == 0) {
// Two MAC addresses are of pretty much same quality.
res = compareAddresses(bestInetAddr, inetAddr);
if (res < 0) {
// Found a MAC address with better INET address.
replace = true;
} else if (res == 0) {
// Cannot tell the difference. Choose the longer one.
if (bestMacAddr.length < macAddr.length) {
replace = true;
}
}
}
if (replace) {
bestMacAddr = macAddr;
bestInetAddr = inetAddr;
}
}
if (bestMacAddr == NOT_FOUND) {
bestMacAddr = new byte[MACHINE_ID_LEN];
ThreadLocalRandom.current().nextBytes(bestMacAddr);
logger.warn(
"Failed to find a usable hardware address from the network interfaces; using random bytes: {}",
formatAddress(bestMacAddr));
}
switch (bestMacAddr.length) {
case 6: // EUI-48 - convert to EUI-64
byte[] newAddr = new byte[MACHINE_ID_LEN];
System.arraycopy(bestMacAddr, 0, newAddr, 0, 3);
newAddr[3] = (byte) 0xFF;
newAddr[4] = (byte) 0xFE;
System.arraycopy(bestMacAddr, 3, newAddr, 5, 3);
bestMacAddr = newAddr;
break;
default: // Unknown
bestMacAddr = Arrays.copyOf(bestMacAddr, MACHINE_ID_LEN);
}
return bestMacAddr;
}
/**
* @return positive - current is better, 0 - cannot tell from MAC addr, negative - candidate is better.
*/
private static int compareAddresses(byte[] current, byte[] candidate) {
if (candidate == null) {
return 1;
}
// Must be EUI-48 or longer.
if (candidate.length < 6) {
return 1;
}
// Must not be filled with only 0 and 1.
boolean onlyZeroAndOne = true;
for (byte b: candidate) {
if (b != 0 && b != 1) {
onlyZeroAndOne = false;
break;
}
}
if (onlyZeroAndOne) {
return 1;
}
// Must not be a multicast address
if ((candidate[0] & 1) != 0) {
return 1;
}
// Prefer globally unique address.
if ((current[0] & 2) == 0) {
if ((candidate[0] & 2) == 0) {
// Both current and candidate are globally unique addresses.
return 0;
} else {
// Only current is globally unique.
return 1;
}
} else {
if ((candidate[0] & 2) == 0) {
// Only candidate is globally unique.
return -1;
} else {
// Both current and candidate are non-unique.
return 0;
}
}
}
/**
* @return positive - current is better, 0 - cannot tell, negative - candidate is better
*/
private static int compareAddresses(InetAddress current, InetAddress candidate) {
return scoreAddress(current) - scoreAddress(candidate);
}
private static int scoreAddress(InetAddress addr) {
if (addr.isAnyLocalAddress()) {
return 0;
}
if (addr.isMulticastAddress()) {
return 1;
}
if (addr.isLinkLocalAddress()) {
return 2;
}
if (addr.isSiteLocalAddress()) {
return 3;
}
return 4;
}
private static String formatAddress(byte[] addr) {
StringBuilder buf = new StringBuilder(24);
for (byte b: addr) {
buf.append(String.format("%02x:", b & 0xff));
}
return buf.substring(0, buf.length() - 1);
}
private static int defaultProcessId() {
final ClassLoader loader = PlatformDependent.getSystemClassLoader();
String value;
try {
// Invoke java.lang.management.ManagementFactory.getRuntimeMXBean().getName()
Class> mgmtFactoryType = Class.forName("java.lang.management.ManagementFactory", true, loader);
Class> runtimeMxBeanType = Class.forName("java.lang.management.RuntimeMXBean", true, loader);
Method getRuntimeMXBean = mgmtFactoryType.getMethod("getRuntimeMXBean", EmptyArrays.EMPTY_CLASSES);
Object bean = getRuntimeMXBean.invoke(null, EmptyArrays.EMPTY_OBJECTS);
Method getName = runtimeMxBeanType.getDeclaredMethod("getName", EmptyArrays.EMPTY_CLASSES);
value = (String) getName.invoke(bean, EmptyArrays.EMPTY_OBJECTS);
} catch (Exception e) {
logger.debug("Could not invoke ManagementFactory.getRuntimeMXBean().getName(); Android?", e);
try {
// Invoke android.os.Process.myPid()
Class> processType = Class.forName("android.os.Process", true, loader);
Method myPid = processType.getMethod("myPid", EmptyArrays.EMPTY_CLASSES);
value = myPid.invoke(null, EmptyArrays.EMPTY_OBJECTS).toString();
} catch (Exception e2) {
logger.debug("Could not invoke Process.myPid(); not Android?", e2);
value = "";
}
}
int atIndex = value.indexOf('@');
if (atIndex >= 0) {
value = value.substring(0, atIndex);
}
int pid;
try {
pid = Integer.parseInt(value);
} catch (NumberFormatException e) {
// value did not contain an integer.
pid = -1;
}
if (pid < 0 || pid > MAX_PROCESS_ID) {
pid = ThreadLocalRandom.current().nextInt(MAX_PROCESS_ID + 1);
logger.warn("Failed to find the current process ID from '{}'; using a random value: {}", value, pid);
}
return pid;
}
static final int writeInt(byte[] data, int i, int value) {
data[i ++] = (byte) (value >>> 24);
data[i ++] = (byte) (value >>> 16);
data[i ++] = (byte) (value >>> 8);
data[i ++] = (byte) value;
return i;
}
static final int writeLong(byte[] data, int i, long value) {
data[i ++] = (byte) (value >>> 56);
data[i ++] = (byte) (value >>> 48);
data[i ++] = (byte) (value >>> 40);
data[i ++] = (byte) (value >>> 32);
data[i ++] = (byte) (value >>> 24);
data[i ++] = (byte) (value >>> 16);
data[i ++] = (byte) (value >>> 8);
data[i ++] = (byte) value;
return i;
}
static final int readInt(byte[] data, int i) {
return Ints.fromBytes(data[i], data[i+1], data[i+2], data[i+3]);
}
static final long readLong(byte[] data, int i) {
return Longs.fromBytes(data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
}
/**
*
* @param randomHashCode
* @return
*/
public static final byte[] newInstanceBytes() {
// Create a new instance-buffer
final byte[] data = new byte[MACHINE_ID_LEN + PROCESS_ID_LEN + SEQUENCE_LEN + TIMESTAMP_LEN + RANDOM_LEN];
int i = 0;
// machineId
System.arraycopy(MACHINE_ID, 0, data, i, MACHINE_ID_LEN);
i += MACHINE_ID_LEN;
// processId
i = writeInt(data, i, PROCESS_ID);
// sequence
i = writeInt(data, i, nextSequence.getAndIncrement());
// timestamp (kind of)
i = writeLong(data, i, Long.reverse(System.nanoTime()) ^ System.currentTimeMillis());
// random
// random
int random = ThreadLocalRandom.current().nextInt();
i = writeInt(data, i, random);
assert i == data.length;
return data;
}
}
================================================
FILE: src/main/java/io/netty/handler/codec/http/NettyHttpHeaders.java
================================================
package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/**
* A copy-and-paste reimpleemntation of the Netty HttpHeaders, to decouple
* it from the HTTP handler.
*
* We want to be able to multiplex it on the channel. Note that we would much
* rather just reuse the existing Netty handler functionality, but it is not
* easily extensible.
*
* @see HttpHeaders
*
*
* Provides the constants for the standard HTTP header names and values and
* commonly used utility methods that accesses an {@link HttpMessage}.
*/
public abstract class NettyHttpHeaders {
// Prevent instantiation
private NettyHttpHeaders() {}
// Just delegate to the package Netty version (dumb)
public static void encode(HttpHeaders headers, ByteBuf buf) throws Exception {
HttpHeaders.encode(headers, buf);
}
// Just delegate to the package Netty version (dumb)
public static void encodeAscii0(CharSequence seq, ByteBuf buf) {
HttpHeaders.encodeAscii0(seq, buf);
}
}
================================================
FILE: src/main/resources/css/bootstrap-theme.css
================================================
/*!
* Bootstrap v3.3.2 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-default .badge,
.btn-primary .badge,
.btn-success .badge,
.btn-info .badge,
.btn-warning .badge,
.btn-danger .badge {
text-shadow: none;
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-default.disabled,
.btn-default:disabled,
.btn-default[disabled] {
background-color: #e0e0e0;
background-image: none;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #245580;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #265a88;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #265a88;
border-color: #245580;
}
.btn-primary.disabled,
.btn-primary:disabled,
.btn-primary[disabled] {
background-color: #265a88;
background-image: none;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-success.disabled,
.btn-success:disabled,
.btn-success[disabled] {
background-color: #419641;
background-image: none;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-info.disabled,
.btn-info:disabled,
.btn-info[disabled] {
background-color: #2aabd2;
background-image: none;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-warning.disabled,
.btn-warning:disabled,
.btn-warning[disabled] {
background-color: #eb9316;
background-image: none;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.btn-danger.disabled,
.btn-danger:disabled,
.btn-danger[disabled] {
background-color: #c12e2a;
background-image: none;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #2e6da4;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
}
.navbar-inverse .navbar-nav > .open > a,
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
@media (max-width: 767px) {
.navbar .navbar-nav .open .dropdown-menu > .active > a,
.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #fff;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-striped {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #286090;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
background-repeat: repeat-x;
border-color: #2b669a;
}
.list-group-item.active .badge,
.list-group-item.active:hover .badge,
.list-group-item.active:focus .badge {
text-shadow: none;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
background-repeat: repeat-x;
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
background-repeat: repeat-x;
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */
================================================
FILE: src/main/resources/css/bootstrap.css
================================================
/*!
* Bootstrap v3.3.2 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
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;
}
a {
background-color: transparent;
}
a:active,
a:hover {
outline: 0;
}
abbr[title] {
border-bottom: 1px dotted;
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
h1 {
margin: .67em 0;
font-size: 2em;
}
mark {
color: #000;
background: #ff0;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sup {
top: -.5em;
}
sub {
bottom: -.25em;
}
img {
border: 0;
}
svg:not(:root) {
overflow: hidden;
}
figure {
margin: 1em 40px;
}
hr {
height: 0;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
pre {
overflow: auto;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
button,
input,
optgroup,
select,
textarea {
margin: 0;
font: inherit;
color: inherit;
}
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 {
padding: 0;
border: 0;
}
input {
line-height: normal;
}
input[type="checkbox"],
input[type="radio"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
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-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
fieldset {
padding: .35em .625em .75em;
margin: 0 2px;
border: 1px solid #c0c0c0;
}
legend {
padding: 0;
border: 0;
}
textarea {
overflow: auto;
}
optgroup {
font-weight: bold;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
td,
th {
padding: 0;
}
/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
@media print {
*,
*:before,
*:after {
color: #000 !important;
text-shadow: none !important;
background: transparent !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
select {
background: #fff !important;
}
.navbar {
display: none;
}
.btn > .caret,
.dropup > .btn > .caret {
border-top-color: #000 !important;
}
.label {
border: 1px solid #000;
}
.table {
border-collapse: collapse !important;
}
.table td,
.table th {
background-color: #fff !important;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #ddd !important;
}
}
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
.glyphicon {
position: relative;
top: 1px;
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon-asterisk:before {
content: "\2a";
}
.glyphicon-plus:before {
content: "\2b";
}
.glyphicon-euro:before,
.glyphicon-eur:before {
content: "\20ac";
}
.glyphicon-minus:before {
content: "\2212";
}
.glyphicon-cloud:before {
content: "\2601";
}
.glyphicon-envelope:before {
content: "\2709";
}
.glyphicon-pencil:before {
content: "\270f";
}
.glyphicon-glass:before {
content: "\e001";
}
.glyphicon-music:before {
content: "\e002";
}
.glyphicon-search:before {
content: "\e003";
}
.glyphicon-heart:before {
content: "\e005";
}
.glyphicon-star:before {
content: "\e006";
}
.glyphicon-star-empty:before {
content: "\e007";
}
.glyphicon-user:before {
content: "\e008";
}
.glyphicon-film:before {
content: "\e009";
}
.glyphicon-th-large:before {
content: "\e010";
}
.glyphicon-th:before {
content: "\e011";
}
.glyphicon-th-list:before {
content: "\e012";
}
.glyphicon-ok:before {
content: "\e013";
}
.glyphicon-remove:before {
content: "\e014";
}
.glyphicon-zoom-in:before {
content: "\e015";
}
.glyphicon-zoom-out:before {
content: "\e016";
}
.glyphicon-off:before {
content: "\e017";
}
.glyphicon-signal:before {
content: "\e018";
}
.glyphicon-cog:before {
content: "\e019";
}
.glyphicon-trash:before {
content: "\e020";
}
.glyphicon-home:before {
content: "\e021";
}
.glyphicon-file:before {
content: "\e022";
}
.glyphicon-time:before {
content: "\e023";
}
.glyphicon-road:before {
content: "\e024";
}
.glyphicon-download-alt:before {
content: "\e025";
}
.glyphicon-download:before {
content: "\e026";
}
.glyphicon-upload:before {
content: "\e027";
}
.glyphicon-inbox:before {
content: "\e028";
}
.glyphicon-play-circle:before {
content: "\e029";
}
.glyphicon-repeat:before {
content: "\e030";
}
.glyphicon-refresh:before {
content: "\e031";
}
.glyphicon-list-alt:before {
content: "\e032";
}
.glyphicon-lock:before {
content: "\e033";
}
.glyphicon-flag:before {
content: "\e034";
}
.glyphicon-headphones:before {
content: "\e035";
}
.glyphicon-volume-off:before {
content: "\e036";
}
.glyphicon-volume-down:before {
content: "\e037";
}
.glyphicon-volume-up:before {
content: "\e038";
}
.glyphicon-qrcode:before {
content: "\e039";
}
.glyphicon-barcode:before {
content: "\e040";
}
.glyphicon-tag:before {
content: "\e041";
}
.glyphicon-tags:before {
content: "\e042";
}
.glyphicon-book:before {
content: "\e043";
}
.glyphicon-bookmark:before {
content: "\e044";
}
.glyphicon-print:before {
content: "\e045";
}
.glyphicon-camera:before {
content: "\e046";
}
.glyphicon-font:before {
content: "\e047";
}
.glyphicon-bold:before {
content: "\e048";
}
.glyphicon-italic:before {
content: "\e049";
}
.glyphicon-text-height:before {
content: "\e050";
}
.glyphicon-text-width:before {
content: "\e051";
}
.glyphicon-align-left:before {
content: "\e052";
}
.glyphicon-align-center:before {
content: "\e053";
}
.glyphicon-align-right:before {
content: "\e054";
}
.glyphicon-align-justify:before {
content: "\e055";
}
.glyphicon-list:before {
content: "\e056";
}
.glyphicon-indent-left:before {
content: "\e057";
}
.glyphicon-indent-right:before {
content: "\e058";
}
.glyphicon-facetime-video:before {
content: "\e059";
}
.glyphicon-picture:before {
content: "\e060";
}
.glyphicon-map-marker:before {
content: "\e062";
}
.glyphicon-adjust:before {
content: "\e063";
}
.glyphicon-tint:before {
content: "\e064";
}
.glyphicon-edit:before {
content: "\e065";
}
.glyphicon-share:before {
content: "\e066";
}
.glyphicon-check:before {
content: "\e067";
}
.glyphicon-move:before {
content: "\e068";
}
.glyphicon-step-backward:before {
content: "\e069";
}
.glyphicon-fast-backward:before {
content: "\e070";
}
.glyphicon-backward:before {
content: "\e071";
}
.glyphicon-play:before {
content: "\e072";
}
.glyphicon-pause:before {
content: "\e073";
}
.glyphicon-stop:before {
content: "\e074";
}
.glyphicon-forward:before {
content: "\e075";
}
.glyphicon-fast-forward:before {
content: "\e076";
}
.glyphicon-step-forward:before {
content: "\e077";
}
.glyphicon-eject:before {
content: "\e078";
}
.glyphicon-chevron-left:before {
content: "\e079";
}
.glyphicon-chevron-right:before {
content: "\e080";
}
.glyphicon-plus-sign:before {
content: "\e081";
}
.glyphicon-minus-sign:before {
content: "\e082";
}
.glyphicon-remove-sign:before {
content: "\e083";
}
.glyphicon-ok-sign:before {
content: "\e084";
}
.glyphicon-question-sign:before {
content: "\e085";
}
.glyphicon-info-sign:before {
content: "\e086";
}
.glyphicon-screenshot:before {
content: "\e087";
}
.glyphicon-remove-circle:before {
content: "\e088";
}
.glyphicon-ok-circle:before {
content: "\e089";
}
.glyphicon-ban-circle:before {
content: "\e090";
}
.glyphicon-arrow-left:before {
content: "\e091";
}
.glyphicon-arrow-right:before {
content: "\e092";
}
.glyphicon-arrow-up:before {
content: "\e093";
}
.glyphicon-arrow-down:before {
content: "\e094";
}
.glyphicon-share-alt:before {
content: "\e095";
}
.glyphicon-resize-full:before {
content: "\e096";
}
.glyphicon-resize-small:before {
content: "\e097";
}
.glyphicon-exclamation-sign:before {
content: "\e101";
}
.glyphicon-gift:before {
content: "\e102";
}
.glyphicon-leaf:before {
content: "\e103";
}
.glyphicon-fire:before {
content: "\e104";
}
.glyphicon-eye-open:before {
content: "\e105";
}
.glyphicon-eye-close:before {
content: "\e106";
}
.glyphicon-warning-sign:before {
content: "\e107";
}
.glyphicon-plane:before {
content: "\e108";
}
.glyphicon-calendar:before {
content: "\e109";
}
.glyphicon-random:before {
content: "\e110";
}
.glyphicon-comment:before {
content: "\e111";
}
.glyphicon-magnet:before {
content: "\e112";
}
.glyphicon-chevron-up:before {
content: "\e113";
}
.glyphicon-chevron-down:before {
content: "\e114";
}
.glyphicon-retweet:before {
content: "\e115";
}
.glyphicon-shopping-cart:before {
content: "\e116";
}
.glyphicon-folder-close:before {
content: "\e117";
}
.glyphicon-folder-open:before {
content: "\e118";
}
.glyphicon-resize-vertical:before {
content: "\e119";
}
.glyphicon-resize-horizontal:before {
content: "\e120";
}
.glyphicon-hdd:before {
content: "\e121";
}
.glyphicon-bullhorn:before {
content: "\e122";
}
.glyphicon-bell:before {
content: "\e123";
}
.glyphicon-certificate:before {
content: "\e124";
}
.glyphicon-thumbs-up:before {
content: "\e125";
}
.glyphicon-thumbs-down:before {
content: "\e126";
}
.glyphicon-hand-right:before {
content: "\e127";
}
.glyphicon-hand-left:before {
content: "\e128";
}
.glyphicon-hand-up:before {
content: "\e129";
}
.glyphicon-hand-down:before {
content: "\e130";
}
.glyphicon-circle-arrow-right:before {
content: "\e131";
}
.glyphicon-circle-arrow-left:before {
content: "\e132";
}
.glyphicon-circle-arrow-up:before {
content: "\e133";
}
.glyphicon-circle-arrow-down:before {
content: "\e134";
}
.glyphicon-globe:before {
content: "\e135";
}
.glyphicon-wrench:before {
content: "\e136";
}
.glyphicon-tasks:before {
content: "\e137";
}
.glyphicon-filter:before {
content: "\e138";
}
.glyphicon-briefcase:before {
content: "\e139";
}
.glyphicon-fullscreen:before {
content: "\e140";
}
.glyphicon-dashboard:before {
content: "\e141";
}
.glyphicon-paperclip:before {
content: "\e142";
}
.glyphicon-heart-empty:before {
content: "\e143";
}
.glyphicon-link:before {
content: "\e144";
}
.glyphicon-phone:before {
content: "\e145";
}
.glyphicon-pushpin:before {
content: "\e146";
}
.glyphicon-usd:before {
content: "\e148";
}
.glyphicon-gbp:before {
content: "\e149";
}
.glyphicon-sort:before {
content: "\e150";
}
.glyphicon-sort-by-alphabet:before {
content: "\e151";
}
.glyphicon-sort-by-alphabet-alt:before {
content: "\e152";
}
.glyphicon-sort-by-order:before {
content: "\e153";
}
.glyphicon-sort-by-order-alt:before {
content: "\e154";
}
.glyphicon-sort-by-attributes:before {
content: "\e155";
}
.glyphicon-sort-by-attributes-alt:before {
content: "\e156";
}
.glyphicon-unchecked:before {
content: "\e157";
}
.glyphicon-expand:before {
content: "\e158";
}
.glyphicon-collapse-down:before {
content: "\e159";
}
.glyphicon-collapse-up:before {
content: "\e160";
}
.glyphicon-log-in:before {
content: "\e161";
}
.glyphicon-flash:before {
content: "\e162";
}
.glyphicon-log-out:before {
content: "\e163";
}
.glyphicon-new-window:before {
content: "\e164";
}
.glyphicon-record:before {
content: "\e165";
}
.glyphicon-save:before {
content: "\e166";
}
.glyphicon-open:before {
content: "\e167";
}
.glyphicon-saved:before {
content: "\e168";
}
.glyphicon-import:before {
content: "\e169";
}
.glyphicon-export:before {
content: "\e170";
}
.glyphicon-send:before {
content: "\e171";
}
.glyphicon-floppy-disk:before {
content: "\e172";
}
.glyphicon-floppy-saved:before {
content: "\e173";
}
.glyphicon-floppy-remove:before {
content: "\e174";
}
.glyphicon-floppy-save:before {
content: "\e175";
}
.glyphicon-floppy-open:before {
content: "\e176";
}
.glyphicon-credit-card:before {
content: "\e177";
}
.glyphicon-transfer:before {
content: "\e178";
}
.glyphicon-cutlery:before {
content: "\e179";
}
.glyphicon-header:before {
content: "\e180";
}
.glyphicon-compressed:before {
content: "\e181";
}
.glyphicon-earphone:before {
content: "\e182";
}
.glyphicon-phone-alt:before {
content: "\e183";
}
.glyphicon-tower:before {
content: "\e184";
}
.glyphicon-stats:before {
content: "\e185";
}
.glyphicon-sd-video:before {
content: "\e186";
}
.glyphicon-hd-video:before {
content: "\e187";
}
.glyphicon-subtitles:before {
content: "\e188";
}
.glyphicon-sound-stereo:before {
content: "\e189";
}
.glyphicon-sound-dolby:before {
content: "\e190";
}
.glyphicon-sound-5-1:before {
content: "\e191";
}
.glyphicon-sound-6-1:before {
content: "\e192";
}
.glyphicon-sound-7-1:before {
content: "\e193";
}
.glyphicon-copyright-mark:before {
content: "\e194";
}
.glyphicon-registration-mark:before {
content: "\e195";
}
.glyphicon-cloud-download:before {
content: "\e197";
}
.glyphicon-cloud-upload:before {
content: "\e198";
}
.glyphicon-tree-conifer:before {
content: "\e199";
}
.glyphicon-tree-deciduous:before {
content: "\e200";
}
.glyphicon-cd:before {
content: "\e201";
}
.glyphicon-save-file:before {
content: "\e202";
}
.glyphicon-open-file:before {
content: "\e203";
}
.glyphicon-level-up:before {
content: "\e204";
}
.glyphicon-copy:before {
content: "\e205";
}
.glyphicon-paste:before {
content: "\e206";
}
.glyphicon-alert:before {
content: "\e209";
}
.glyphicon-equalizer:before {
content: "\e210";
}
.glyphicon-king:before {
content: "\e211";
}
.glyphicon-queen:before {
content: "\e212";
}
.glyphicon-pawn:before {
content: "\e213";
}
.glyphicon-bishop:before {
content: "\e214";
}
.glyphicon-knight:before {
content: "\e215";
}
.glyphicon-baby-formula:before {
content: "\e216";
}
.glyphicon-tent:before {
content: "\26fa";
}
.glyphicon-blackboard:before {
content: "\e218";
}
.glyphicon-bed:before {
content: "\e219";
}
.glyphicon-apple:before {
content: "\f8ff";
}
.glyphicon-erase:before {
content: "\e221";
}
.glyphicon-hourglass:before {
content: "\231b";
}
.glyphicon-lamp:before {
content: "\e223";
}
.glyphicon-duplicate:before {
content: "\e224";
}
.glyphicon-piggy-bank:before {
content: "\e225";
}
.glyphicon-scissors:before {
content: "\e226";
}
.glyphicon-bitcoin:before {
content: "\e227";
}
.glyphicon-yen:before {
content: "\00a5";
}
.glyphicon-ruble:before {
content: "\20bd";
}
.glyphicon-scale:before {
content: "\e230";
}
.glyphicon-ice-lolly:before {
content: "\e231";
}
.glyphicon-ice-lolly-tasted:before {
content: "\e232";
}
.glyphicon-education:before {
content: "\e233";
}
.glyphicon-option-horizontal:before {
content: "\e234";
}
.glyphicon-option-vertical:before {
content: "\e235";
}
.glyphicon-menu-hamburger:before {
content: "\e236";
}
.glyphicon-modal-window:before {
content: "\e237";
}
.glyphicon-oil:before {
content: "\e238";
}
.glyphicon-grain:before {
content: "\e239";
}
.glyphicon-sunglasses:before {
content: "\e240";
}
.glyphicon-text-size:before {
content: "\e241";
}
.glyphicon-text-color:before {
content: "\e242";
}
.glyphicon-text-background:before {
content: "\e243";
}
.glyphicon-object-align-top:before {
content: "\e244";
}
.glyphicon-object-align-bottom:before {
content: "\e245";
}
.glyphicon-object-align-horizontal:before {
content: "\e246";
}
.glyphicon-object-align-left:before {
content: "\e247";
}
.glyphicon-object-align-vertical:before {
content: "\e248";
}
.glyphicon-object-align-right:before {
content: "\e249";
}
.glyphicon-triangle-right:before {
content: "\e250";
}
.glyphicon-triangle-left:before {
content: "\e251";
}
.glyphicon-triangle-bottom:before {
content: "\e252";
}
.glyphicon-triangle-top:before {
content: "\e253";
}
.glyphicon-console:before {
content: "\e254";
}
.glyphicon-superscript:before {
content: "\e255";
}
.glyphicon-subscript:before {
content: "\e256";
}
.glyphicon-menu-left:before {
content: "\e257";
}
.glyphicon-menu-right:before {
content: "\e258";
}
.glyphicon-menu-down:before {
content: "\e259";
}
.glyphicon-menu-up:before {
content: "\e260";
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 10px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #333;
background-color: #fff;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
a {
color: #337ab7;
text-decoration: none;
}
a:hover,
a:focus {
color: #23527c;
text-decoration: underline;
}
a:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
figure {
margin: 0;
}
img {
vertical-align: middle;
}
.img-responsive,
.thumbnail > img,
.thumbnail a > img,
.carousel-inner > .item > img,
.carousel-inner > .item > a > img {
display: block;
max-width: 100%;
height: auto;
}
.img-rounded {
border-radius: 6px;
}
.img-thumbnail {
display: inline-block;
max-width: 100%;
height: auto;
padding: 4px;
line-height: 1.42857143;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
-webkit-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
transition: all .2s ease-in-out;
}
.img-circle {
border-radius: 50%;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color: inherit;
}
h1 small,
h2 small,
h3 small,
h4 small,
h5 small,
h6 small,
.h1 small,
.h2 small,
.h3 small,
.h4 small,
.h5 small,
.h6 small,
h1 .small,
h2 .small,
h3 .small,
h4 .small,
h5 .small,
h6 .small,
.h1 .small,
.h2 .small,
.h3 .small,
.h4 .small,
.h5 .small,
.h6 .small {
font-weight: normal;
line-height: 1;
color: #777;
}
h1,
.h1,
h2,
.h2,
h3,
.h3 {
margin-top: 20px;
margin-bottom: 10px;
}
h1 small,
.h1 small,
h2 small,
.h2 small,
h3 small,
.h3 small,
h1 .small,
.h1 .small,
h2 .small,
.h2 .small,
h3 .small,
.h3 .small {
font-size: 65%;
}
h4,
.h4,
h5,
.h5,
h6,
.h6 {
margin-top: 10px;
margin-bottom: 10px;
}
h4 small,
.h4 small,
h5 small,
.h5 small,
h6 small,
.h6 small,
h4 .small,
.h4 .small,
h5 .small,
.h5 .small,
h6 .small,
.h6 .small {
font-size: 75%;
}
h1,
.h1 {
font-size: 36px;
}
h2,
.h2 {
font-size: 30px;
}
h3,
.h3 {
font-size: 24px;
}
h4,
.h4 {
font-size: 18px;
}
h5,
.h5 {
font-size: 14px;
}
h6,
.h6 {
font-size: 12px;
}
p {
margin: 0 0 10px;
}
.lead {
margin-bottom: 20px;
font-size: 16px;
font-weight: 300;
line-height: 1.4;
}
@media (min-width: 768px) {
.lead {
font-size: 21px;
}
}
small,
.small {
font-size: 85%;
}
mark,
.mark {
padding: .2em;
background-color: #fcf8e3;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
.text-nowrap {
white-space: nowrap;
}
.text-lowercase {
text-transform: lowercase;
}
.text-uppercase {
text-transform: uppercase;
}
.text-capitalize {
text-transform: capitalize;
}
.text-muted {
color: #777;
}
.text-primary {
color: #337ab7;
}
a.text-primary:hover {
color: #286090;
}
.text-success {
color: #3c763d;
}
a.text-success:hover {
color: #2b542c;
}
.text-info {
color: #31708f;
}
a.text-info:hover {
color: #245269;
}
.text-warning {
color: #8a6d3b;
}
a.text-warning:hover {
color: #66512c;
}
.text-danger {
color: #a94442;
}
a.text-danger:hover {
color: #843534;
}
.bg-primary {
color: #fff;
background-color: #337ab7;
}
a.bg-primary:hover {
background-color: #286090;
}
.bg-success {
background-color: #dff0d8;
}
a.bg-success:hover {
background-color: #c1e2b3;
}
.bg-info {
background-color: #d9edf7;
}
a.bg-info:hover {
background-color: #afd9ee;
}
.bg-warning {
background-color: #fcf8e3;
}
a.bg-warning:hover {
background-color: #f7ecb5;
}
.bg-danger {
background-color: #f2dede;
}
a.bg-danger:hover {
background-color: #e4b9b9;
}
.page-header {
padding-bottom: 9px;
margin: 40px 0 20px;
border-bottom: 1px solid #eee;
}
ul,
ol {
margin-top: 0;
margin-bottom: 10px;
}
ul ul,
ol ul,
ul ol,
ol ol {
margin-bottom: 0;
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.list-inline {
padding-left: 0;
margin-left: -5px;
list-style: none;
}
.list-inline > li {
display: inline-block;
padding-right: 5px;
padding-left: 5px;
}
dl {
margin-top: 0;
margin-bottom: 20px;
}
dt,
dd {
line-height: 1.42857143;
}
dt {
font-weight: bold;
}
dd {
margin-left: 0;
}
@media (min-width: 768px) {
.dl-horizontal dt {
float: left;
width: 160px;
overflow: hidden;
clear: left;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.dl-horizontal dd {
margin-left: 180px;
}
}
abbr[title],
abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted #777;
}
.initialism {
font-size: 90%;
text-transform: uppercase;
}
blockquote {
padding: 10px 20px;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
}
blockquote p:last-child,
blockquote ul:last-child,
blockquote ol:last-child {
margin-bottom: 0;
}
blockquote footer,
blockquote small,
blockquote .small {
display: block;
font-size: 80%;
line-height: 1.42857143;
color: #777;
}
blockquote footer:before,
blockquote small:before,
blockquote .small:before {
content: '\2014 \00A0';
}
.blockquote-reverse,
blockquote.pull-right {
padding-right: 15px;
padding-left: 0;
text-align: right;
border-right: 5px solid #eee;
border-left: 0;
}
.blockquote-reverse footer:before,
blockquote.pull-right footer:before,
.blockquote-reverse small:before,
blockquote.pull-right small:before,
.blockquote-reverse .small:before,
blockquote.pull-right .small:before {
content: '';
}
.blockquote-reverse footer:after,
blockquote.pull-right footer:after,
.blockquote-reverse small:after,
blockquote.pull-right small:after,
.blockquote-reverse .small:after,
blockquote.pull-right .small:after {
content: '\00A0 \2014';
}
address {
margin-bottom: 20px;
font-style: normal;
line-height: 1.42857143;
}
code,
kbd,
pre,
samp {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
kbd {
padding: 2px 4px;
font-size: 90%;
color: #fff;
background-color: #333;
border-radius: 3px;
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);
}
kbd kbd {
padding: 0;
font-size: 100%;
font-weight: bold;
-webkit-box-shadow: none;
box-shadow: none;
}
pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
pre code {
padding: 0;
font-size: inherit;
color: inherit;
white-space: pre-wrap;
background-color: transparent;
border-radius: 0;
}
.pre-scrollable {
max-height: 340px;
overflow-y: scroll;
}
.container {
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 768px) {
.container {
width: 750px;
}
}
@media (min-width: 992px) {
.container {
width: 970px;
}
}
@media (min-width: 1200px) {
.container {
width: 1170px;
}
}
.container-fluid {
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.row {
margin-right: -15px;
margin-left: -15px;
}
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
float: left;
}
.col-xs-12 {
width: 100%;
}
.col-xs-11 {
width: 91.66666667%;
}
.col-xs-10 {
width: 83.33333333%;
}
.col-xs-9 {
width: 75%;
}
.col-xs-8 {
width: 66.66666667%;
}
.col-xs-7 {
width: 58.33333333%;
}
.col-xs-6 {
width: 50%;
}
.col-xs-5 {
width: 41.66666667%;
}
.col-xs-4 {
width: 33.33333333%;
}
.col-xs-3 {
width: 25%;
}
.col-xs-2 {
width: 16.66666667%;
}
.col-xs-1 {
width: 8.33333333%;
}
.col-xs-pull-12 {
right: 100%;
}
.col-xs-pull-11 {
right: 91.66666667%;
}
.col-xs-pull-10 {
right: 83.33333333%;
}
.col-xs-pull-9 {
right: 75%;
}
.col-xs-pull-8 {
right: 66.66666667%;
}
.col-xs-pull-7 {
right: 58.33333333%;
}
.col-xs-pull-6 {
right: 50%;
}
.col-xs-pull-5 {
right: 41.66666667%;
}
.col-xs-pull-4 {
right: 33.33333333%;
}
.col-xs-pull-3 {
right: 25%;
}
.col-xs-pull-2 {
right: 16.66666667%;
}
.col-xs-pull-1 {
right: 8.33333333%;
}
.col-xs-pull-0 {
right: auto;
}
.col-xs-push-12 {
left: 100%;
}
.col-xs-push-11 {
left: 91.66666667%;
}
.col-xs-push-10 {
left: 83.33333333%;
}
.col-xs-push-9 {
left: 75%;
}
.col-xs-push-8 {
left: 66.66666667%;
}
.col-xs-push-7 {
left: 58.33333333%;
}
.col-xs-push-6 {
left: 50%;
}
.col-xs-push-5 {
left: 41.66666667%;
}
.col-xs-push-4 {
left: 33.33333333%;
}
.col-xs-push-3 {
left: 25%;
}
.col-xs-push-2 {
left: 16.66666667%;
}
.col-xs-push-1 {
left: 8.33333333%;
}
.col-xs-push-0 {
left: auto;
}
.col-xs-offset-12 {
margin-left: 100%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-0 {
margin-left: 0;
}
@media (min-width: 768px) {
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
float: left;
}
.col-sm-12 {
width: 100%;
}
.col-sm-11 {
width: 91.66666667%;
}
.col-sm-10 {
width: 83.33333333%;
}
.col-sm-9 {
width: 75%;
}
.col-sm-8 {
width: 66.66666667%;
}
.col-sm-7 {
width: 58.33333333%;
}
.col-sm-6 {
width: 50%;
}
.col-sm-5 {
width: 41.66666667%;
}
.col-sm-4 {
width: 33.33333333%;
}
.col-sm-3 {
width: 25%;
}
.col-sm-2 {
width: 16.66666667%;
}
.col-sm-1 {
width: 8.33333333%;
}
.col-sm-pull-12 {
right: 100%;
}
.col-sm-pull-11 {
right: 91.66666667%;
}
.col-sm-pull-10 {
right: 83.33333333%;
}
.col-sm-pull-9 {
right: 75%;
}
.col-sm-pull-8 {
right: 66.66666667%;
}
.col-sm-pull-7 {
right: 58.33333333%;
}
.col-sm-pull-6 {
right: 50%;
}
.col-sm-pull-5 {
right: 41.66666667%;
}
.col-sm-pull-4 {
right: 33.33333333%;
}
.col-sm-pull-3 {
right: 25%;
}
.col-sm-pull-2 {
right: 16.66666667%;
}
.col-sm-pull-1 {
right: 8.33333333%;
}
.col-sm-pull-0 {
right: auto;
}
.col-sm-push-12 {
left: 100%;
}
.col-sm-push-11 {
left: 91.66666667%;
}
.col-sm-push-10 {
left: 83.33333333%;
}
.col-sm-push-9 {
left: 75%;
}
.col-sm-push-8 {
left: 66.66666667%;
}
.col-sm-push-7 {
left: 58.33333333%;
}
.col-sm-push-6 {
left: 50%;
}
.col-sm-push-5 {
left: 41.66666667%;
}
.col-sm-push-4 {
left: 33.33333333%;
}
.col-sm-push-3 {
left: 25%;
}
.col-sm-push-2 {
left: 16.66666667%;
}
.col-sm-push-1 {
left: 8.33333333%;
}
.col-sm-push-0 {
left: auto;
}
.col-sm-offset-12 {
margin-left: 100%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-0 {
margin-left: 0;
}
}
@media (min-width: 992px) {
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
float: left;
}
.col-md-12 {
width: 100%;
}
.col-md-11 {
width: 91.66666667%;
}
.col-md-10 {
width: 83.33333333%;
}
.col-md-9 {
width: 75%;
}
.col-md-8 {
width: 66.66666667%;
}
.col-md-7 {
width: 58.33333333%;
}
.col-md-6 {
width: 50%;
}
.col-md-5 {
width: 41.66666667%;
}
.col-md-4 {
width: 33.33333333%;
}
.col-md-3 {
width: 25%;
}
.col-md-2 {
width: 16.66666667%;
}
.col-md-1 {
width: 8.33333333%;
}
.col-md-pull-12 {
right: 100%;
}
.col-md-pull-11 {
right: 91.66666667%;
}
.col-md-pull-10 {
right: 83.33333333%;
}
.col-md-pull-9 {
right: 75%;
}
.col-md-pull-8 {
right: 66.66666667%;
}
.col-md-pull-7 {
right: 58.33333333%;
}
.col-md-pull-6 {
right: 50%;
}
.col-md-pull-5 {
right: 41.66666667%;
}
.col-md-pull-4 {
right: 33.33333333%;
}
.col-md-pull-3 {
right: 25%;
}
.col-md-pull-2 {
right: 16.66666667%;
}
.col-md-pull-1 {
right: 8.33333333%;
}
.col-md-pull-0 {
right: auto;
}
.col-md-push-12 {
left: 100%;
}
.col-md-push-11 {
left: 91.66666667%;
}
.col-md-push-10 {
left: 83.33333333%;
}
.col-md-push-9 {
left: 75%;
}
.col-md-push-8 {
left: 66.66666667%;
}
.col-md-push-7 {
left: 58.33333333%;
}
.col-md-push-6 {
left: 50%;
}
.col-md-push-5 {
left: 41.66666667%;
}
.col-md-push-4 {
left: 33.33333333%;
}
.col-md-push-3 {
left: 25%;
}
.col-md-push-2 {
left: 16.66666667%;
}
.col-md-push-1 {
left: 8.33333333%;
}
.col-md-push-0 {
left: auto;
}
.col-md-offset-12 {
margin-left: 100%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-0 {
margin-left: 0;
}
}
@media (min-width: 1200px) {
.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
float: left;
}
.col-lg-12 {
width: 100%;
}
.col-lg-11 {
width: 91.66666667%;
}
.col-lg-10 {
width: 83.33333333%;
}
.col-lg-9 {
width: 75%;
}
.col-lg-8 {
width: 66.66666667%;
}
.col-lg-7 {
width: 58.33333333%;
}
.col-lg-6 {
width: 50%;
}
.col-lg-5 {
width: 41.66666667%;
}
.col-lg-4 {
width: 33.33333333%;
}
.col-lg-3 {
width: 25%;
}
.col-lg-2 {
width: 16.66666667%;
}
.col-lg-1 {
width: 8.33333333%;
}
.col-lg-pull-12 {
right: 100%;
}
.col-lg-pull-11 {
right: 91.66666667%;
}
.col-lg-pull-10 {
right: 83.33333333%;
}
.col-lg-pull-9 {
right: 75%;
}
.col-lg-pull-8 {
right: 66.66666667%;
}
.col-lg-pull-7 {
right: 58.33333333%;
}
.col-lg-pull-6 {
right: 50%;
}
.col-lg-pull-5 {
right: 41.66666667%;
}
.col-lg-pull-4 {
right: 33.33333333%;
}
.col-lg-pull-3 {
right: 25%;
}
.col-lg-pull-2 {
right: 16.66666667%;
}
.col-lg-pull-1 {
right: 8.33333333%;
}
.col-lg-pull-0 {
right: auto;
}
.col-lg-push-12 {
left: 100%;
}
.col-lg-push-11 {
left: 91.66666667%;
}
.col-lg-push-10 {
left: 83.33333333%;
}
.col-lg-push-9 {
left: 75%;
}
.col-lg-push-8 {
left: 66.66666667%;
}
.col-lg-push-7 {
left: 58.33333333%;
}
.col-lg-push-6 {
left: 50%;
}
.col-lg-push-5 {
left: 41.66666667%;
}
.col-lg-push-4 {
left: 33.33333333%;
}
.col-lg-push-3 {
left: 25%;
}
.col-lg-push-2 {
left: 16.66666667%;
}
.col-lg-push-1 {
left: 8.33333333%;
}
.col-lg-push-0 {
left: auto;
}
.col-lg-offset-12 {
margin-left: 100%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-0 {
margin-left: 0;
}
}
table {
background-color: transparent;
}
caption {
padding-top: 8px;
padding-bottom: 8px;
color: #777;
text-align: left;
}
th {
text-align: left;
}
.table {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
}
.table > thead > tr > th,
.table > tbody > tr > th,
.table > tfoot > tr > th,
.table > thead > tr > td,
.table > tbody > tr > td,
.table > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #ddd;
}
.table > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #ddd;
}
.table > caption + thead > tr:first-child > th,
.table > colgroup + thead > tr:first-child > th,
.table > thead:first-child > tr:first-child > th,
.table > caption + thead > tr:first-child > td,
.table > colgroup + thead > tr:first-child > td,
.table > thead:first-child > tr:first-child > td {
border-top: 0;
}
.table > tbody + tbody {
border-top: 2px solid #ddd;
}
.table .table {
background-color: #fff;
}
.table-condensed > thead > tr > th,
.table-condensed > tbody > tr > th,
.table-condensed > tfoot > tr > th,
.table-condensed > thead > tr > td,
.table-condensed > tbody > tr > td,
.table-condensed > tfoot > tr > td {
padding: 5px;
}
.table-bordered {
border: 1px solid #ddd;
}
.table-bordered > thead > tr > th,
.table-bordered > tbody > tr > th,
.table-bordered > tfoot > tr > th,
.table-bordered > thead > tr > td,
.table-bordered > tbody > tr > td,
.table-bordered > tfoot > tr > td {
border: 1px solid #ddd;
}
.table-bordered > thead > tr > th,
.table-bordered > thead > tr > td {
border-bottom-width: 2px;
}
.table-striped > tbody > tr:nth-of-type(odd) {
background-color: #f9f9f9;
}
.table-hover > tbody > tr:hover {
background-color: #f5f5f5;
}
table col[class*="col-"] {
position: static;
display: table-column;
float: none;
}
table td[class*="col-"],
table th[class*="col-"] {
position: static;
display: table-cell;
float: none;
}
.table > thead > tr > td.active,
.table > tbody > tr > td.active,
.table > tfoot > tr > td.active,
.table > thead > tr > th.active,
.table > tbody > tr > th.active,
.table > tfoot > tr > th.active,
.table > thead > tr.active > td,
.table > tbody > tr.active > td,
.table > tfoot > tr.active > td,
.table > thead > tr.active > th,
.table > tbody > tr.active > th,
.table > tfoot > tr.active > th {
background-color: #f5f5f5;
}
.table-hover > tbody > tr > td.active:hover,
.table-hover > tbody > tr > th.active:hover,
.table-hover > tbody > tr.active:hover > td,
.table-hover > tbody > tr:hover > .active,
.table-hover > tbody > tr.active:hover > th {
background-color: #e8e8e8;
}
.table > thead > tr > td.success,
.table > tbody > tr > td.success,
.table > tfoot > tr > td.success,
.table > thead > tr > th.success,
.table > tbody > tr > th.success,
.table > tfoot > tr > th.success,
.table > thead > tr.success > td,
.table > tbody > tr.success > td,
.table > tfoot > tr.success > td,
.table > thead > tr.success > th,
.table > tbody > tr.success > th,
.table > tfoot > tr.success > th {
background-color: #dff0d8;
}
.table-hover > tbody > tr > td.success:hover,
.table-hover > tbody > tr > th.success:hover,
.table-hover > tbody > tr.success:hover > td,
.table-hover > tbody > tr:hover > .success,
.table-hover > tbody > tr.success:hover > th {
background-color: #d0e9c6;
}
.table > thead > tr > td.info,
.table > tbody > tr > td.info,
.table > tfoot > tr > td.info,
.table > thead > tr > th.info,
.table > tbody > tr > th.info,
.table > tfoot > tr > th.info,
.table > thead > tr.info > td,
.table > tbody > tr.info > td,
.table > tfoot > tr.info > td,
.table > thead > tr.info > th,
.table > tbody > tr.info > th,
.table > tfoot > tr.info > th {
background-color: #d9edf7;
}
.table-hover > tbody > tr > td.info:hover,
.table-hover > tbody > tr > th.info:hover,
.table-hover > tbody > tr.info:hover > td,
.table-hover > tbody > tr:hover > .info,
.table-hover > tbody > tr.info:hover > th {
background-color: #c4e3f3;
}
.table > thead > tr > td.warning,
.table > tbody > tr > td.warning,
.table > tfoot > tr > td.warning,
.table > thead > tr > th.warning,
.table > tbody > tr > th.warning,
.table > tfoot > tr > th.warning,
.table > thead > tr.warning > td,
.table > tbody > tr.warning > td,
.table > tfoot > tr.warning > td,
.table > thead > tr.warning > th,
.table > tbody > tr.warning > th,
.table > tfoot > tr.warning > th {
background-color: #fcf8e3;
}
.table-hover > tbody > tr > td.warning:hover,
.table-hover > tbody > tr > th.warning:hover,
.table-hover > tbody > tr.warning:hover > td,
.table-hover > tbody > tr:hover > .warning,
.table-hover > tbody > tr.warning:hover > th {
background-color: #faf2cc;
}
.table > thead > tr > td.danger,
.table > tbody > tr > td.danger,
.table > tfoot > tr > td.danger,
.table > thead > tr > th.danger,
.table > tbody > tr > th.danger,
.table > tfoot > tr > th.danger,
.table > thead > tr.danger > td,
.table > tbody > tr.danger > td,
.table > tfoot > tr.danger > td,
.table > thead > tr.danger > th,
.table > tbody > tr.danger > th,
.table > tfoot > tr.danger > th {
background-color: #f2dede;
}
.table-hover > tbody > tr > td.danger:hover,
.table-hover > tbody > tr > th.danger:hover,
.table-hover > tbody > tr.danger:hover > td,
.table-hover > tbody > tr:hover > .danger,
.table-hover > tbody > tr.danger:hover > th {
background-color: #ebcccc;
}
.table-responsive {
min-height: .01%;
overflow-x: auto;
}
@media screen and (max-width: 767px) {
.table-responsive {
width: 100%;
margin-bottom: 15px;
overflow-y: hidden;
-ms-overflow-style: -ms-autohiding-scrollbar;
border: 1px solid #ddd;
}
.table-responsive > .table {
margin-bottom: 0;
}
.table-responsive > .table > thead > tr > th,
.table-responsive > .table > tbody > tr > th,
.table-responsive > .table > tfoot > tr > th,
.table-responsive > .table > thead > tr > td,
.table-responsive > .table > tbody > tr > td,
.table-responsive > .table > tfoot > tr > td {
white-space: nowrap;
}
.table-responsive > .table-bordered {
border: 0;
}
.table-responsive > .table-bordered > thead > tr > th:first-child,
.table-responsive > .table-bordered > tbody > tr > th:first-child,
.table-responsive > .table-bordered > tfoot > tr > th:first-child,
.table-responsive > .table-bordered > thead > tr > td:first-child,
.table-responsive > .table-bordered > tbody > tr > td:first-child,
.table-responsive > .table-bordered > tfoot > tr > td:first-child {
border-left: 0;
}
.table-responsive > .table-bordered > thead > tr > th:last-child,
.table-responsive > .table-bordered > tbody > tr > th:last-child,
.table-responsive > .table-bordered > tfoot > tr > th:last-child,
.table-responsive > .table-bordered > thead > tr > td:last-child,
.table-responsive > .table-bordered > tbody > tr > td:last-child,
.table-responsive > .table-bordered > tfoot > tr > td:last-child {
border-right: 0;
}
.table-responsive > .table-bordered > tbody > tr:last-child > th,
.table-responsive > .table-bordered > tfoot > tr:last-child > th,
.table-responsive > .table-bordered > tbody > tr:last-child > td,
.table-responsive > .table-bordered > tfoot > tr:last-child > td {
border-bottom: 0;
}
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 20px;
font-size: 21px;
line-height: inherit;
color: #333;
border: 0;
border-bottom: 1px solid #e5e5e5;
}
label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: bold;
}
input[type="search"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
input[type="radio"],
input[type="checkbox"] {
margin: 4px 0 0;
margin-top: 1px \9;
line-height: normal;
}
input[type="file"] {
display: block;
}
input[type="range"] {
display: block;
width: 100%;
}
select[multiple],
select[size] {
height: auto;
}
input[type="file"]:focus,
input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
output {
display: block;
padding-top: 7px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
}
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
}
.form-control:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
}
.form-control::-moz-placeholder {
color: #999;
opacity: 1;
}
.form-control:-ms-input-placeholder {
color: #999;
}
.form-control::-webkit-input-placeholder {
color: #999;
}
.form-control[disabled],
.form-control[readonly],
fieldset[disabled] .form-control {
cursor: not-allowed;
background-color: #eee;
opacity: 1;
}
textarea.form-control {
height: auto;
}
input[type="search"] {
-webkit-appearance: none;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
line-height: 34px;
}
input[type="date"].input-sm,
input[type="time"].input-sm,
input[type="datetime-local"].input-sm,
input[type="month"].input-sm,
.input-group-sm input[type="date"],
.input-group-sm input[type="time"],
.input-group-sm input[type="datetime-local"],
.input-group-sm input[type="month"] {
line-height: 30px;
}
input[type="date"].input-lg,
input[type="time"].input-lg,
input[type="datetime-local"].input-lg,
input[type="month"].input-lg,
.input-group-lg input[type="date"],
.input-group-lg input[type="time"],
.input-group-lg input[type="datetime-local"],
.input-group-lg input[type="month"] {
line-height: 46px;
}
}
.form-group {
margin-bottom: 15px;
}
.radio,
.checkbox {
position: relative;
display: block;
margin-top: 10px;
margin-bottom: 10px;
}
.radio label,
.checkbox label {
min-height: 20px;
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
cursor: pointer;
}
.radio input[type="radio"],
.radio-inline input[type="radio"],
.checkbox input[type="checkbox"],
.checkbox-inline input[type="checkbox"] {
position: absolute;
margin-top: 4px \9;
margin-left: -20px;
}
.radio + .radio,
.checkbox + .checkbox {
margin-top: -5px;
}
.radio-inline,
.checkbox-inline {
display: inline-block;
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
vertical-align: middle;
cursor: pointer;
}
.radio-inline + .radio-inline,
.checkbox-inline + .checkbox-inline {
margin-top: 0;
margin-left: 10px;
}
input[type="radio"][disabled],
input[type="checkbox"][disabled],
input[type="radio"].disabled,
input[type="checkbox"].disabled,
fieldset[disabled] input[type="radio"],
fieldset[disabled] input[type="checkbox"] {
cursor: not-allowed;
}
.radio-inline.disabled,
.checkbox-inline.disabled,
fieldset[disabled] .radio-inline,
fieldset[disabled] .checkbox-inline {
cursor: not-allowed;
}
.radio.disabled label,
.checkbox.disabled label,
fieldset[disabled] .radio label,
fieldset[disabled] .checkbox label {
cursor: not-allowed;
}
.form-control-static {
padding-top: 7px;
padding-bottom: 7px;
margin-bottom: 0;
}
.form-control-static.input-lg,
.form-control-static.input-sm {
padding-right: 0;
padding-left: 0;
}
.input-sm {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
select.input-sm {
height: 30px;
line-height: 30px;
}
textarea.input-sm,
select[multiple].input-sm {
height: auto;
}
.form-group-sm .form-control {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
select.form-group-sm .form-control {
height: 30px;
line-height: 30px;
}
textarea.form-group-sm .form-control,
select[multiple].form-group-sm .form-control {
height: auto;
}
.form-group-sm .form-control-static {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
}
.input-lg {
height: 46px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
select.input-lg {
height: 46px;
line-height: 46px;
}
textarea.input-lg,
select[multiple].input-lg {
height: auto;
}
.form-group-lg .form-control {
height: 46px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
select.form-group-lg .form-control {
height: 46px;
line-height: 46px;
}
textarea.form-group-lg .form-control,
select[multiple].form-group-lg .form-control {
height: auto;
}
.form-group-lg .form-control-static {
height: 46px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
}
.has-feedback {
position: relative;
}
.has-feedback .form-control {
padding-right: 42.5px;
}
.form-control-feedback {
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: block;
width: 34px;
height: 34px;
line-height: 34px;
text-align: center;
pointer-events: none;
}
.input-lg + .form-control-feedback {
width: 46px;
height: 46px;
line-height: 46px;
}
.input-sm + .form-control-feedback {
width: 30px;
height: 30px;
line-height: 30px;
}
.has-success .help-block,
.has-success .control-label,
.has-success .radio,
.has-success .checkbox,
.has-success .radio-inline,
.has-success .checkbox-inline,
.has-success.radio label,
.has-success.checkbox label,
.has-success.radio-inline label,
.has-success.checkbox-inline label {
color: #3c763d;
}
.has-success .form-control {
border-color: #3c763d;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.has-success .form-control:focus {
border-color: #2b542c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;
}
.has-success .input-group-addon {
color: #3c763d;
background-color: #dff0d8;
border-color: #3c763d;
}
.has-success .form-control-feedback {
color: #3c763d;
}
.has-warning .help-block,
.has-warning .control-label,
.has-warning .radio,
.has-warning .checkbox,
.has-warning .radio-inline,
.has-warning .checkbox-inline,
.has-warning.radio label,
.has-warning.checkbox label,
.has-warning.radio-inline label,
.has-warning.checkbox-inline label {
color: #8a6d3b;
}
.has-warning .form-control {
border-color: #8a6d3b;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.has-warning .form-control:focus {
border-color: #66512c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;
}
.has-warning .input-group-addon {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #8a6d3b;
}
.has-warning .form-control-feedback {
color: #8a6d3b;
}
.has-error .help-block,
.has-error .control-label,
.has-error .radio,
.has-error .checkbox,
.has-error .radio-inline,
.has-error .checkbox-inline,
.has-error.radio label,
.has-error.checkbox label,
.has-error.radio-inline label,
.has-error.checkbox-inline label {
color: #a94442;
}
.has-error .form-control {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.has-error .form-control:focus {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
}
.has-error .input-group-addon {
color: #a94442;
background-color: #f2dede;
border-color: #a94442;
}
.has-error .form-control-feedback {
color: #a94442;
}
.has-feedback label ~ .form-control-feedback {
top: 25px;
}
.has-feedback label.sr-only ~ .form-control-feedback {
top: 0;
}
.help-block {
display: block;
margin-top: 5px;
margin-bottom: 10px;
color: #737373;
}
@media (min-width: 768px) {
.form-inline .form-group {
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
}
.form-inline .form-control {
display: inline-block;
width: auto;
vertical-align: middle;
}
.form-inline .form-control-static {
display: inline-block;
}
.form-inline .input-group {
display: inline-table;
vertical-align: middle;
}
.form-inline .input-group .input-group-addon,
.form-inline .input-group .input-group-btn,
.form-inline .input-group .form-control {
width: auto;
}
.form-inline .input-group > .form-control {
width: 100%;
}
.form-inline .control-label {
margin-bottom: 0;
vertical-align: middle;
}
.form-inline .radio,
.form-inline .checkbox {
display: inline-block;
margin-top: 0;
margin-bottom: 0;
vertical-align: middle;
}
.form-inline .radio label,
.form-inline .checkbox label {
padding-left: 0;
}
.form-inline .radio input[type="radio"],
.form-inline .checkbox input[type="checkbox"] {
position: relative;
margin-left: 0;
}
.form-inline .has-feedback .form-control-feedback {
top: 0;
}
}
.form-horizontal .radio,
.form-horizontal .checkbox,
.form-horizontal .radio-inline,
.form-horizontal .checkbox-inline {
padding-top: 7px;
margin-top: 0;
margin-bottom: 0;
}
.form-horizontal .radio,
.form-horizontal .checkbox {
min-height: 27px;
}
.form-horizontal .form-group {
margin-right: -15px;
margin-left: -15px;
}
@media (min-width: 768px) {
.form-horizontal .control-label {
padding-top: 7px;
margin-bottom: 0;
text-align: right;
}
}
.form-horizontal .has-feedback .form-control-feedback {
right: 15px;
}
@media (min-width: 768px) {
.form-horizontal .form-group-lg .control-label {
padding-top: 14.333333px;
}
}
@media (min-width: 768px) {
.form-horizontal .form-group-sm .control-label {
padding-top: 6px;
}
}
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: normal;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.btn:focus,
.btn:active:focus,
.btn.active:focus,
.btn.focus,
.btn:active.focus,
.btn.active.focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
.btn:hover,
.btn:focus,
.btn.focus {
color: #333;
text-decoration: none;
}
.btn:active,
.btn.active {
background-image: none;
outline: 0;
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
pointer-events: none;
cursor: not-allowed;
filter: alpha(opacity=65);
-webkit-box-shadow: none;
box-shadow: none;
opacity: .65;
}
.btn-default {
color: #333;
background-color: #fff;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus,
.btn-default.focus,
.btn-default:active,
.btn-default.active,
.open > .dropdown-toggle.btn-default {
color: #333;
background-color: #e6e6e6;
border-color: #adadad;
}
.btn-default:active,
.btn-default.active,
.open > .dropdown-toggle.btn-default {
background-image: none;
}
.btn-default.disabled,
.btn-default[disabled],
fieldset[disabled] .btn-default,
.btn-default.disabled:hover,
.btn-default[disabled]:hover,
fieldset[disabled] .btn-default:hover,
.btn-default.disabled:focus,
.btn-default[disabled]:focus,
fieldset[disabled] .btn-default:focus,
.btn-default.disabled.focus,
.btn-default[disabled].focus,
fieldset[disabled] .btn-default.focus,
.btn-default.disabled:active,
.btn-default[disabled]:active,
fieldset[disabled] .btn-default:active,
.btn-default.disabled.active,
.btn-default[disabled].active,
fieldset[disabled] .btn-default.active {
background-color: #fff;
border-color: #ccc;
}
.btn-default .badge {
color: #fff;
background-color: #333;
}
.btn-primary {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary.focus,
.btn-primary:active,
.btn-primary.active,
.open > .dropdown-toggle.btn-primary {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
.btn-primary:active,
.btn-primary.active,
.open > .dropdown-toggle.btn-primary {
background-image: none;
}
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled.focus,
.btn-primary[disabled].focus,
fieldset[disabled] .btn-primary.focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active {
background-color: #337ab7;
border-color: #2e6da4;
}
.btn-primary .badge {
color: #337ab7;
background-color: #fff;
}
.btn-success {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
}
.btn-success:hover,
.btn-success:focus,
.btn-success.focus,
.btn-success:active,
.btn-success.active,
.open > .dropdown-toggle.btn-success {
color: #fff;
background-color: #449d44;
border-color: #398439;
}
.btn-success:active,
.btn-success.active,
.open > .dropdown-toggle.btn-success {
background-image: none;
}
.btn-success.disabled,
.btn-success[disabled],
fieldset[disabled] .btn-success,
.btn-success.disabled:hover,
.btn-success[disabled]:hover,
fieldset[disabled] .btn-success:hover,
.btn-success.disabled:focus,
.btn-success[disabled]:focus,
fieldset[disabled] .btn-success:focus,
.btn-success.disabled.focus,
.btn-success[disabled].focus,
fieldset[disabled] .btn-success.focus,
.btn-success.disabled:active,
.btn-success[disabled]:active,
fieldset[disabled] .btn-success:active,
.btn-success.disabled.active,
.btn-success[disabled].active,
fieldset[disabled] .btn-success.active {
background-color: #5cb85c;
border-color: #4cae4c;
}
.btn-success .badge {
color: #5cb85c;
background-color: #fff;
}
.btn-info {
color: #fff;
background-color: #5bc0de;
border-color: #46b8da;
}
.btn-info:hover,
.btn-info:focus,
.btn-info.focus,
.btn-info:active,
.btn-info.active,
.open > .dropdown-toggle.btn-info {
color: #fff;
background-color: #31b0d5;
border-color: #269abc;
}
.btn-info:active,
.btn-info.active,
.open > .dropdown-toggle.btn-info {
background-image: none;
}
.btn-info.disabled,
.btn-info[disabled],
fieldset[disabled] .btn-info,
.btn-info.disabled:hover,
.btn-info[disabled]:hover,
fieldset[disabled] .btn-info:hover,
.btn-info.disabled:focus,
.btn-info[disabled]:focus,
fieldset[disabled] .btn-info:focus,
.btn-info.disabled.focus,
.btn-info[disabled].focus,
fieldset[disabled] .btn-info.focus,
.btn-info.disabled:active,
.btn-info[disabled]:active,
fieldset[disabled] .btn-info:active,
.btn-info.disabled.active,
.btn-info[disabled].active,
fieldset[disabled] .btn-info.active {
background-color: #5bc0de;
border-color: #46b8da;
}
.btn-info .badge {
color: #5bc0de;
background-color: #fff;
}
.btn-warning {
color: #fff;
background-color: #f0ad4e;
border-color: #eea236;
}
.btn-warning:hover,
.btn-warning:focus,
.btn-warning.focus,
.btn-warning:active,
.btn-warning.active,
.open > .dropdown-toggle.btn-warning {
color: #fff;
background-color: #ec971f;
border-color: #d58512;
}
.btn-warning:active,
.btn-warning.active,
.open > .dropdown-toggle.btn-warning {
background-image: none;
}
.btn-warning.disabled,
.btn-warning[disabled],
fieldset[disabled] .btn-warning,
.btn-warning.disabled:hover,
.btn-warning[disabled]:hover,
fieldset[disabled] .btn-warning:hover,
.btn-warning.disabled:focus,
.btn-warning[disabled]:focus,
fieldset[disabled] .btn-warning:focus,
.btn-warning.disabled.focus,
.btn-warning[disabled].focus,
fieldset[disabled] .btn-warning.focus,
.btn-warning.disabled:active,
.btn-warning[disabled]:active,
fieldset[disabled] .btn-warning:active,
.btn-warning.disabled.active,
.btn-warning[disabled].active,
fieldset[disabled] .btn-warning.active {
background-color: #f0ad4e;
border-color: #eea236;
}
.btn-warning .badge {
color: #f0ad4e;
background-color: #fff;
}
.btn-danger {
color: #fff;
background-color: #d9534f;
border-color: #d43f3a;
}
.btn-danger:hover,
.btn-danger:focus,
.btn-danger.focus,
.btn-danger:active,
.btn-danger.active,
.open > .dropdown-toggle.btn-danger {
color: #fff;
background-color: #c9302c;
border-color: #ac2925;
}
.btn-danger:active,
.btn-danger.active,
.open > .dropdown-toggle.btn-danger {
background-image: none;
}
.btn-danger.disabled,
.btn-danger[disabled],
fieldset[disabled] .btn-danger,
.btn-danger.disabled:hover,
.btn-danger[disabled]:hover,
fieldset[disabled] .btn-danger:hover,
.btn-danger.disabled:focus,
.btn-danger[disabled]:focus,
fieldset[disabled] .btn-danger:focus,
.btn-danger.disabled.focus,
.btn-danger[disabled].focus,
fieldset[disabled] .btn-danger.focus,
.btn-danger.disabled:active,
.btn-danger[disabled]:active,
fieldset[disabled] .btn-danger:active,
.btn-danger.disabled.active,
.btn-danger[disabled].active,
fieldset[disabled] .btn-danger.active {
background-color: #d9534f;
border-color: #d43f3a;
}
.btn-danger .badge {
color: #d9534f;
background-color: #fff;
}
.btn-link {
font-weight: normal;
color: #337ab7;
border-radius: 0;
}
.btn-link,
.btn-link:active,
.btn-link.active,
.btn-link[disabled],
fieldset[disabled] .btn-link {
background-color: transparent;
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-link,
.btn-link:hover,
.btn-link:focus,
.btn-link:active {
border-color: transparent;
}
.btn-link:hover,
.btn-link:focus {
color: #23527c;
text-decoration: underline;
background-color: transparent;
}
.btn-link[disabled]:hover,
fieldset[disabled] .btn-link:hover,
.btn-link[disabled]:focus,
fieldset[disabled] .btn-link:focus {
color: #777;
text-decoration: none;
}
.btn-lg,
.btn-group-lg > .btn {
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
.btn-sm,
.btn-group-sm > .btn {
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
.btn-xs,
.btn-group-xs > .btn {
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
.btn-block {
display: block;
width: 100%;
}
.btn-block + .btn-block {
margin-top: 5px;
}
input[type="submit"].btn-block,
input[type="reset"].btn-block,
input[type="button"].btn-block {
width: 100%;
}
.fade {
opacity: 0;
-webkit-transition: opacity .15s linear;
-o-transition: opacity .15s linear;
transition: opacity .15s linear;
}
.fade.in {
opacity: 1;
}
.collapse {
display: none;
visibility: hidden;
}
.collapse.in {
display: block;
visibility: visible;
}
tr.collapse.in {
display: table-row;
}
tbody.collapse.in {
display: table-row-group;
}
.collapsing {
position: relative;
height: 0;
overflow: hidden;
-webkit-transition-timing-function: ease;
-o-transition-timing-function: ease;
transition-timing-function: ease;
-webkit-transition-duration: .35s;
-o-transition-duration: .35s;
transition-duration: .35s;
-webkit-transition-property: height, visibility;
-o-transition-property: height, visibility;
transition-property: height, visibility;
}
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 4px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.dropup,
.dropdown {
position: relative;
}
.dropdown-toggle:focus {
outline: 0;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.dropdown-menu.pull-right {
right: 0;
left: auto;
}
.dropdown-menu .divider {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
}
.dropdown-menu > li > a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
color: #fff;
text-decoration: none;
background-color: #337ab7;
outline: 0;
}
.dropdown-menu > .disabled > a,
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
color: #777;
}
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
text-decoration: none;
cursor: not-allowed;
background-color: transparent;
background-image: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.open > .dropdown-menu {
display: block;
}
.open > a {
outline: 0;
}
.dropdown-menu-right {
right: 0;
left: auto;
}
.dropdown-menu-left {
right: auto;
left: 0;
}
.dropdown-header {
display: block;
padding: 3px 20px;
font-size: 12px;
line-height: 1.42857143;
color: #777;
white-space: nowrap;
}
.dropdown-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 990;
}
.pull-right > .dropdown-menu {
right: 0;
left: auto;
}
.dropup .caret,
.navbar-fixed-bottom .dropdown .caret {
content: "";
border-top: 0;
border-bottom: 4px solid;
}
.dropup .dropdown-menu,
.navbar-fixed-bottom .dropdown .dropdown-menu {
top: auto;
bottom: 100%;
margin-bottom: 2px;
}
@media (min-width: 768px) {
.navbar-right .dropdown-menu {
right: 0;
left: auto;
}
.navbar-right .dropdown-menu-left {
right: auto;
left: 0;
}
}
.btn-group,
.btn-group-vertical {
position: relative;
display: inline-block;
vertical-align: middle;
}
.btn-group > .btn,
.btn-group-vertical > .btn {
position: relative;
float: left;
}
.btn-group > .btn:hover,
.btn-group-vertical > .btn:hover,
.btn-group > .btn:focus,
.btn-group-vertical > .btn:focus,
.btn-group > .btn:active,
.btn-group-vertical > .btn:active,
.btn-group > .btn.active,
.btn-group-vertical > .btn.active {
z-index: 2;
}
.btn-group .btn + .btn,
.btn-group .btn + .btn-group,
.btn-group .btn-group + .btn,
.btn-group .btn-group + .btn-group {
margin-left: -1px;
}
.btn-toolbar {
margin-left: -5px;
}
.btn-toolbar .btn-group,
.btn-toolbar .input-group {
float: left;
}
.btn-toolbar > .btn,
.btn-toolbar > .btn-group,
.btn-toolbar > .input-group {
margin-left: 5px;
}
.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
border-radius: 0;
}
.btn-group > .btn:first-child {
margin-left: 0;
}
.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn:last-child:not(:first-child),
.btn-group > .dropdown-toggle:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group > .btn-group {
float: left;
}
.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
border-radius: 0;
}
.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,
.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group .dropdown-toggle:active,
.btn-group.open .dropdown-toggle {
outline: 0;
}
.btn-group > .btn + .dropdown-toggle {
padding-right: 8px;
padding-left: 8px;
}
.btn-group > .btn-lg + .dropdown-toggle {
padding-right: 12px;
padding-left: 12px;
}
.btn-group.open .dropdown-toggle {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-group.open .dropdown-toggle.btn-link {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn .caret {
margin-left: 0;
}
.btn-lg .caret {
border-width: 5px 5px 0;
border-bottom-width: 0;
}
.dropup .btn-lg .caret {
border-width: 0 5px 5px;
}
.btn-group-vertical > .btn,
.btn-group-vertical > .btn-group,
.btn-group-vertical > .btn-group > .btn {
display: block;
float: none;
width: 100%;
max-width: 100%;
}
.btn-group-vertical > .btn-group > .btn {
float: none;
}
.btn-group-vertical > .btn + .btn,
.btn-group-vertical > .btn + .btn-group,
.btn-group-vertical > .btn-group + .btn,
.btn-group-vertical > .btn-group + .btn-group {
margin-top: -1px;
margin-left: 0;
}
.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
border-radius: 0;
}
.btn-group-vertical > .btn:first-child:not(:last-child) {
border-top-right-radius: 4px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group-vertical > .btn:last-child:not(:first-child) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 4px;
}
.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
border-radius: 0;
}
.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,
.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.btn-group-justified {
display: table;
width: 100%;
table-layout: fixed;
border-collapse: separate;
}
.btn-group-justified > .btn,
.btn-group-justified > .btn-group {
display: table-cell;
float: none;
width: 1%;
}
.btn-group-justified > .btn-group .btn {
width: 100%;
}
.btn-group-justified > .btn-group .dropdown-menu {
left: auto;
}
[data-toggle="buttons"] > .btn input[type="radio"],
[data-toggle="buttons"] > .btn-group > .btn input[type="radio"],
[data-toggle="buttons"] > .btn input[type="checkbox"],
[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] {
position: absolute;
clip: rect(0, 0, 0, 0);
pointer-events: none;
}
.input-group {
position: relative;
display: table;
border-collapse: separate;
}
.input-group[class*="col-"] {
float: none;
padding-right: 0;
padding-left: 0;
}
.input-group .form-control {
position: relative;
z-index: 2;
float: left;
width: 100%;
margin-bottom: 0;
}
.input-group-lg > .form-control,
.input-group-lg > .input-group-addon,
.input-group-lg > .input-group-btn > .btn {
height: 46px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
select.input-group-lg > .form-control,
select.input-group-lg > .input-group-addon,
select.input-group-lg > .input-group-btn > .btn {
height: 46px;
line-height: 46px;
}
textarea.input-group-lg > .form-control,
textarea.input-group-lg > .input-group-addon,
textarea.input-group-lg > .input-group-btn > .btn,
select[multiple].input-group-lg > .form-control,
select[multiple].input-group-lg > .input-group-addon,
select[multiple].input-group-lg > .input-group-btn > .btn {
height: auto;
}
.input-group-sm > .form-control,
.input-group-sm > .input-group-addon,
.input-group-sm > .input-group-btn > .btn {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
select.input-group-sm > .form-control,
select.input-group-sm > .input-group-addon,
select.input-group-sm > .input-group-btn > .btn {
height: 30px;
line-height: 30px;
}
textarea.input-group-sm > .form-control,
textarea.input-group-sm > .input-group-addon,
textarea.input-group-sm > .input-group-btn > .btn,
select[multiple].input-group-sm > .form-control,
select[multiple].input-group-sm > .input-group-addon,
select[multiple].input-group-sm > .input-group-btn > .btn {
height: auto;
}
.input-group-addon,
.input-group-btn,
.input-group .form-control {
display: table-cell;
}
.input-group-addon:not(:first-child):not(:last-child),
.input-group-btn:not(:first-child):not(:last-child),
.input-group .form-control:not(:first-child):not(:last-child) {
border-radius: 0;
}
.input-group-addon,
.input-group-btn {
width: 1%;
white-space: nowrap;
vertical-align: middle;
}
.input-group-addon {
padding: 6px 12px;
font-size: 14px;
font-weight: normal;
line-height: 1;
color: #555;
text-align: center;
background-color: #eee;
border: 1px solid #ccc;
border-radius: 4px;
}
.input-group-addon.input-sm {
padding: 5px 10px;
font-size: 12px;
border-radius: 3px;
}
.input-group-addon.input-lg {
padding: 10px 16px;
font-size: 18px;
border-radius: 6px;
}
.input-group-addon input[type="radio"],
.input-group-addon input[type="checkbox"] {
margin-top: 0;
}
.input-group .form-control:first-child,
.input-group-addon:first-child,
.input-group-btn:first-child > .btn,
.input-group-btn:first-child > .btn-group > .btn,
.input-group-btn:first-child > .dropdown-toggle,
.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group-addon:first-child {
border-right: 0;
}
.input-group .form-control:last-child,
.input-group-addon:last-child,
.input-group-btn:last-child > .btn,
.input-group-btn:last-child > .btn-group > .btn,
.input-group-btn:last-child > .dropdown-toggle,
.input-group-btn:first-child > .btn:not(:first-child),
.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group-addon:last-child {
border-left: 0;
}
.input-group-btn {
position: relative;
font-size: 0;
white-space: nowrap;
}
.input-group-btn > .btn {
position: relative;
}
.input-group-btn > .btn + .btn {
margin-left: -1px;
}
.input-group-btn > .btn:hover,
.input-group-btn > .btn:focus,
.input-group-btn > .btn:active {
z-index: 2;
}
.input-group-btn:first-child > .btn,
.input-group-btn:first-child > .btn-group {
margin-right: -1px;
}
.input-group-btn:last-child > .btn,
.input-group-btn:last-child > .btn-group {
margin-left: -1px;
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.nav > li {
position: relative;
display: block;
}
.nav > li > a {
position: relative;
display: block;
padding: 10px 15px;
}
.nav > li > a:hover,
.nav > li > a:focus {
text-decoration: none;
background-color: #eee;
}
.nav > li.disabled > a {
color: #777;
}
.nav > li.disabled > a:hover,
.nav > li.disabled > a:focus {
color: #777;
text-decoration: none;
cursor: not-allowed;
background-color: transparent;
}
.nav .open > a,
.nav .open > a:hover,
.nav .open > a:focus {
background-color: #eee;
border-color: #337ab7;
}
.nav .nav-divider {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
}
.nav > li > a > img {
max-width: none;
}
.nav-tabs {
border-bottom: 1px solid #ddd;
}
.nav-tabs > li {
float: left;
margin-bottom: -1px;
}
.nav-tabs > li > a {
margin-right: 2px;
line-height: 1.42857143;
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
}
.nav-tabs > li > a:hover {
border-color: #eee #eee #ddd;
}
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
color: #555;
cursor: default;
background-color: #fff;
border: 1px solid #ddd;
border-bottom-color: transparent;
}
.nav-tabs.nav-justified {
width: 100%;
border-bottom: 0;
}
.nav-tabs.nav-justified > li {
float: none;
}
.nav-tabs.nav-justified > li > a {
margin-bottom: 5px;
text-align: center;
}
.nav-tabs.nav-justified > .dropdown .dropdown-menu {
top: auto;
left: auto;
}
@media (min-width: 768px) {
.nav-tabs.nav-justified > li {
display: table-cell;
width: 1%;
}
.nav-tabs.nav-justified > li > a {
margin-bottom: 0;
}
}
.nav-tabs.nav-justified > li > a {
margin-right: 0;
border-radius: 4px;
}
.nav-tabs.nav-justified > .active > a,
.nav-tabs.nav-justified > .active > a:hover,
.nav-tabs.nav-justified > .active > a:focus {
border: 1px solid #ddd;
}
@media (min-width: 768px) {
.nav-tabs.nav-justified > li > a {
border-bottom: 1px solid #ddd;
border-radius: 4px 4px 0 0;
}
.nav-tabs.nav-justified > .active > a,
.nav-tabs.nav-justified > .active > a:hover,
.nav-tabs.nav-justified > .active > a:focus {
border-bottom-color: #fff;
}
}
.nav-pills > li {
float: left;
}
.nav-pills > li > a {
border-radius: 4px;
}
.nav-pills > li + li {
margin-left: 2px;
}
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover,
.nav-pills > li.active > a:focus {
color: #fff;
background-color: #337ab7;
}
.nav-stacked > li {
float: none;
}
.nav-stacked > li + li {
margin-top: 2px;
margin-left: 0;
}
.nav-justified {
width: 100%;
}
.nav-justified > li {
float: none;
}
.nav-justified > li > a {
margin-bottom: 5px;
text-align: center;
}
.nav-justified > .dropdown .dropdown-menu {
top: auto;
left: auto;
}
@media (min-width: 768px) {
.nav-justified > li {
display: table-cell;
width: 1%;
}
.nav-justified > li > a {
margin-bottom: 0;
}
}
.nav-tabs-justified {
border-bottom: 0;
}
.nav-tabs-justified > li > a {
margin-right: 0;
border-radius: 4px;
}
.nav-tabs-justified > .active > a,
.nav-tabs-justified > .active > a:hover,
.nav-tabs-justified > .active > a:focus {
border: 1px solid #ddd;
}
@media (min-width: 768px) {
.nav-tabs-justified > li > a {
border-bottom: 1px solid #ddd;
border-radius: 4px 4px 0 0;
}
.nav-tabs-justified > .active > a,
.nav-tabs-justified > .active > a:hover,
.nav-tabs-justified > .active > a:focus {
border-bottom-color: #fff;
}
}
.tab-content > .tab-pane {
display: none;
visibility: hidden;
}
.tab-content > .active {
display: block;
visibility: visible;
}
.nav-tabs .dropdown-menu {
margin-top: -1px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.navbar {
position: relative;
min-height: 50px;
margin-bottom: 20px;
border: 1px solid transparent;
}
@media (min-width: 768px) {
.navbar {
border-radius: 4px;
}
}
@media (min-width: 768px) {
.navbar-header {
float: left;
}
}
.navbar-collapse {
padding-right: 15px;
padding-left: 15px;
overflow-x: visible;
-webkit-overflow-scrolling: touch;
border-top: 1px solid transparent;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
}
.navbar-collapse.in {
overflow-y: auto;
}
@media (min-width: 768px) {
.navbar-collapse {
width: auto;
border-top: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.navbar-collapse.collapse {
display: block !important;
height: auto !important;
padding-bottom: 0;
overflow: visible !important;
visibility: visible !important;
}
.navbar-collapse.in {
overflow-y: visible;
}
.navbar-fixed-top .navbar-collapse,
.navbar-static-top .navbar-collapse,
.navbar-fixed-bottom .navbar-collapse {
padding-right: 0;
padding-left: 0;
}
}
.navbar-fixed-top .navbar-collapse,
.navbar-fixed-bottom .navbar-collapse {
max-height: 340px;
}
@media (max-device-width: 480px) and (orientation: landscape) {
.navbar-fixed-top .navbar-collapse,
.navbar-fixed-bottom .navbar-collapse {
max-height: 200px;
}
}
.container > .navbar-header,
.container-fluid > .navbar-header,
.container > .navbar-collapse,
.container-fluid > .navbar-collapse {
margin-right: -15px;
margin-left: -15px;
}
@media (min-width: 768px) {
.container > .navbar-header,
.container-fluid > .navbar-header,
.container > .navbar-collapse,
.container-fluid > .navbar-collapse {
margin-right: 0;
margin-left: 0;
}
}
.navbar-static-top {
z-index: 1000;
border-width: 0 0 1px;
}
@media (min-width: 768px) {
.navbar-static-top {
border-radius: 0;
}
}
.navbar-fixed-top,
.navbar-fixed-bottom {
position: fixed;
right: 0;
left: 0;
z-index: 1030;
}
@media (min-width: 768px) {
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
}
.navbar-fixed-top {
top: 0;
border-width: 0 0 1px;
}
.navbar-fixed-bottom {
bottom: 0;
margin-bottom: 0;
border-width: 1px 0 0;
}
.navbar-brand {
float: left;
height: 50px;
padding: 15px 15px;
font-size: 18px;
line-height: 20px;
}
.navbar-brand:hover,
.navbar-brand:focus {
text-decoration: none;
}
.navbar-brand > img {
display: block;
}
@media (min-width: 768px) {
.navbar > .container .navbar-brand,
.navbar > .container-fluid .navbar-brand {
margin-left: -15px;
}
}
.navbar-toggle {
position: relative;
float: right;
padding: 9px 10px;
margin-top: 8px;
margin-right: 15px;
margin-bottom: 8px;
background-color: transparent;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.navbar-toggle:focus {
outline: 0;
}
.navbar-toggle .icon-bar {
display: block;
width: 22px;
height: 2px;
border-radius: 1px;
}
.navbar-toggle .icon-bar + .icon-bar {
margin-top: 4px;
}
@media (min-width: 768px) {
.navbar-toggle {
display: none;
}
}
.navbar-nav {
margin: 7.5px -15px;
}
.navbar-nav > li > a {
padding-top: 10px;
padding-bottom: 10px;
line-height: 20px;
}
@media (max-width: 767px) {
.navbar-nav .open .dropdown-menu {
position: static;
float: none;
width: auto;
margin-top: 0;
background-color: transparent;
border: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.navbar-nav .open .dropdown-menu > li > a,
.navbar-nav .open .dropdown-menu .dropdown-header {
padding: 5px 15px 5px 25px;
}
.navbar-nav .open .dropdown-menu > li > a {
line-height: 20px;
}
.navbar-nav .open .dropdown-menu > li > a:hover,
.navbar-nav .open .dropdown-menu > li > a:focus {
background-image: none;
}
}
@media (min-width: 768px) {
.navbar-nav {
float: left;
margin: 0;
}
.navbar-nav > li {
float: left;
}
.navbar-nav > li > a {
padding-top: 15px;
padding-bottom: 15px;
}
}
.navbar-form {
padding: 10px 15px;
margin-top: 8px;
margin-right: -15px;
margin-bottom: 8px;
margin-left: -15px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
}
@media (min-width: 768px) {
.navbar-form .form-group {
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
}
.navbar-form .form-control {
display: inline-block;
width: auto;
vertical-align: middle;
}
.navbar-form .form-control-static {
display: inline-block;
}
.navbar-form .input-group {
display: inline-table;
vertical-align: middle;
}
.navbar-form .input-group .input-group-addon,
.navbar-form .input-group .input-group-btn,
.navbar-form .input-group .form-control {
width: auto;
}
.navbar-form .input-group > .form-control {
width: 100%;
}
.navbar-form .control-label {
margin-bottom: 0;
vertical-align: middle;
}
.navbar-form .radio,
.navbar-form .checkbox {
display: inline-block;
margin-top: 0;
margin-bottom: 0;
vertical-align: middle;
}
.navbar-form .radio label,
.navbar-form .checkbox label {
padding-left: 0;
}
.navbar-form .radio input[type="radio"],
.navbar-form .checkbox input[type="checkbox"] {
position: relative;
margin-left: 0;
}
.navbar-form .has-feedback .form-control-feedback {
top: 0;
}
}
@media (max-width: 767px) {
.navbar-form .form-group {
margin-bottom: 5px;
}
.navbar-form .form-group:last-child {
margin-bottom: 0;
}
}
@media (min-width: 768px) {
.navbar-form {
width: auto;
padding-top: 0;
padding-bottom: 0;
margin-right: 0;
margin-left: 0;
border: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
}
.navbar-nav > li > .dropdown-menu {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
margin-bottom: 0;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.navbar-btn {
margin-top: 8px;
margin-bottom: 8px;
}
.navbar-btn.btn-sm {
margin-top: 10px;
margin-bottom: 10px;
}
.navbar-btn.btn-xs {
margin-top: 14px;
margin-bottom: 14px;
}
.navbar-text {
margin-top: 15px;
margin-bottom: 15px;
}
@media (min-width: 768px) {
.navbar-text {
float: left;
margin-right: 15px;
margin-left: 15px;
}
}
@media (min-width: 768px) {
.navbar-left {
float: left !important;
}
.navbar-right {
float: right !important;
margin-right: -15px;
}
.navbar-right ~ .navbar-right {
margin-right: 0;
}
}
.navbar-default {
background-color: #f8f8f8;
border-color: #e7e7e7;
}
.navbar-default .navbar-brand {
color: #777;
}
.navbar-default .navbar-brand:hover,
.navbar-default .navbar-brand:focus {
color: #5e5e5e;
background-color: transparent;
}
.navbar-default .navbar-text {
color: #777;
}
.navbar-default .navbar-nav > li > a {
color: #777;
}
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
color: #333;
background-color: transparent;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
color: #555;
background-color: #e7e7e7;
}
.navbar-default .navbar-nav > .disabled > a,
.navbar-default .navbar-nav > .disabled > a:hover,
.navbar-default .navbar-nav > .disabled > a:focus {
color: #ccc;
background-color: transparent;
}
.navbar-default .navbar-toggle {
border-color: #ddd;
}
.navbar-default .navbar-toggle:hover,
.navbar-default .navbar-toggle:focus {
background-color: #ddd;
}
.navbar-default .navbar-toggle .icon-bar {
background-color: #888;
}
.navbar-default .navbar-collapse,
.navbar-default .navbar-form {
border-color: #e7e7e7;
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a:focus {
color: #555;
background-color: #e7e7e7;
}
@media (max-width: 767px) {
.navbar-default .navbar-nav .open .dropdown-menu > li > a {
color: #777;
}
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
color: #333;
background-color: transparent;
}
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #555;
background-color: #e7e7e7;
}
.navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,
.navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
color: #ccc;
background-color: transparent;
}
}
.navbar-default .navbar-link {
color: #777;
}
.navbar-default .navbar-link:hover {
color: #333;
}
.navbar-default .btn-link {
color: #777;
}
.navbar-default .btn-link:hover,
.navbar-default .btn-link:focus {
color: #333;
}
.navbar-default .btn-link[disabled]:hover,
fieldset[disabled] .navbar-default .btn-link:hover,
.navbar-default .btn-link[disabled]:focus,
fieldset[disabled] .navbar-default .btn-link:focus {
color: #ccc;
}
.navbar-inverse {
background-color: #222;
border-color: #080808;
}
.navbar-inverse .navbar-brand {
color: #9d9d9d;
}
.navbar-inverse .navbar-brand:hover,
.navbar-inverse .navbar-brand:focus {
color: #fff;
background-color: transparent;
}
.navbar-inverse .navbar-text {
color: #9d9d9d;
}
.navbar-inverse .navbar-nav > li > a {
color: #9d9d9d;
}
.navbar-inverse .navbar-nav > li > a:hover,
.navbar-inverse .navbar-nav > li > a:focus {
color: #fff;
background-color: transparent;
}
.navbar-inverse .navbar-nav > .active > a,
.navbar-inverse .navbar-nav > .active > a:hover,
.navbar-inverse .navbar-nav > .active > a:focus {
color: #fff;
background-color: #080808;
}
.navbar-inverse .navbar-nav > .disabled > a,
.navbar-inverse .navbar-nav > .disabled > a:hover,
.navbar-inverse .navbar-nav > .disabled > a:focus {
color: #444;
background-color: transparent;
}
.navbar-inverse .navbar-toggle {
border-color: #333;
}
.navbar-inverse .navbar-toggle:hover,
.navbar-inverse .navbar-toggle:focus {
background-color: #333;
}
.navbar-inverse .navbar-toggle .icon-bar {
background-color: #fff;
}
.navbar-inverse .navbar-collapse,
.navbar-inverse .navbar-form {
border-color: #101010;
}
.navbar-inverse .navbar-nav > .open > a,
.navbar-inverse .navbar-nav > .open > a:hover,
.navbar-inverse .navbar-nav > .open > a:focus {
color: #fff;
background-color: #080808;
}
@media (max-width: 767px) {
.navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
border-color: #080808;
}
.navbar-inverse .navbar-nav .open .dropdown-menu .divider {
background-color: #080808;
}
.navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
color: #9d9d9d;
}
.navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,
.navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
color: #fff;
background-color: transparent;
}
.navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,
.navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #fff;
background-color: #080808;
}
.navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,
.navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,
.navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
color: #444;
background-color: transparent;
}
}
.navbar-inverse .navbar-link {
color: #9d9d9d;
}
.navbar-inverse .navbar-link:hover {
color: #fff;
}
.navbar-inverse .btn-link {
color: #9d9d9d;
}
.navbar-inverse .btn-link:hover,
.navbar-inverse .btn-link:focus {
color: #fff;
}
.navbar-inverse .btn-link[disabled]:hover,
fieldset[disabled] .navbar-inverse .btn-link:hover,
.navbar-inverse .btn-link[disabled]:focus,
fieldset[disabled] .navbar-inverse .btn-link:focus {
color: #444;
}
.breadcrumb {
padding: 8px 15px;
margin-bottom: 20px;
list-style: none;
background-color: #f5f5f5;
border-radius: 4px;
}
.breadcrumb > li {
display: inline-block;
}
.breadcrumb > li + li:before {
padding: 0 5px;
color: #ccc;
content: "/\00a0";
}
.breadcrumb > .active {
color: #777;
}
.pagination {
display: inline-block;
padding-left: 0;
margin: 20px 0;
border-radius: 4px;
}
.pagination > li {
display: inline;
}
.pagination > li > a,
.pagination > li > span {
position: relative;
float: left;
padding: 6px 12px;
margin-left: -1px;
line-height: 1.42857143;
color: #337ab7;
text-decoration: none;
background-color: #fff;
border: 1px solid #ddd;
}
.pagination > li:first-child > a,
.pagination > li:first-child > span {
margin-left: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.pagination > li:last-child > a,
.pagination > li:last-child > span {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.pagination > li > a:hover,
.pagination > li > span:hover,
.pagination > li > a:focus,
.pagination > li > span:focus {
color: #23527c;
background-color: #eee;
border-color: #ddd;
}
.pagination > .active > a,
.pagination > .active > span,
.pagination > .active > a:hover,
.pagination > .active > span:hover,
.pagination > .active > a:focus,
.pagination > .active > span:focus {
z-index: 2;
color: #fff;
cursor: default;
background-color: #337ab7;
border-color: #337ab7;
}
.pagination > .disabled > span,
.pagination > .disabled > span:hover,
.pagination > .disabled > span:focus,
.pagination > .disabled > a,
.pagination > .disabled > a:hover,
.pagination > .disabled > a:focus {
color: #777;
cursor: not-allowed;
background-color: #fff;
border-color: #ddd;
}
.pagination-lg > li > a,
.pagination-lg > li > span {
padding: 10px 16px;
font-size: 18px;
}
.pagination-lg > li:first-child > a,
.pagination-lg > li:first-child > span {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.pagination-lg > li:last-child > a,
.pagination-lg > li:last-child > span {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.pagination-sm > li > a,
.pagination-sm > li > span {
padding: 5px 10px;
font-size: 12px;
}
.pagination-sm > li:first-child > a,
.pagination-sm > li:first-child > span {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.pagination-sm > li:last-child > a,
.pagination-sm > li:last-child > span {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.pager {
padding-left: 0;
margin: 20px 0;
text-align: center;
list-style: none;
}
.pager li {
display: inline;
}
.pager li > a,
.pager li > span {
display: inline-block;
padding: 5px 14px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 15px;
}
.pager li > a:hover,
.pager li > a:focus {
text-decoration: none;
background-color: #eee;
}
.pager .next > a,
.pager .next > span {
float: right;
}
.pager .previous > a,
.pager .previous > span {
float: left;
}
.pager .disabled > a,
.pager .disabled > a:hover,
.pager .disabled > a:focus,
.pager .disabled > span {
color: #777;
cursor: not-allowed;
background-color: #fff;
}
.label {
display: inline;
padding: .2em .6em .3em;
font-size: 75%;
font-weight: bold;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
}
a.label:hover,
a.label:focus {
color: #fff;
text-decoration: none;
cursor: pointer;
}
.label:empty {
display: none;
}
.btn .label {
position: relative;
top: -1px;
}
.label-default {
background-color: #777;
}
.label-default[href]:hover,
.label-default[href]:focus {
background-color: #5e5e5e;
}
.label-primary {
background-color: #337ab7;
}
.label-primary[href]:hover,
.label-primary[href]:focus {
background-color: #286090;
}
.label-success {
background-color: #5cb85c;
}
.label-success[href]:hover,
.label-success[href]:focus {
background-color: #449d44;
}
.label-info {
background-color: #5bc0de;
}
.label-info[href]:hover,
.label-info[href]:focus {
background-color: #31b0d5;
}
.label-warning {
background-color: #f0ad4e;
}
.label-warning[href]:hover,
.label-warning[href]:focus {
background-color: #ec971f;
}
.label-danger {
background-color: #d9534f;
}
.label-danger[href]:hover,
.label-danger[href]:focus {
background-color: #c9302c;
}
.badge {
display: inline-block;
min-width: 10px;
padding: 3px 7px;
font-size: 12px;
font-weight: bold;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
background-color: #777;
border-radius: 10px;
}
.badge:empty {
display: none;
}
.btn .badge {
position: relative;
top: -1px;
}
.btn-xs .badge {
top: 0;
padding: 1px 5px;
}
a.badge:hover,
a.badge:focus {
color: #fff;
text-decoration: none;
cursor: pointer;
}
.list-group-item.active > .badge,
.nav-pills > .active > a > .badge {
color: #337ab7;
background-color: #fff;
}
.list-group-item > .badge {
float: right;
}
.list-group-item > .badge + .badge {
margin-right: 5px;
}
.nav-pills > li > a > .badge {
margin-left: 3px;
}
.jumbotron {
padding: 30px 15px;
margin-bottom: 30px;
color: inherit;
background-color: #eee;
}
.jumbotron h1,
.jumbotron .h1 {
color: inherit;
}
.jumbotron p {
margin-bottom: 15px;
font-size: 21px;
font-weight: 200;
}
.jumbotron > hr {
border-top-color: #d5d5d5;
}
.container .jumbotron,
.container-fluid .jumbotron {
border-radius: 6px;
}
.jumbotron .container {
max-width: 100%;
}
@media screen and (min-width: 768px) {
.jumbotron {
padding: 48px 0;
}
.container .jumbotron,
.container-fluid .jumbotron {
padding-right: 60px;
padding-left: 60px;
}
.jumbotron h1,
.jumbotron .h1 {
font-size: 63px;
}
}
.thumbnail {
display: block;
padding: 4px;
margin-bottom: 20px;
line-height: 1.42857143;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
-webkit-transition: border .2s ease-in-out;
-o-transition: border .2s ease-in-out;
transition: border .2s ease-in-out;
}
.thumbnail > img,
.thumbnail a > img {
margin-right: auto;
margin-left: auto;
}
a.thumbnail:hover,
a.thumbnail:focus,
a.thumbnail.active {
border-color: #337ab7;
}
.thumbnail .caption {
padding: 9px;
color: #333;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert h4 {
margin-top: 0;
color: inherit;
}
.alert .alert-link {
font-weight: bold;
}
.alert > p,
.alert > ul {
margin-bottom: 0;
}
.alert > p + p {
margin-top: 5px;
}
.alert-dismissable,
.alert-dismissible {
padding-right: 35px;
}
.alert-dismissable .close,
.alert-dismissible .close {
position: relative;
top: -2px;
right: -21px;
color: inherit;
}
.alert-success {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.alert-success hr {
border-top-color: #c9e2b3;
}
.alert-success .alert-link {
color: #2b542c;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.alert-info hr {
border-top-color: #a6e1ec;
}
.alert-info .alert-link {
color: #245269;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.alert-warning hr {
border-top-color: #f7e1b5;
}
.alert-warning .alert-link {
color: #66512c;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert-danger hr {
border-top-color: #e4b9c0;
}
.alert-danger .alert-link {
color: #843534;
}
@-webkit-keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
@-o-keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
@keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
.progress {
height: 20px;
margin-bottom: 20px;
overflow: hidden;
background-color: #f5f5f5;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
}
.progress-bar {
float: left;
width: 0;
height: 100%;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background-color: #337ab7;
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
-webkit-transition: width .6s ease;
-o-transition: width .6s ease;
transition: width .6s ease;
}
.progress-striped .progress-bar,
.progress-bar-striped {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
-webkit-background-size: 40px 40px;
background-size: 40px 40px;
}
.progress.active .progress-bar,
.progress-bar.active {
-webkit-animation: progress-bar-stripes 2s linear infinite;
-o-animation: progress-bar-stripes 2s linear infinite;
animation: progress-bar-stripes 2s linear infinite;
}
.progress-bar-success {
background-color: #5cb85c;
}
.progress-striped .progress-bar-success {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.progress-bar-info {
background-color: #5bc0de;
}
.progress-striped .progress-bar-info {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.progress-bar-warning {
background-color: #f0ad4e;
}
.progress-striped .progress-bar-warning {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.progress-bar-danger {
background-color: #d9534f;
}
.progress-striped .progress-bar-danger {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
}
.media {
margin-top: 15px;
}
.media:first-child {
margin-top: 0;
}
.media,
.media-body {
overflow: hidden;
zoom: 1;
}
.media-body {
width: 10000px;
}
.media-object {
display: block;
}
.media-right,
.media > .pull-right {
padding-left: 10px;
}
.media-left,
.media > .pull-left {
padding-right: 10px;
}
.media-left,
.media-right,
.media-body {
display: table-cell;
vertical-align: top;
}
.media-middle {
vertical-align: middle;
}
.media-bottom {
vertical-align: bottom;
}
.media-heading {
margin-top: 0;
margin-bottom: 5px;
}
.media-list {
padding-left: 0;
list-style: none;
}
.list-group {
padding-left: 0;
margin-bottom: 20px;
}
.list-group-item {
position: relative;
display: block;
padding: 10px 15px;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid #ddd;
}
.list-group-item:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.list-group-item:last-child {
margin-bottom: 0;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
a.list-group-item {
color: #555;
}
a.list-group-item .list-group-item-heading {
color: #333;
}
a.list-group-item:hover,
a.list-group-item:focus {
color: #555;
text-decoration: none;
background-color: #f5f5f5;
}
.list-group-item.disabled,
.list-group-item.disabled:hover,
.list-group-item.disabled:focus {
color: #777;
cursor: not-allowed;
background-color: #eee;
}
.list-group-item.disabled .list-group-item-heading,
.list-group-item.disabled:hover .list-group-item-heading,
.list-group-item.disabled:focus .list-group-item-heading {
color: inherit;
}
.list-group-item.disabled .list-group-item-text,
.list-group-item.disabled:hover .list-group-item-text,
.list-group-item.disabled:focus .list-group-item-text {
color: #777;
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
z-index: 2;
color: #fff;
background-color: #337ab7;
border-color: #337ab7;
}
.list-group-item.active .list-group-item-heading,
.list-group-item.active:hover .list-group-item-heading,
.list-group-item.active:focus .list-group-item-heading,
.list-group-item.active .list-group-item-heading > small,
.list-group-item.active:hover .list-group-item-heading > small,
.list-group-item.active:focus .list-group-item-heading > small,
.list-group-item.active .list-group-item-heading > .small,
.list-group-item.active:hover .list-group-item-heading > .small,
.list-group-item.active:focus .list-group-item-heading > .small {
color: inherit;
}
.list-group-item.active .list-group-item-text,
.list-group-item.active:hover .list-group-item-text,
.list-group-item.active:focus .list-group-item-text {
color: #c7ddef;
}
.list-group-item-success {
color: #3c763d;
background-color: #dff0d8;
}
a.list-group-item-success {
color: #3c763d;
}
a.list-group-item-success .list-group-item-heading {
color: inherit;
}
a.list-group-item-success:hover,
a.list-group-item-success:focus {
color: #3c763d;
background-color: #d0e9c6;
}
a.list-group-item-success.active,
a.list-group-item-success.active:hover,
a.list-group-item-success.active:focus {
color: #fff;
background-color: #3c763d;
border-color: #3c763d;
}
.list-group-item-info {
color: #31708f;
background-color: #d9edf7;
}
a.list-group-item-info {
color: #31708f;
}
a.list-group-item-info .list-group-item-heading {
color: inherit;
}
a.list-group-item-info:hover,
a.list-group-item-info:focus {
color: #31708f;
background-color: #c4e3f3;
}
a.list-group-item-info.active,
a.list-group-item-info.active:hover,
a.list-group-item-info.active:focus {
color: #fff;
background-color: #31708f;
border-color: #31708f;
}
.list-group-item-warning {
color: #8a6d3b;
background-color: #fcf8e3;
}
a.list-group-item-warning {
color: #8a6d3b;
}
a.list-group-item-warning .list-group-item-heading {
color: inherit;
}
a.list-group-item-warning:hover,
a.list-group-item-warning:focus {
color: #8a6d3b;
background-color: #faf2cc;
}
a.list-group-item-warning.active,
a.list-group-item-warning.active:hover,
a.list-group-item-warning.active:focus {
color: #fff;
background-color: #8a6d3b;
border-color: #8a6d3b;
}
.list-group-item-danger {
color: #a94442;
background-color: #f2dede;
}
a.list-group-item-danger {
color: #a94442;
}
a.list-group-item-danger .list-group-item-heading {
color: inherit;
}
a.list-group-item-danger:hover,
a.list-group-item-danger:focus {
color: #a94442;
background-color: #ebcccc;
}
a.list-group-item-danger.active,
a.list-group-item-danger.active:hover,
a.list-group-item-danger.active:focus {
color: #fff;
background-color: #a94442;
border-color: #a94442;
}
.list-group-item-heading {
margin-top: 0;
margin-bottom: 5px;
}
.list-group-item-text {
margin-bottom: 0;
line-height: 1.3;
}
.panel {
margin-bottom: 20px;
background-color: #fff;
border: 1px solid transparent;
border-radius: 4px;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
}
.panel-body {
padding: 15px;
}
.panel-heading {
padding: 10px 15px;
border-bottom: 1px solid transparent;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.panel-heading > .dropdown .dropdown-toggle {
color: inherit;
}
.panel-title {
margin-top: 0;
margin-bottom: 0;
font-size: 16px;
color: inherit;
}
.panel-title > a,
.panel-title > small,
.panel-title > .small,
.panel-title > small > a,
.panel-title > .small > a {
color: inherit;
}
.panel-footer {
padding: 10px 15px;
background-color: #f5f5f5;
border-top: 1px solid #ddd;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.panel > .list-group,
.panel > .panel-collapse > .list-group {
margin-bottom: 0;
}
.panel > .list-group .list-group-item,
.panel > .panel-collapse > .list-group .list-group-item {
border-width: 1px 0;
border-radius: 0;
}
.panel > .list-group:first-child .list-group-item:first-child,
.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {
border-top: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.panel > .list-group:last-child .list-group-item:last-child,
.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {
border-bottom: 0;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.panel-heading + .list-group .list-group-item:first-child {
border-top-width: 0;
}
.list-group + .panel-footer {
border-top-width: 0;
}
.panel > .table,
.panel > .table-responsive > .table,
.panel > .panel-collapse > .table {
margin-bottom: 0;
}
.panel > .table caption,
.panel > .table-responsive > .table caption,
.panel > .panel-collapse > .table caption {
padding-right: 15px;
padding-left: 15px;
}
.panel > .table:first-child,
.panel > .table-responsive:first-child > .table:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.panel > .table:first-child > thead:first-child > tr:first-child,
.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,
.panel > .table:first-child > tbody:first-child > tr:first-child,
.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,
.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,
.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,
.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,
.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,
.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,
.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,
.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
border-top-left-radius: 3px;
}
.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,
.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,
.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,
.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,
.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,
.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,
.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,
.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
border-top-right-radius: 3px;
}
.panel > .table:last-child,
.panel > .table-responsive:last-child > .table:last-child {
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.panel > .table:last-child > tbody:last-child > tr:last-child,
.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,
.panel > .table:last-child > tfoot:last-child > tr:last-child,
.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,
.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,
.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,
.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,
.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,
.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
border-bottom-left-radius: 3px;
}
.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,
.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,
.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,
.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,
.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,
.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
border-bottom-right-radius: 3px;
}
.panel > .panel-body + .table,
.panel > .panel-body + .table-responsive,
.panel > .table + .panel-body,
.panel > .table-responsive + .panel-body {
border-top: 1px solid #ddd;
}
.panel > .table > tbody:first-child > tr:first-child th,
.panel > .table > tbody:first-child > tr:first-child td {
border-top: 0;
}
.panel > .table-bordered,
.panel > .table-responsive > .table-bordered {
border: 0;
}
.panel > .table-bordered > thead > tr > th:first-child,
.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
.panel > .table-bordered > tbody > tr > th:first-child,
.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
.panel > .table-bordered > tfoot > tr > th:first-child,
.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
.panel > .table-bordered > thead > tr > td:first-child,
.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
.panel > .table-bordered > tbody > tr > td:first-child,
.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
.panel > .table-bordered > tfoot > tr > td:first-child,
.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
border-left: 0;
}
.panel > .table-bordered > thead > tr > th:last-child,
.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
.panel > .table-bordered > tbody > tr > th:last-child,
.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
.panel > .table-bordered > tfoot > tr > th:last-child,
.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
.panel > .table-bordered > thead > tr > td:last-child,
.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
.panel > .table-bordered > tbody > tr > td:last-child,
.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
.panel > .table-bordered > tfoot > tr > td:last-child,
.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
border-right: 0;
}
.panel > .table-bordered > thead > tr:first-child > td,
.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,
.panel > .table-bordered > tbody > tr:first-child > td,
.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,
.panel > .table-bordered > thead > tr:first-child > th,
.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,
.panel > .table-bordered > tbody > tr:first-child > th,
.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {
border-bottom: 0;
}
.panel > .table-bordered > tbody > tr:last-child > td,
.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,
.panel > .table-bordered > tfoot > tr:last-child > td,
.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,
.panel > .table-bordered > tbody > tr:last-child > th,
.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,
.panel > .table-bordered > tfoot > tr:last-child > th,
.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {
border-bottom: 0;
}
.panel > .table-responsive {
margin-bottom: 0;
border: 0;
}
.panel-group {
margin-bottom: 20px;
}
.panel-group .panel {
margin-bottom: 0;
border-radius: 4px;
}
.panel-group .panel + .panel {
margin-top: 5px;
}
.panel-group .panel-heading {
border-bottom: 0;
}
.panel-group .panel-heading + .panel-collapse > .panel-body,
.panel-group .panel-heading + .panel-collapse > .list-group {
border-top: 1px solid #ddd;
}
.panel-group .panel-footer {
border-top: 0;
}
.panel-group .panel-footer + .panel-collapse .panel-body {
border-bottom: 1px solid #ddd;
}
.panel-default {
border-color: #ddd;
}
.panel-default > .panel-heading {
color: #333;
background-color: #f5f5f5;
border-color: #ddd;
}
.panel-default > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #ddd;
}
.panel-default > .panel-heading .badge {
color: #f5f5f5;
background-color: #333;
}
.panel-default > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #ddd;
}
.panel-primary {
border-color: #337ab7;
}
.panel-primary > .panel-heading {
color: #fff;
background-color: #337ab7;
border-color: #337ab7;
}
.panel-primary > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #337ab7;
}
.panel-primary > .panel-heading .badge {
color: #337ab7;
background-color: #fff;
}
.panel-primary > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #337ab7;
}
.panel-success {
border-color: #d6e9c6;
}
.panel-success > .panel-heading {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.panel-success > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #d6e9c6;
}
.panel-success > .panel-heading .badge {
color: #dff0d8;
background-color: #3c763d;
}
.panel-success > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #d6e9c6;
}
.panel-info {
border-color: #bce8f1;
}
.panel-info > .panel-heading {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.panel-info > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #bce8f1;
}
.panel-info > .panel-heading .badge {
color: #d9edf7;
background-color: #31708f;
}
.panel-info > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #bce8f1;
}
.panel-warning {
border-color: #faebcc;
}
.panel-warning > .panel-heading {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.panel-warning > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #faebcc;
}
.panel-warning > .panel-heading .badge {
color: #fcf8e3;
background-color: #8a6d3b;
}
.panel-warning > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #faebcc;
}
.panel-danger {
border-color: #ebccd1;
}
.panel-danger > .panel-heading {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.panel-danger > .panel-heading + .panel-collapse > .panel-body {
border-top-color: #ebccd1;
}
.panel-danger > .panel-heading .badge {
color: #f2dede;
background-color: #a94442;
}
.panel-danger > .panel-footer + .panel-collapse > .panel-body {
border-bottom-color: #ebccd1;
}
.embed-responsive {
position: relative;
display: block;
height: 0;
padding: 0;
overflow: hidden;
}
.embed-responsive .embed-responsive-item,
.embed-responsive iframe,
.embed-responsive embed,
.embed-responsive object,
.embed-responsive video {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
.embed-responsive.embed-responsive-16by9 {
padding-bottom: 56.25%;
}
.embed-responsive.embed-responsive-4by3 {
padding-bottom: 75%;
}
.well {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
.well blockquote {
border-color: #ddd;
border-color: rgba(0, 0, 0, .15);
}
.well-lg {
padding: 24px;
border-radius: 6px;
}
.well-sm {
padding: 9px;
border-radius: 3px;
}
.close {
float: right;
font-size: 21px;
font-weight: bold;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .2;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
filter: alpha(opacity=50);
opacity: .5;
}
button.close {
-webkit-appearance: none;
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
}
.modal-open {
overflow: hidden;
}
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
display: none;
overflow: hidden;
-webkit-overflow-scrolling: touch;
outline: 0;
}
.modal.fade .modal-dialog {
-webkit-transition: -webkit-transform .3s ease-out;
-o-transition: -o-transform .3s ease-out;
transition: transform .3s ease-out;
-webkit-transform: translate(0, -25%);
-ms-transform: translate(0, -25%);
-o-transform: translate(0, -25%);
transform: translate(0, -25%);
}
.modal.in .modal-dialog {
-webkit-transform: translate(0, 0);
-ms-transform: translate(0, 0);
-o-transform: translate(0, 0);
transform: translate(0, 0);
}
.modal-open .modal {
overflow-x: hidden;
overflow-y: auto;
}
.modal-dialog {
position: relative;
width: auto;
margin: 10px;
}
.modal-content {
position: relative;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #999;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 6px;
outline: 0;
-webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5);
box-shadow: 0 3px 9px rgba(0, 0, 0, .5);
}
.modal-backdrop {
position: absolute;
top: 0;
right: 0;
left: 0;
background-color: #000;
}
.modal-backdrop.fade {
filter: alpha(opacity=0);
opacity: 0;
}
.modal-backdrop.in {
filter: alpha(opacity=50);
opacity: .5;
}
.modal-header {
min-height: 16.42857143px;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
}
.modal-header .close {
margin-top: -2px;
}
.modal-title {
margin: 0;
line-height: 1.42857143;
}
.modal-body {
position: relative;
padding: 15px;
}
.modal-footer {
padding: 15px;
text-align: right;
border-top: 1px solid #e5e5e5;
}
.modal-footer .btn + .btn {
margin-bottom: 0;
margin-left: 5px;
}
.modal-footer .btn-group .btn + .btn {
margin-left: -1px;
}
.modal-footer .btn-block + .btn-block {
margin-left: 0;
}
.modal-scrollbar-measure {
position: absolute;
top: -9999px;
width: 50px;
height: 50px;
overflow: scroll;
}
@media (min-width: 768px) {
.modal-dialog {
width: 600px;
margin: 30px auto;
}
.modal-content {
-webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
}
.modal-sm {
width: 300px;
}
}
@media (min-width: 992px) {
.modal-lg {
width: 900px;
}
}
.tooltip {
position: absolute;
z-index: 1070;
display: block;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 12px;
font-weight: normal;
line-height: 1.4;
visibility: visible;
filter: alpha(opacity=0);
opacity: 0;
}
.tooltip.in {
filter: alpha(opacity=90);
opacity: .9;
}
.tooltip.top {
padding: 5px 0;
margin-top: -3px;
}
.tooltip.right {
padding: 0 5px;
margin-left: 3px;
}
.tooltip.bottom {
padding: 5px 0;
margin-top: 3px;
}
.tooltip.left {
padding: 0 5px;
margin-left: -3px;
}
.tooltip-inner {
max-width: 200px;
padding: 3px 8px;
color: #fff;
text-align: center;
text-decoration: none;
background-color: #000;
border-radius: 4px;
}
.tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.tooltip.top .tooltip-arrow {
bottom: 0;
left: 50%;
margin-left: -5px;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.top-left .tooltip-arrow {
right: 5px;
bottom: 0;
margin-bottom: -5px;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.top-right .tooltip-arrow {
bottom: 0;
left: 5px;
margin-bottom: -5px;
border-width: 5px 5px 0;
border-top-color: #000;
}
.tooltip.right .tooltip-arrow {
top: 50%;
left: 0;
margin-top: -5px;
border-width: 5px 5px 5px 0;
border-right-color: #000;
}
.tooltip.left .tooltip-arrow {
top: 50%;
right: 0;
margin-top: -5px;
border-width: 5px 0 5px 5px;
border-left-color: #000;
}
.tooltip.bottom .tooltip-arrow {
top: 0;
left: 50%;
margin-left: -5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}
.tooltip.bottom-left .tooltip-arrow {
top: 0;
right: 5px;
margin-top: -5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}
.tooltip.bottom-right .tooltip-arrow {
top: 0;
left: 5px;
margin-top: -5px;
border-width: 0 5px 5px;
border-bottom-color: #000;
}
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1060;
display: none;
max-width: 276px;
padding: 1px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.42857143;
text-align: left;
white-space: normal;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
}
.popover.top {
margin-top: -10px;
}
.popover.right {
margin-left: 10px;
}
.popover.bottom {
margin-top: 10px;
}
.popover.left {
margin-left: -10px;
}
.popover-title {
padding: 8px 14px;
margin: 0;
font-size: 14px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}
.popover-content {
padding: 9px 14px;
}
.popover > .arrow,
.popover > .arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.popover > .arrow {
border-width: 11px;
}
.popover > .arrow:after {
content: "";
border-width: 10px;
}
.popover.top > .arrow {
bottom: -11px;
left: 50%;
margin-left: -11px;
border-top-color: #999;
border-top-color: rgba(0, 0, 0, .25);
border-bottom-width: 0;
}
.popover.top > .arrow:after {
bottom: 1px;
margin-left: -10px;
content: " ";
border-top-color: #fff;
border-bottom-width: 0;
}
.popover.right > .arrow {
top: 50%;
left: -11px;
margin-top: -11px;
border-right-color: #999;
border-right-color: rgba(0, 0, 0, .25);
border-left-width: 0;
}
.popover.right > .arrow:after {
bottom: -10px;
left: 1px;
content: " ";
border-right-color: #fff;
border-left-width: 0;
}
.popover.bottom > .arrow {
top: -11px;
left: 50%;
margin-left: -11px;
border-top-width: 0;
border-bottom-color: #999;
border-bottom-color: rgba(0, 0, 0, .25);
}
.popover.bottom > .arrow:after {
top: 1px;
margin-left: -10px;
content: " ";
border-top-width: 0;
border-bottom-color: #fff;
}
.popover.left > .arrow {
top: 50%;
right: -11px;
margin-top: -11px;
border-right-width: 0;
border-left-color: #999;
border-left-color: rgba(0, 0, 0, .25);
}
.popover.left > .arrow:after {
right: 1px;
bottom: -10px;
content: " ";
border-right-width: 0;
border-left-color: #fff;
}
.carousel {
position: relative;
}
.carousel-inner {
position: relative;
width: 100%;
overflow: hidden;
}
.carousel-inner > .item {
position: relative;
display: none;
-webkit-transition: .6s ease-in-out left;
-o-transition: .6s ease-in-out left;
transition: .6s ease-in-out left;
}
.carousel-inner > .item > img,
.carousel-inner > .item > a > img {
line-height: 1;
}
@media all and (transform-3d), (-webkit-transform-3d) {
.carousel-inner > .item {
-webkit-transition: -webkit-transform .6s ease-in-out;
-o-transition: -o-transform .6s ease-in-out;
transition: transform .6s ease-in-out;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-perspective: 1000;
perspective: 1000;
}
.carousel-inner > .item.next,
.carousel-inner > .item.active.right {
left: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
.carousel-inner > .item.prev,
.carousel-inner > .item.active.left {
left: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
.carousel-inner > .item.next.left,
.carousel-inner > .item.prev.right,
.carousel-inner > .item.active {
left: 0;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.carousel-inner > .active,
.carousel-inner > .next,
.carousel-inner > .prev {
display: block;
}
.carousel-inner > .active {
left: 0;
}
.carousel-inner > .next,
.carousel-inner > .prev {
position: absolute;
top: 0;
width: 100%;
}
.carousel-inner > .next {
left: 100%;
}
.carousel-inner > .prev {
left: -100%;
}
.carousel-inner > .next.left,
.carousel-inner > .prev.right {
left: 0;
}
.carousel-inner > .active.left {
left: -100%;
}
.carousel-inner > .active.right {
left: 100%;
}
.carousel-control {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 15%;
font-size: 20px;
color: #fff;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
filter: alpha(opacity=50);
opacity: .5;
}
.carousel-control.left {
background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);
background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);
background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001)));
background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
background-repeat: repeat-x;
}
.carousel-control.right {
right: 0;
left: auto;
background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);
background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);
background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5)));
background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
background-repeat: repeat-x;
}
.carousel-control:hover,
.carousel-control:focus {
color: #fff;
text-decoration: none;
filter: alpha(opacity=90);
outline: 0;
opacity: .9;
}
.carousel-control .icon-prev,
.carousel-control .icon-next,
.carousel-control .glyphicon-chevron-left,
.carousel-control .glyphicon-chevron-right {
position: absolute;
top: 50%;
z-index: 5;
display: inline-block;
}
.carousel-control .icon-prev,
.carousel-control .glyphicon-chevron-left {
left: 50%;
margin-left: -10px;
}
.carousel-control .icon-next,
.carousel-control .glyphicon-chevron-right {
right: 50%;
margin-right: -10px;
}
.carousel-control .icon-prev,
.carousel-control .icon-next {
width: 20px;
height: 20px;
margin-top: -10px;
font-family: serif;
line-height: 1;
}
.carousel-control .icon-prev:before {
content: '\2039';
}
.carousel-control .icon-next:before {
content: '\203a';
}
.carousel-indicators {
position: absolute;
bottom: 10px;
left: 50%;
z-index: 15;
width: 60%;
padding-left: 0;
margin-left: -30%;
text-align: center;
list-style: none;
}
.carousel-indicators li {
display: inline-block;
width: 10px;
height: 10px;
margin: 1px;
text-indent: -999px;
cursor: pointer;
background-color: #000 \9;
background-color: rgba(0, 0, 0, 0);
border: 1px solid #fff;
border-radius: 10px;
}
.carousel-indicators .active {
width: 12px;
height: 12px;
margin: 0;
background-color: #fff;
}
.carousel-caption {
position: absolute;
right: 15%;
bottom: 20px;
left: 15%;
z-index: 10;
padding-top: 20px;
padding-bottom: 20px;
color: #fff;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
.carousel-caption .btn {
text-shadow: none;
}
@media screen and (min-width: 768px) {
.carousel-control .glyphicon-chevron-left,
.carousel-control .glyphicon-chevron-right,
.carousel-control .icon-prev,
.carousel-control .icon-next {
width: 30px;
height: 30px;
margin-top: -15px;
font-size: 30px;
}
.carousel-control .glyphicon-chevron-left,
.carousel-control .icon-prev {
margin-left: -15px;
}
.carousel-control .glyphicon-chevron-right,
.carousel-control .icon-next {
margin-right: -15px;
}
.carousel-caption {
right: 20%;
left: 20%;
padding-bottom: 30px;
}
.carousel-indicators {
bottom: 20px;
}
}
.clearfix:before,
.clearfix:after,
.dl-horizontal dd:before,
.dl-horizontal dd:after,
.container:before,
.container:after,
.container-fluid:before,
.container-fluid:after,
.row:before,
.row:after,
.form-horizontal .form-group:before,
.form-horizontal .form-group:after,
.btn-toolbar:before,
.btn-toolbar:after,
.btn-group-vertical > .btn-group:before,
.btn-group-vertical > .btn-group:after,
.nav:before,
.nav:after,
.navbar:before,
.navbar:after,
.navbar-header:before,
.navbar-header:after,
.navbar-collapse:before,
.navbar-collapse:after,
.pager:before,
.pager:after,
.panel-body:before,
.panel-body:after,
.modal-footer:before,
.modal-footer:after {
display: table;
content: " ";
}
.clearfix:after,
.dl-horizontal dd:after,
.container:after,
.container-fluid:after,
.row:after,
.form-horizontal .form-group:after,
.btn-toolbar:after,
.btn-group-vertical > .btn-group:after,
.nav:after,
.navbar:after,
.navbar-header:after,
.navbar-collapse:after,
.pager:after,
.panel-body:after,
.modal-footer:after {
clear: both;
}
.center-block {
display: block;
margin-right: auto;
margin-left: auto;
}
.pull-right {
float: right !important;
}
.pull-left {
float: left !important;
}
.hide {
display: none !important;
}
.show {
display: block !important;
}
.invisible {
visibility: hidden;
}
.text-hide {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.hidden {
display: none !important;
visibility: hidden !important;
}
.affix {
position: fixed;
}
@-ms-viewport {
width: device-width;
}
.visible-xs,
.visible-sm,
.visible-md,
.visible-lg {
display: none !important;
}
.visible-xs-block,
.visible-xs-inline,
.visible-xs-inline-block,
.visible-sm-block,
.visible-sm-inline,
.visible-sm-inline-block,
.visible-md-block,
.visible-md-inline,
.visible-md-inline-block,
.visible-lg-block,
.visible-lg-inline,
.visible-lg-inline-block {
display: none !important;
}
@media (max-width: 767px) {
.visible-xs {
display: block !important;
}
table.visible-xs {
display: table;
}
tr.visible-xs {
display: table-row !important;
}
th.visible-xs,
td.visible-xs {
display: table-cell !important;
}
}
@media (max-width: 767px) {
.visible-xs-block {
display: block !important;
}
}
@media (max-width: 767px) {
.visible-xs-inline {
display: inline !important;
}
}
@media (max-width: 767px) {
.visible-xs-inline-block {
display: inline-block !important;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.visible-sm {
display: block !important;
}
table.visible-sm {
display: table;
}
tr.visible-sm {
display: table-row !important;
}
th.visible-sm,
td.visible-sm {
display: table-cell !important;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.visible-sm-block {
display: block !important;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.visible-sm-inline {
display: inline !important;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.visible-sm-inline-block {
display: inline-block !important;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.visible-md {
display: block !important;
}
table.visible-md {
display: table;
}
tr.visible-md {
display: table-row !important;
}
th.visible-md,
td.visible-md {
display: table-cell !important;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.visible-md-block {
display: block !important;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.visible-md-inline {
display: inline !important;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.visible-md-inline-block {
display: inline-block !important;
}
}
@media (min-width: 1200px) {
.visible-lg {
display: block !important;
}
table.visible-lg {
display: table;
}
tr.visible-lg {
display: table-row !important;
}
th.visible-lg,
td.visible-lg {
display: table-cell !important;
}
}
@media (min-width: 1200px) {
.visible-lg-block {
display: block !important;
}
}
@media (min-width: 1200px) {
.visible-lg-inline {
display: inline !important;
}
}
@media (min-width: 1200px) {
.visible-lg-inline-block {
display: inline-block !important;
}
}
@media (max-width: 767px) {
.hidden-xs {
display: none !important;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.hidden-sm {
display: none !important;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.hidden-md {
display: none !important;
}
}
@media (min-width: 1200px) {
.hidden-lg {
display: none !important;
}
}
.visible-print {
display: none !important;
}
@media print {
.visible-print {
display: block !important;
}
table.visible-print {
display: table;
}
tr.visible-print {
display: table-row !important;
}
th.visible-print,
td.visible-print {
display: table-cell !important;
}
}
.visible-print-block {
display: none !important;
}
@media print {
.visible-print-block {
display: block !important;
}
}
.visible-print-inline {
display: none !important;
}
@media print {
.visible-print-inline {
display: inline !important;
}
}
.visible-print-inline-block {
display: none !important;
}
@media print {
.visible-print-inline-block {
display: inline-block !important;
}
}
@media print {
.hidden-print {
display: none !important;
}
}
/*# sourceMappingURL=bootstrap.css.map */
================================================
FILE: src/main/resources/css/dashboard.css
================================================
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #428bca;
}
/*
* Main content
*/
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
/*
* Placeholder dashboard ideas
*/
.placeholders {
margin-bottom: 30px;
text-align: center;
}
.placeholders h4 {
margin-bottom: 0;
}
.placeholder {
margin-bottom: 20px;
}
.placeholder img {
display: inline-block;
border-radius: 50%;
}
================================================
FILE: src/main/resources/js/bootstrap.js
================================================
/*!
* Bootstrap v3.3.2 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
if (typeof jQuery === 'undefined') {
throw new Error('Bootstrap\'s JavaScript requires jQuery')
}
+function ($) {
'use strict';
var version = $.fn.jquery.split(' ')[0].split('.')
if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) {
throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher')
}
}(jQuery);
/* ========================================================================
* Bootstrap: transition.js v3.3.2
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
function transitionEnd() {
var el = document.createElement('bootstrap')
var transEndEventNames = {
WebkitTransition : 'webkitTransitionEnd',
MozTransition : 'transitionend',
OTransition : 'oTransitionEnd otransitionend',
transition : 'transitionend'
}
for (var name in transEndEventNames) {
if (el.style[name] !== undefined) {
return { end: transEndEventNames[name] }
}
}
return false // explicit for ie8 ( ._.)
}
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
var called = false
var $el = this
$(this).one('bsTransitionEnd', function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
return this
}
$(function () {
$.support.transition = transitionEnd()
if (!$.support.transition) return
$.event.special.bsTransitionEnd = {
bindType: $.support.transition.end,
delegateType: $.support.transition.end,
handle: function (e) {
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
}
}
})
}(jQuery);
/* ========================================================================
* Bootstrap: alert.js v3.3.2
* http://getbootstrap.com/javascript/#alerts
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// ALERT CLASS DEFINITION
// ======================
var dismiss = '[data-dismiss="alert"]'
var Alert = function (el) {
$(el).on('click', dismiss, this.close)
}
Alert.VERSION = '3.3.2'
Alert.TRANSITION_DURATION = 150
Alert.prototype.close = function (e) {
var $this = $(this)
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = $(selector)
if (e) e.preventDefault()
if (!$parent.length) {
$parent = $this.closest('.alert')
}
$parent.trigger(e = $.Event('close.bs.alert'))
if (e.isDefaultPrevented()) return
$parent.removeClass('in')
function removeElement() {
// detach from parent, fire event then clean up data
$parent.detach().trigger('closed.bs.alert').remove()
}
$.support.transition && $parent.hasClass('fade') ?
$parent
.one('bsTransitionEnd', removeElement)
.emulateTransitionEnd(Alert.TRANSITION_DURATION) :
removeElement()
}
// ALERT PLUGIN DEFINITION
// =======================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.alert')
if (!data) $this.data('bs.alert', (data = new Alert(this)))
if (typeof option == 'string') data[option].call($this)
})
}
var old = $.fn.alert
$.fn.alert = Plugin
$.fn.alert.Constructor = Alert
// ALERT NO CONFLICT
// =================
$.fn.alert.noConflict = function () {
$.fn.alert = old
return this
}
// ALERT DATA-API
// ==============
$(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
}(jQuery);
/* ========================================================================
* Bootstrap: button.js v3.3.2
* http://getbootstrap.com/javascript/#buttons
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// BUTTON PUBLIC CLASS DEFINITION
// ==============================
var Button = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Button.DEFAULTS, options)
this.isLoading = false
}
Button.VERSION = '3.3.2'
Button.DEFAULTS = {
loadingText: 'loading...'
}
Button.prototype.setState = function (state) {
var d = 'disabled'
var $el = this.$element
var val = $el.is('input') ? 'val' : 'html'
var data = $el.data()
state = state + 'Text'
if (data.resetText == null) $el.data('resetText', $el[val]())
// push to event loop to allow forms to submit
setTimeout($.proxy(function () {
$el[val](data[state] == null ? this.options[state] : data[state])
if (state == 'loadingText') {
this.isLoading = true
$el.addClass(d).attr(d, d)
} else if (this.isLoading) {
this.isLoading = false
$el.removeClass(d).removeAttr(d)
}
}, this), 0)
}
Button.prototype.toggle = function () {
var changed = true
var $parent = this.$element.closest('[data-toggle="buttons"]')
if ($parent.length) {
var $input = this.$element.find('input')
if ($input.prop('type') == 'radio') {
if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
else $parent.find('.active').removeClass('active')
}
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
} else {
this.$element.attr('aria-pressed', !this.$element.hasClass('active'))
}
if (changed) this.$element.toggleClass('active')
}
// BUTTON PLUGIN DEFINITION
// ========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.button')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.button', (data = new Button(this, options)))
if (option == 'toggle') data.toggle()
else if (option) data.setState(option)
})
}
var old = $.fn.button
$.fn.button = Plugin
$.fn.button.Constructor = Button
// BUTTON NO CONFLICT
// ==================
$.fn.button.noConflict = function () {
$.fn.button = old
return this
}
// BUTTON DATA-API
// ===============
$(document)
.on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
var $btn = $(e.target)
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
Plugin.call($btn, 'toggle')
e.preventDefault()
})
.on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
$(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
})
}(jQuery);
/* ========================================================================
* Bootstrap: carousel.js v3.3.2
* http://getbootstrap.com/javascript/#carousel
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// CAROUSEL CLASS DEFINITION
// =========================
var Carousel = function (element, options) {
this.$element = $(element)
this.$indicators = this.$element.find('.carousel-indicators')
this.options = options
this.paused =
this.sliding =
this.interval =
this.$active =
this.$items = null
this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))
this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element
.on('mouseenter.bs.carousel', $.proxy(this.pause, this))
.on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
}
Carousel.VERSION = '3.3.2'
Carousel.TRANSITION_DURATION = 600
Carousel.DEFAULTS = {
interval: 5000,
pause: 'hover',
wrap: true,
keyboard: true
}
Carousel.prototype.keydown = function (e) {
if (/input|textarea/i.test(e.target.tagName)) return
switch (e.which) {
case 37: this.prev(); break
case 39: this.next(); break
default: return
}
e.preventDefault()
}
Carousel.prototype.cycle = function (e) {
e || (this.paused = false)
this.interval && clearInterval(this.interval)
this.options.interval
&& !this.paused
&& (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
return this
}
Carousel.prototype.getItemIndex = function (item) {
this.$items = item.parent().children('.item')
return this.$items.index(item || this.$active)
}
Carousel.prototype.getItemForDirection = function (direction, active) {
var activeIndex = this.getItemIndex(active)
var willWrap = (direction == 'prev' && activeIndex === 0)
|| (direction == 'next' && activeIndex == (this.$items.length - 1))
if (willWrap && !this.options.wrap) return active
var delta = direction == 'prev' ? -1 : 1
var itemIndex = (activeIndex + delta) % this.$items.length
return this.$items.eq(itemIndex)
}
Carousel.prototype.to = function (pos) {
var that = this
var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active'))
if (pos > (this.$items.length - 1) || pos < 0) return
if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid"
if (activeIndex == pos) return this.pause().cycle()
return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos))
}
Carousel.prototype.pause = function (e) {
e || (this.paused = true)
if (this.$element.find('.next, .prev').length && $.support.transition) {
this.$element.trigger($.support.transition.end)
this.cycle(true)
}
this.interval = clearInterval(this.interval)
return this
}
Carousel.prototype.next = function () {
if (this.sliding) return
return this.slide('next')
}
Carousel.prototype.prev = function () {
if (this.sliding) return
return this.slide('prev')
}
Carousel.prototype.slide = function (type, next) {
var $active = this.$element.find('.item.active')
var $next = next || this.getItemForDirection(type, $active)
var isCycling = this.interval
var direction = type == 'next' ? 'left' : 'right'
var that = this
if ($next.hasClass('active')) return (this.sliding = false)
var relatedTarget = $next[0]
var slideEvent = $.Event('slide.bs.carousel', {
relatedTarget: relatedTarget,
direction: direction
})
this.$element.trigger(slideEvent)
if (slideEvent.isDefaultPrevented()) return
this.sliding = true
isCycling && this.pause()
if (this.$indicators.length) {
this.$indicators.find('.active').removeClass('active')
var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)])
$nextIndicator && $nextIndicator.addClass('active')
}
var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
if ($.support.transition && this.$element.hasClass('slide')) {
$next.addClass(type)
$next[0].offsetWidth // force reflow
$active.addClass(direction)
$next.addClass(direction)
$active
.one('bsTransitionEnd', function () {
$next.removeClass([type, direction].join(' ')).addClass('active')
$active.removeClass(['active', direction].join(' '))
that.sliding = false
setTimeout(function () {
that.$element.trigger(slidEvent)
}, 0)
})
.emulateTransitionEnd(Carousel.TRANSITION_DURATION)
} else {
$active.removeClass('active')
$next.addClass('active')
this.sliding = false
this.$element.trigger(slidEvent)
}
isCycling && this.cycle()
return this
}
// CAROUSEL PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.carousel')
var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
var action = typeof option == 'string' ? option : options.slide
if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
if (typeof option == 'number') data.to(option)
else if (action) data[action]()
else if (options.interval) data.pause().cycle()
})
}
var old = $.fn.carousel
$.fn.carousel = Plugin
$.fn.carousel.Constructor = Carousel
// CAROUSEL NO CONFLICT
// ====================
$.fn.carousel.noConflict = function () {
$.fn.carousel = old
return this
}
// CAROUSEL DATA-API
// =================
var clickHandler = function (e) {
var href
var $this = $(this)
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
if (!$target.hasClass('carousel')) return
var options = $.extend({}, $target.data(), $this.data())
var slideIndex = $this.attr('data-slide-to')
if (slideIndex) options.interval = false
Plugin.call($target, options)
if (slideIndex) {
$target.data('bs.carousel').to(slideIndex)
}
e.preventDefault()
}
$(document)
.on('click.bs.carousel.data-api', '[data-slide]', clickHandler)
.on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler)
$(window).on('load', function () {
$('[data-ride="carousel"]').each(function () {
var $carousel = $(this)
Plugin.call($carousel, $carousel.data())
})
})
}(jQuery);
/* ========================================================================
* Bootstrap: collapse.js v3.3.2
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================
var Collapse = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Collapse.DEFAULTS, options)
this.$trigger = $(this.options.trigger).filter('[href="#' + element.id + '"], [data-target="#' + element.id + '"]')
this.transitioning = null
if (this.options.parent) {
this.$parent = this.getParent()
} else {
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
}
if (this.options.toggle) this.toggle()
}
Collapse.VERSION = '3.3.2'
Collapse.TRANSITION_DURATION = 350
Collapse.DEFAULTS = {
toggle: true,
trigger: '[data-toggle="collapse"]'
}
Collapse.prototype.dimension = function () {
var hasWidth = this.$element.hasClass('width')
return hasWidth ? 'width' : 'height'
}
Collapse.prototype.show = function () {
if (this.transitioning || this.$element.hasClass('in')) return
var activesData
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
if (actives && actives.length) {
activesData = actives.data('bs.collapse')
if (activesData && activesData.transitioning) return
}
var startEvent = $.Event('show.bs.collapse')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
if (actives && actives.length) {
Plugin.call(actives, 'hide')
activesData || actives.data('bs.collapse', null)
}
var dimension = this.dimension()
this.$element
.removeClass('collapse')
.addClass('collapsing')[dimension](0)
.attr('aria-expanded', true)
this.$trigger
.removeClass('collapsed')
.attr('aria-expanded', true)
this.transitioning = 1
var complete = function () {
this.$element
.removeClass('collapsing')
.addClass('collapse in')[dimension]('')
this.transitioning = 0
this.$element
.trigger('shown.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
this.$element
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
}
Collapse.prototype.hide = function () {
if (this.transitioning || !this.$element.hasClass('in')) return
var startEvent = $.Event('hide.bs.collapse')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
var dimension = this.dimension()
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
this.$element
.addClass('collapsing')
.removeClass('collapse in')
.attr('aria-expanded', false)
this.$trigger
.addClass('collapsed')
.attr('aria-expanded', false)
this.transitioning = 1
var complete = function () {
this.transitioning = 0
this.$element
.removeClass('collapsing')
.addClass('collapse')
.trigger('hidden.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
this.$element
[dimension](0)
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
}
Collapse.prototype.toggle = function () {
this[this.$element.hasClass('in') ? 'hide' : 'show']()
}
Collapse.prototype.getParent = function () {
return $(this.options.parent)
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
.each($.proxy(function (i, element) {
var $element = $(element)
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
}, this))
.end()
}
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
var isOpen = $element.hasClass('in')
$element.attr('aria-expanded', isOpen)
$trigger
.toggleClass('collapsed', !isOpen)
.attr('aria-expanded', isOpen)
}
function getTargetFromTrigger($trigger) {
var href
var target = $trigger.attr('data-target')
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
return $(target)
}
// COLLAPSE PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.collapse')
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data && options.toggle && option == 'show') options.toggle = false
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.collapse
$.fn.collapse = Plugin
$.fn.collapse.Constructor = Collapse
// COLLAPSE NO CONFLICT
// ====================
$.fn.collapse.noConflict = function () {
$.fn.collapse = old
return this
}
// COLLAPSE DATA-API
// =================
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
var $this = $(this)
if (!$this.attr('data-target')) e.preventDefault()
var $target = getTargetFromTrigger($this)
var data = $target.data('bs.collapse')
var option = data ? 'toggle' : $.extend({}, $this.data(), { trigger: this })
Plugin.call($target, option)
})
}(jQuery);
/* ========================================================================
* Bootstrap: dropdown.js v3.3.2
* http://getbootstrap.com/javascript/#dropdowns
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// DROPDOWN CLASS DEFINITION
// =========================
var backdrop = '.dropdown-backdrop'
var toggle = '[data-toggle="dropdown"]'
var Dropdown = function (element) {
$(element).on('click.bs.dropdown', this.toggle)
}
Dropdown.VERSION = '3.3.2'
Dropdown.prototype.toggle = function (e) {
var $this = $(this)
if ($this.is('.disabled, :disabled')) return
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
clearMenus()
if (!isActive) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we use a backdrop because click events don't delegate
$('').insertAfter($(this)).on('click', clearMenus)
}
var relatedTarget = { relatedTarget: this }
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
$this
.trigger('focus')
.attr('aria-expanded', 'true')
$parent
.toggleClass('open')
.trigger('shown.bs.dropdown', relatedTarget)
}
return false
}
Dropdown.prototype.keydown = function (e) {
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
var $this = $(this)
e.preventDefault()
e.stopPropagation()
if ($this.is('.disabled, :disabled')) return
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
if (e.which == 27) $parent.find(toggle).trigger('focus')
return $this.trigger('click')
}
var desc = ' li:not(.divider):visible a'
var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc)
if (!$items.length) return
var index = $items.index(e.target)
if (e.which == 38 && index > 0) index-- // up
if (e.which == 40 && index < $items.length - 1) index++ // down
if (!~index) index = 0
$items.eq(index).trigger('focus')
}
function clearMenus(e) {
if (e && e.which === 3) return
$(backdrop).remove()
$(toggle).each(function () {
var $this = $(this)
var $parent = getParent($this)
var relatedTarget = { relatedTarget: this }
if (!$parent.hasClass('open')) return
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
$this.attr('aria-expanded', 'false')
$parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
})
}
function getParent($this) {
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
return $parent && $parent.length ? $parent : $this.parent()
}
// DROPDOWN PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.dropdown')
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
if (typeof option == 'string') data[option].call($this)
})
}
var old = $.fn.dropdown
$.fn.dropdown = Plugin
$.fn.dropdown.Constructor = Dropdown
// DROPDOWN NO CONFLICT
// ====================
$.fn.dropdown.noConflict = function () {
$.fn.dropdown = old
return this
}
// APPLY TO STANDARD DROPDOWN ELEMENTS
// ===================================
$(document)
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown)
}(jQuery);
/* ========================================================================
* Bootstrap: modal.js v3.3.2
* http://getbootstrap.com/javascript/#modals
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// MODAL CLASS DEFINITION
// ======================
var Modal = function (element, options) {
this.options = options
this.$body = $(document.body)
this.$element = $(element)
this.$backdrop =
this.isShown = null
this.scrollbarWidth = 0
if (this.options.remote) {
this.$element
.find('.modal-content')
.load(this.options.remote, $.proxy(function () {
this.$element.trigger('loaded.bs.modal')
}, this))
}
}
Modal.VERSION = '3.3.2'
Modal.TRANSITION_DURATION = 300
Modal.BACKDROP_TRANSITION_DURATION = 150
Modal.DEFAULTS = {
backdrop: true,
keyboard: true,
show: true
}
Modal.prototype.toggle = function (_relatedTarget) {
return this.isShown ? this.hide() : this.show(_relatedTarget)
}
Modal.prototype.show = function (_relatedTarget) {
var that = this
var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
this.$element.trigger(e)
if (this.isShown || e.isDefaultPrevented()) return
this.isShown = true
this.checkScrollbar()
this.setScrollbar()
this.$body.addClass('modal-open')
this.escape()
this.resize()
this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
this.backdrop(function () {
var transition = $.support.transition && that.$element.hasClass('fade')
if (!that.$element.parent().length) {
that.$element.appendTo(that.$body) // don't move modals dom position
}
that.$element
.show()
.scrollTop(0)
if (that.options.backdrop) that.adjustBackdrop()
that.adjustDialog()
if (transition) {
that.$element[0].offsetWidth // force reflow
}
that.$element
.addClass('in')
.attr('aria-hidden', false)
that.enforceFocus()
var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
transition ?
that.$element.find('.modal-dialog') // wait for modal to slide in
.one('bsTransitionEnd', function () {
that.$element.trigger('focus').trigger(e)
})
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
that.$element.trigger('focus').trigger(e)
})
}
Modal.prototype.hide = function (e) {
if (e) e.preventDefault()
e = $.Event('hide.bs.modal')
this.$element.trigger(e)
if (!this.isShown || e.isDefaultPrevented()) return
this.isShown = false
this.escape()
this.resize()
$(document).off('focusin.bs.modal')
this.$element
.removeClass('in')
.attr('aria-hidden', true)
.off('click.dismiss.bs.modal')
$.support.transition && this.$element.hasClass('fade') ?
this.$element
.one('bsTransitionEnd', $.proxy(this.hideModal, this))
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
this.hideModal()
}
Modal.prototype.enforceFocus = function () {
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
this.$element.trigger('focus')
}
}, this))
}
Modal.prototype.escape = function () {
if (this.isShown && this.options.keyboard) {
this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
e.which == 27 && this.hide()
}, this))
} else if (!this.isShown) {
this.$element.off('keydown.dismiss.bs.modal')
}
}
Modal.prototype.resize = function () {
if (this.isShown) {
$(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
} else {
$(window).off('resize.bs.modal')
}
}
Modal.prototype.hideModal = function () {
var that = this
this.$element.hide()
this.backdrop(function () {
that.$body.removeClass('modal-open')
that.resetAdjustments()
that.resetScrollbar()
that.$element.trigger('hidden.bs.modal')
})
}
Modal.prototype.removeBackdrop = function () {
this.$backdrop && this.$backdrop.remove()
this.$backdrop = null
}
Modal.prototype.backdrop = function (callback) {
var that = this
var animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('')
.prependTo(this.$element)
.on('click.dismiss.bs.modal', $.proxy(function (e) {
if (e.target !== e.currentTarget) return
this.options.backdrop == 'static'
? this.$element[0].focus.call(this.$element[0])
: this.hide.call(this)
}, this))
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
this.$backdrop.addClass('in')
if (!callback) return
doAnimate ?
this.$backdrop
.one('bsTransitionEnd', callback)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
var callbackRemove = function () {
that.removeBackdrop()
callback && callback()
}
$.support.transition && this.$element.hasClass('fade') ?
this.$backdrop
.one('bsTransitionEnd', callbackRemove)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callbackRemove()
} else if (callback) {
callback()
}
}
// these following methods are used to handle overflowing modals
Modal.prototype.handleUpdate = function () {
if (this.options.backdrop) this.adjustBackdrop()
this.adjustDialog()
}
Modal.prototype.adjustBackdrop = function () {
this.$backdrop
.css('height', 0)
.css('height', this.$element[0].scrollHeight)
}
Modal.prototype.adjustDialog = function () {
var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
this.$element.css({
paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
})
}
Modal.prototype.resetAdjustments = function () {
this.$element.css({
paddingLeft: '',
paddingRight: ''
})
}
Modal.prototype.checkScrollbar = function () {
this.bodyIsOverflowing = document.body.scrollHeight > document.documentElement.clientHeight
this.scrollbarWidth = this.measureScrollbar()
}
Modal.prototype.setScrollbar = function () {
var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
}
Modal.prototype.resetScrollbar = function () {
this.$body.css('padding-right', '')
}
Modal.prototype.measureScrollbar = function () { // thx walsh
var scrollDiv = document.createElement('div')
scrollDiv.className = 'modal-scrollbar-measure'
this.$body.append(scrollDiv)
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
this.$body[0].removeChild(scrollDiv)
return scrollbarWidth
}
// MODAL PLUGIN DEFINITION
// =======================
function Plugin(option, _relatedTarget) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.modal')
var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
if (typeof option == 'string') data[option](_relatedTarget)
else if (options.show) data.show(_relatedTarget)
})
}
var old = $.fn.modal
$.fn.modal = Plugin
$.fn.modal.Constructor = Modal
// MODAL NO CONFLICT
// =================
$.fn.modal.noConflict = function () {
$.fn.modal = old
return this
}
// MODAL DATA-API
// ==============
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this)
var href = $this.attr('href')
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
if ($this.is('a')) e.preventDefault()
$target.one('show.bs.modal', function (showEvent) {
if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
$target.one('hidden.bs.modal', function () {
$this.is(':visible') && $this.trigger('focus')
})
})
Plugin.call($target, option, this)
})
}(jQuery);
/* ========================================================================
* Bootstrap: tooltip.js v3.3.2
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.$element = null
this.init('tooltip', element, options)
}
Tooltip.VERSION = '3.3.2'
Tooltip.TRANSITION_DURATION = 150
Tooltip.DEFAULTS = {
animation: true,
placement: 'top',
selector: false,
template: '',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false,
viewport: {
selector: 'body',
padding: 0
}
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (self && self.$tip && self.$tip.is(':visible')) {
self.hoverState = 'in'
return
}
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
var that = this
var $tip = this.tip()
var tipId = this.getUID(this.type)
this.setContent()
$tip.attr('id', tipId)
this.$element.attr('aria-describedby', tipId)
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var orgPlacement = placement
var $container = this.options.container ? $(this.options.container) : this.$element.parent()
var containerDim = this.getPosition($container)
placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
var complete = function () {
var prevHoverState = that.hoverState
that.$element.trigger('shown.bs.' + that.type)
that.hoverState = null
if (prevHoverState == 'out') that.leave(that)
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({
using: function (props) {
$tip.css({
top: Math.round(props.top),
left: Math.round(props.left)
})
}
}, offset), 0)
$tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
offset.top = offset.top + height - actualHeight
}
var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
if (delta.left) offset.left += delta.left
else offset.top += delta.top
var isVertical = /top|bottom/.test(placement)
var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
$tip.offset(offset)
this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) {
this.arrow()
.css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isHorizontal ? 'top' : 'left', '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function (callback) {
var that = this
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
callback && callback()
}
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getPosition = function ($element) {
$element = $element || this.$element
var el = $element[0]
var isBody = el.tagName == 'BODY'
var elRect = el.getBoundingClientRect()
if (elRect.width == null) {
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
}
var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
return $.extend({}, elRect, scroll, outerDims, elOffset)
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
var delta = { top: 0, left: 0 }
if (!this.$viewport) return delta
var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
var viewportDimensions = this.getPosition(this.$viewport)
if (/right|left/.test(placement)) {
var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
if (topEdgeOffset < viewportDimensions.top) { // top overflow
delta.top = viewportDimensions.top - topEdgeOffset
} else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
}
} else {
var leftEdgeOffset = pos.left - viewportPadding
var rightEdgeOffset = pos.left + viewportPadding + actualWidth
if (leftEdgeOffset < viewportDimensions.left) { // left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
} else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
}
}
return delta
}
Tooltip.prototype.getTitle = function () {
var title
var $e = this.$element
var o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
return title
}
Tooltip.prototype.getUID = function (prefix) {
do prefix += ~~(Math.random() * 1000000)
while (document.getElementById(prefix))
return prefix
}
Tooltip.prototype.tip = function () {
return (this.$tip = this.$tip || $(this.options.template))
}
Tooltip.prototype.arrow = function () {
return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (e) {
var self = this
if (e) {
self = $(e.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(e.currentTarget, this.getDelegateOptions())
$(e.currentTarget).data('bs.' + this.type, self)
}
}
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
var that = this
clearTimeout(this.timeout)
this.hide(function () {
that.$element.off('.' + that.type).removeData('bs.' + that.type)
})
}
// TOOLTIP PLUGIN DEFINITION
// =========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.tooltip
$.fn.tooltip = Plugin
$.fn.tooltip.Constructor = Tooltip
// TOOLTIP NO CONFLICT
// ===================
$.fn.tooltip.noConflict = function () {
$.fn.tooltip = old
return this
}
}(jQuery);
/* ========================================================================
* Bootstrap: popover.js v3.3.2
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// POPOVER PUBLIC CLASS DEFINITION
// ===============================
var Popover = function (element, options) {
this.init('popover', element, options)
}
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.VERSION = '3.3.2'
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
trigger: 'click',
content: '',
template: ''
})
// NOTE: POPOVER EXTENDS tooltip.js
// ================================
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
Popover.prototype.constructor = Popover
Popover.prototype.getDefaults = function () {
return Popover.DEFAULTS
}
Popover.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
](content)
$tip.removeClass('fade top bottom left right in')
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
}
Popover.prototype.hasContent = function () {
return this.getTitle() || this.getContent()
}
Popover.prototype.getContent = function () {
var $e = this.$element
var o = this.options
return $e.attr('data-content')
|| (typeof o.content == 'function' ?
o.content.call($e[0]) :
o.content)
}
Popover.prototype.arrow = function () {
return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
}
Popover.prototype.tip = function () {
if (!this.$tip) this.$tip = $(this.options.template)
return this.$tip
}
// POPOVER PLUGIN DEFINITION
// =========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.popover
$.fn.popover = Plugin
$.fn.popover.Constructor = Popover
// POPOVER NO CONFLICT
// ===================
$.fn.popover.noConflict = function () {
$.fn.popover = old
return this
}
}(jQuery);
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.2
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// SCROLLSPY CLASS DEFINITION
// ==========================
function ScrollSpy(element, options) {
var process = $.proxy(this.process, this)
this.$body = $('body')
this.$scrollElement = $(element).is('body') ? $(window) : $(element)
this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
this.selector = (this.options.target || '') + ' .nav li > a'
this.offsets = []
this.targets = []
this.activeTarget = null
this.scrollHeight = 0
this.$scrollElement.on('scroll.bs.scrollspy', process)
this.refresh()
this.process()
}
ScrollSpy.VERSION = '3.3.2'
ScrollSpy.DEFAULTS = {
offset: 10
}
ScrollSpy.prototype.getScrollHeight = function () {
return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
}
ScrollSpy.prototype.refresh = function () {
var offsetMethod = 'offset'
var offsetBase = 0
if (!$.isWindow(this.$scrollElement[0])) {
offsetMethod = 'position'
offsetBase = this.$scrollElement.scrollTop()
}
this.offsets = []
this.targets = []
this.scrollHeight = this.getScrollHeight()
var self = this
this.$body
.find(this.selector)
.map(function () {
var $el = $(this)
var href = $el.data('target') || $el.attr('href')
var $href = /^#./.test(href) && $(href)
return ($href
&& $href.length
&& $href.is(':visible')
&& [[$href[offsetMethod]().top + offsetBase, href]]) || null
})
.sort(function (a, b) { return a[0] - b[0] })
.each(function () {
self.offsets.push(this[0])
self.targets.push(this[1])
})
}
ScrollSpy.prototype.process = function () {
var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
var scrollHeight = this.getScrollHeight()
var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height()
var offsets = this.offsets
var targets = this.targets
var activeTarget = this.activeTarget
var i
if (this.scrollHeight != scrollHeight) {
this.refresh()
}
if (scrollTop >= maxScroll) {
return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
}
if (activeTarget && scrollTop < offsets[0]) {
this.activeTarget = null
return this.clear()
}
for (i = offsets.length; i--;) {
activeTarget != targets[i]
&& scrollTop >= offsets[i]
&& (!offsets[i + 1] || scrollTop <= offsets[i + 1])
&& this.activate(targets[i])
}
}
ScrollSpy.prototype.activate = function (target) {
this.activeTarget = target
this.clear()
var selector = this.selector +
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
var active = $(selector)
.parents('li')
.addClass('active')
if (active.parent('.dropdown-menu').length) {
active = active
.closest('li.dropdown')
.addClass('active')
}
active.trigger('activate.bs.scrollspy')
}
ScrollSpy.prototype.clear = function () {
$(this.selector)
.parentsUntil(this.options.target, '.active')
.removeClass('active')
}
// SCROLLSPY PLUGIN DEFINITION
// ===========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.scrollspy')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.scrollspy
$.fn.scrollspy = Plugin
$.fn.scrollspy.Constructor = ScrollSpy
// SCROLLSPY NO CONFLICT
// =====================
$.fn.scrollspy.noConflict = function () {
$.fn.scrollspy = old
return this
}
// SCROLLSPY DATA-API
// ==================
$(window).on('load.bs.scrollspy.data-api', function () {
$('[data-spy="scroll"]').each(function () {
var $spy = $(this)
Plugin.call($spy, $spy.data())
})
})
}(jQuery);
/* ========================================================================
* Bootstrap: tab.js v3.3.2
* http://getbootstrap.com/javascript/#tabs
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TAB CLASS DEFINITION
// ====================
var Tab = function (element) {
this.element = $(element)
}
Tab.VERSION = '3.3.2'
Tab.TRANSITION_DURATION = 150
Tab.prototype.show = function () {
var $this = this.element
var $ul = $this.closest('ul:not(.dropdown-menu)')
var selector = $this.data('target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
if ($this.parent('li').hasClass('active')) return
var $previous = $ul.find('.active:last a')
var hideEvent = $.Event('hide.bs.tab', {
relatedTarget: $this[0]
})
var showEvent = $.Event('show.bs.tab', {
relatedTarget: $previous[0]
})
$previous.trigger(hideEvent)
$this.trigger(showEvent)
if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
var $target = $(selector)
this.activate($this.closest('li'), $ul)
this.activate($target, $target.parent(), function () {
$previous.trigger({
type: 'hidden.bs.tab',
relatedTarget: $this[0]
})
$this.trigger({
type: 'shown.bs.tab',
relatedTarget: $previous[0]
})
})
}
Tab.prototype.activate = function (element, container, callback) {
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
&& (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length)
function next() {
$active
.removeClass('active')
.find('> .dropdown-menu > .active')
.removeClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', false)
element
.addClass('active')
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
if (transition) {
element[0].offsetWidth // reflow for transition
element.addClass('in')
} else {
element.removeClass('fade')
}
if (element.parent('.dropdown-menu')) {
element
.closest('li.dropdown')
.addClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
}
callback && callback()
}
$active.length && transition ?
$active
.one('bsTransitionEnd', next)
.emulateTransitionEnd(Tab.TRANSITION_DURATION) :
next()
$active.removeClass('in')
}
// TAB PLUGIN DEFINITION
// =====================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tab')
if (!data) $this.data('bs.tab', (data = new Tab(this)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.tab
$.fn.tab = Plugin
$.fn.tab.Constructor = Tab
// TAB NO CONFLICT
// ===============
$.fn.tab.noConflict = function () {
$.fn.tab = old
return this
}
// TAB DATA-API
// ============
var clickHandler = function (e) {
e.preventDefault()
Plugin.call($(this), 'show')
}
$(document)
.on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
.on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
}(jQuery);
/* ========================================================================
* Bootstrap: affix.js v3.3.2
* http://getbootstrap.com/javascript/#affix
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// AFFIX CLASS DEFINITION
// ======================
var Affix = function (element, options) {
this.options = $.extend({}, Affix.DEFAULTS, options)
this.$target = $(this.options.target)
.on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
.on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
this.$element = $(element)
this.affixed =
this.unpin =
this.pinnedOffset = null
this.checkPosition()
}
Affix.VERSION = '3.3.2'
Affix.RESET = 'affix affix-top affix-bottom'
Affix.DEFAULTS = {
offset: 0,
target: window
}
Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
var targetHeight = this.$target.height()
if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
if (this.affixed == 'bottom') {
if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
}
var initializing = this.affixed == null
var colliderTop = initializing ? scrollTop : position.top
var colliderHeight = initializing ? targetHeight : height
if (offsetTop != null && scrollTop <= offsetTop) return 'top'
if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
return false
}
Affix.prototype.getPinnedOffset = function () {
if (this.pinnedOffset) return this.pinnedOffset
this.$element.removeClass(Affix.RESET).addClass('affix')
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
return (this.pinnedOffset = position.top - scrollTop)
}
Affix.prototype.checkPositionWithEventLoop = function () {
setTimeout($.proxy(this.checkPosition, this), 1)
}
Affix.prototype.checkPosition = function () {
if (!this.$element.is(':visible')) return
var height = this.$element.height()
var offset = this.options.offset
var offsetTop = offset.top
var offsetBottom = offset.bottom
var scrollHeight = $('body').height()
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
if (this.affixed != affix) {
if (this.unpin != null) this.$element.css('top', '')
var affixType = 'affix' + (affix ? '-' + affix : '')
var e = $.Event(affixType + '.bs.affix')
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
this.affixed = affix
this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
this.$element
.removeClass(Affix.RESET)
.addClass(affixType)
.trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
}
if (affix == 'bottom') {
this.$element.offset({
top: scrollHeight - height - offsetBottom
})
}
}
// AFFIX PLUGIN DEFINITION
// =======================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.affix')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.affix
$.fn.affix = Plugin
$.fn.affix.Constructor = Affix
// AFFIX NO CONFLICT
// =================
$.fn.affix.noConflict = function () {
$.fn.affix = old
return this
}
// AFFIX DATA-API
// ==============
$(window).on('load', function () {
$('[data-spy="affix"]').each(function () {
var $spy = $(this)
var data = $spy.data()
data.offset = data.offset || {}
if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
if (data.offsetTop != null) data.offset.top = data.offsetTop
Plugin.call($spy, data)
})
})
}(jQuery);
================================================
FILE: src/main/resources/reference.conf
================================================
# Configuration file for SLB Reference Implementation
#
# This configuration document is divided into three components. Note that (in this simplified
# model) each component shares the same base configuration.
# - http-server
# - load-balancer
# - daemon
#
#
neutrino {
# Core engine settings
core {}
# Example initializers
initializers = []
# Configured channel listeners (interfaces)
listeners = [
# By default, we listen for HTTP port 8080
{}
]
# Pool List.
# A a user should configure one or more pools for use by populating pool items
pools = []
# Default Listener Settings
# These will be merged into individual listeners[]
listener = {
# Hostname or IP of interface to listen on.
host = "0.0.0.0"
# Port to listen on (required).
# Must be greater than 1024 if not running as root (recommended)
port = [8080]
# Port alias
# Used as backup in matching portmaps to downstream. This is not listened on.
# If you want this port to be listened on as well as the primary, add it to the primary
# 'port' list instead.
port-alias = [80]
# Protocol
# One of ["http", "zmq"]
protocol = "http"
# Default Pipeline Class for interface
pipeline-class = null
# Swappable pool-selection mechanisim
# One of [none, first, default, cname, or full.class.Name]
pool-resolver = "none"
# Interface-specific timeouts
timeout = ${neutrino.timeout}
# Channel Options
#
channel-options {
# Force downstream channel to keep-alive or use client's keep-alive default
force-keepalive = true
# Event audit support
#
# Enabling this will capture timestamps of events within the pipeline and output
# them when the threshold is passed.
# > audit-threshold = 5s
audit-threshold = null
}
}
# Default channel timeout settings
#
# These will be inherited (and can be overridden) in either VIP or Pool settings. This means the following
# defaults are provided:
# ebay.neutrino.timeout
# ebay.neutrino.pool.timeout
# ebay.neutrino.pools[x].timeout
#
timeout = {
# Timeout for read-idle; if a read has not been been made on this VIP's socket within this duration,
# fail and close channel
# Default (30 seconds) closes channel if no reads or writes within 2 seconds
# Zero (0 seconds) disables timeout
read-idle-timeout = 30s
# Timeout for write-idle; if a read has not been been made on this VIP's socket within this duration,
# fail and close channel
# Default (30 seconds) closes channel if no reads or writes within 2 seconds
# Zero (0 seconds) disables timeout
write-idle-timeout = 30s
# Timeout for individual request; if the request is open for longer than this duration, fail and close
# Default (30 seconds) closes request's channel after 30 seconds, regardless of usage
# Zero (0 seconds) disables timeout
request-timeout = 30s
# Timeout for HTTP session (VIP connection); if the channel is open for longer than this duration,
# fail and close.
# Default (2 minutes) closes channel after 2 mins, regardless of usage
# Zero (0 seconds) disables timeout
session-timeout = 2m
# Timeout for writes; if a write has not been completed within this duration, fail and close channel
# Default (0 seconds) disables timeout
write-timeout = 5s
# Connection timeout; if applied to an outbound connection, will fail the connect() attempt if
# not completed within this time
# Default (5 seconds) fails attempt after 5 seconds
connection-timeout = 5s
}
# Default Pool Settings
# These will be merged into individual pool settings
pool {
# Users should ensure they provide their own valid ID if more than one pool is configured
id = "default"
# Virtual Address (VIP) List
# A user should configure one or more VIPs for use by populating pool items.
addresses = [
# Address Settings
# These
]
# Configured protocol
protocol = "http"
# Servers
servers = []
# Load balancer configurationbal
# One of [rr/round-robin, lc/least-connection, class-name]
balancer = "round-robin"
# Health monitoring
health {
# Default health monitoring is a status-200 monitor on root URL
#monitor-class = "com.ebay.neutrino.health.Status200Monitor"
url = "/"
}
# Pool-specific timeouts
timeout = ${neutrino.timeout}
}
# Reporting/Metrics settings
# These can be enabled by including the MetricsLifecycle component in the initializers
metrics = [
# Support for console logging
{ type = "console", publish-period = 20m },
# Support for graphite publishing
# { type = "graphite", publish-period = 1m, host = "10.65.255.15", port = 2003 }
]
supervisorThreadCount = 1
workerThreadCount = 4
}
================================================
FILE: src/main/resources/simplelogger.properties
================================================
org.slf4j.simpleLogger.defaultLogLevel=warn
# Overall logging tools (simple and full event logging)
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.ops.HttpDiagnosticsHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.ops.HeaderDiagnosticsHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.ops.NeutrinoAuditHandler$=debug
org.slf4j.simpleLogger.log.io.netty.handler.logging.LoggingHandler=info
# Noisy handler diagnostics
org.slf4j.simpleLogger.log.com.ebay.neutrino=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.PooledService=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.pipeline=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.PipelineHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.NeutrinoDownstreamHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.NeutrinoDownstreamListener=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.NeutrinoDownstreamAttempt=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.NeutrinoRequestHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.NeutrinoEmbeddedHandler=info
# Operational handlers
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.ops.ChannelTimeout=debug
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.ops.ChannelTimeoutHandler=debug
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.neutrino.NeutrinoEncodingHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.handler.EndpointHandler=info
#org.slf4j.simpleLogger.log.com.ebay.neutrino.pipeline.HttpMessageUtils$=info
================================================
FILE: src/main/resources/slb.conf
================================================
neutrino {
# Use this to enable/disable the direct SLB api
enable-api = true,
}
neutrino.datasource {
#Refresh time for file
refresh-period = 30s
datasource-reader = "com.ebay.neutrino.datasource.FileReader"
}
resolvers.neutrino {
listeners = [
{
# pool resolvers configured
pool-resolver = ["cname", "layerseven"],
# listening port of SLB
port = 8080,
protocol = "http"
}
]
initializers = [
"com.ebay.neutrino.metrics.MetricsLifecycle"
]
metrics = [
# Support for console logging
{ type = "console", publish-period = 1m }
]
# pools configuration
pools = [
# pool 1, lc = least connection
{ id = "cname1_id", protocol = "http", balancer = "lc", port = "8080",
# servers are the VM/Servers to which traffic will be routed
servers = [
{ id="server10", host = "localhost", port = "9999" }
{ id="server2", host = "localhost", port = "9998" }
]
# All traffic which comes with header Host=cname1.com will be routed to localhost.
# cname1.com should uniquely to idenitfy a pool
addresses = [
{ host="cname1.com" }
{ host="cname5.com" }
{ host="d-sjc-00541471.corp.ebay.com" }
]
# wildcard shares the balancer and protocol with addresses
# All traffic which comes with header host=cnamewildcard.com amd path=local will be routed to localhost.
# host + path should be unique to identify the pool
wildcard = [
{host="cnamewildcard.com", path="/website" }
]
timeout= {
read-idle-timeout = 1s,
write-idle-timeout = 1s,
write-timeout = 1s,
session-timeout = 1s,
request-timeout = 1s,
connection-timeout = 1s
}
},
# pool 2
// { id = "cname2_id", protocol = "http", balancer = "round-robin", port = "8080",
// servers = [
// { id="server3", host = "www.ebay1.com", port = "80", weight = 5 }
// ]
// addresses = [
// {host="cname2.com" }
// ]
// wildcard = [
// {host="cnamewildcard.com", path="/ebay" }
// ]
// }
# pool 2
{ id = "cname2_id", protocol = "http", balancer = "weighted-round-robin", port = "8080",
servers = [
{ id="server3", host = "www.ebay1.com", port = "80", weight = 5 }
]
addresses = [
{host="cname2.com" }
]
wildcard = [
{host="cnamewildcard.com", path="/ebay" }
]
}
]
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoCore.scala
================================================
package com.ebay.neutrino
import com.ebay.neutrino.channel.NeutrinoServices
import com.ebay.neutrino.config._
import com.ebay.neutrino.metrics.{Instrumented, MetricsKey}
import com.ebay.neutrino.util.Utilities
import com.typesafe.config.Config
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.nio.NioEventLoopGroup
import scala.concurrent.Future
/**
* Core Balancer Component.
*
* Good Netty design practices:
* @see http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#28.0
*
* Netty Read throttling:
* @see https://groups.google.com/forum/?fromgroups=#!topic/netty/Zz4enelRwYE
*/
class NeutrinoCore(private[neutrino] val settings: NeutrinoSettings) extends StrictLogging with Instrumented {
//implicit val ec = ExecutionContext.fromExecutor(Executors.newCachedThreadPool()newFixedThreadPool(10))
implicit val context = scala.concurrent.ExecutionContext.Implicits.global
// Connections (this should be externalized? We're sort of bleeding impl here)
val services = new NeutrinoServices(this)
val supervisor = new NioEventLoopGroup(settings.supervisorThreadCount)
val workers = new NioEventLoopGroup(settings.workerThreadCount)
// Add some simple diagnostic metrics
metrics.safegauge(MetricsKey.BossThreads) { supervisor.executorCount }
metrics.safegauge(MetricsKey.IOThreads) { workers.executorCount }
/**
* Configure the balancer with the settings provided.
*
* Note - this method is depricated. Callers should (ideally) configure the underlying
* service directly.
*
* @param settings
*/
@Deprecated
def configure(settings: LoadBalancer) =
// Defer the configuration to the available services; they will split/remove relevant configurations
services map (_.update(settings.pools:_*))
/**
* Start the balancer core's lifecycle.
*
* @return a future indicating success or failure of startup ports. Either all will be successful, or all will fail.
*/
def start(): Future[_] = {
import Utilities._
// Before starting listeners, run the startup lifecycle event
settings.lifecycleListeners foreach (_.start(this))
// Register worker-completion activities on the work-groups, including Shutdown lifecycle events
workers.terminationFuture().future() onComplete {
// TODO make sure this is consistent with the core.shutdown()
case result => settings.lifecycleListeners foreach (_.shutdown(NeutrinoCore.this))
}
// Initialize individual ports
val listeners = services flatMap (_.listen())
// Convert listeners to a group-future
val future = Future.sequence(listeners.toMap.values)
// If any of these guys fail, clean up them all
future onFailure { case ex =>
listeners foreach {
case (address, channel) => channel.onFailure {
case ex => logger.warn(s"Unable to successfully start listener on address: $address", ex)
}
}
shutdown()
}
future
}
/**
* Stop the balancer's execution, if running.
*
* @return a completion future, to notify completion of execution
*/
def shutdown(): Future[_] = {
import Utilities._
// Shutdown services
services map (_.shutdown())
// Wrap Netty futures in scala futures
val groups = Seq(supervisor.shutdownGracefully().future(), workers.shutdownGracefully().future())
// Return when both are complete
Future.sequence(groups)
}
/**
* Resolve registered components.
*/
def component[T <: NeutrinoLifecycle](clazz: Class[T]): Option[T] =
settings.lifecycleListeners.find(_.getClass == clazz).asInstanceOf[Option[T]]
}
object NeutrinoCore {
// Default constructor of balancer
def apply(): NeutrinoCore = NeutrinoCore(Configuration.load())
// Default constructor of balancer
def apply(config: Config): NeutrinoCore = new NeutrinoCore(NeutrinoSettings(config))
// Default constructor of balancer
def apply(settings: NeutrinoSettings, lbconfig: LoadBalancer): NeutrinoCore = {
val core = new NeutrinoCore(settings)
core.configure(lbconfig)
core
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoLifecycle.scala
================================================
package com.ebay.neutrino
import java.lang.reflect.Constructor
import com.typesafe.config.Config
import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}
/**
* Marker interface for BalancerLifecycle.
*
* This will be created reflectively if provided in dot-delimited class format in the
* application.conf/reference.conf configuration file.
*
* It supports one of two reflective constructors:
* .newInstance()
* .newInstance(config: Config)
*/
trait NeutrinoLifecycle {
// Component lifecycle starting
def start(balancer: NeutrinoCore)
// Component lifecycle stopping
def shutdown(balancer: NeutrinoCore)
}
/**
* Helper utility class for resolving initializers from the active balancer core.
*
* TODO rename to something akin to component
* TODO rename class for easier Java API
*/
object NeutrinoLifecycle {
// Try a one-parameter ctor for settings
def create[T](clazz: Class[_ <: T], c: Config): Option[T] =
constructor(clazz.getConstructor(classOf[Config])) map (_.newInstance(c))
// Try a default (no-param) setting
def create[T](clazz: Class[_ <: T]): Option[T] =
constructor(clazz.getConstructor()) map (_.newInstance())
// Resolve the constructor provided, skipping unavailable ones
def constructor[T](ctor: => Constructor[T]): Option[Constructor[T]] =
Try(ctor) match {
case Success(ctor) => Option(ctor)
case Failure(x: NoSuchMethodException) => None
case Failure(x) => throw x
}
def hasConstructor[T](clazz: Class[_ <: T]): Boolean =
constructor(clazz.getConstructor()).isDefined ||
constructor(clazz.getConstructor(classOf[Config])).isDefined
// Java-compatible version
def getInitializer[T <: NeutrinoLifecycle](core: NeutrinoCore, clazz: Class[T]): Option[T] =
core.component[T](clazz)
// Java-compatible version
def getInitializer[T <: NeutrinoLifecycle](request: NeutrinoRequest, clazz: Class[T]): Option[T] =
getInitializer(request.session.service.core, clazz)
// Convenience method; resolve an initializer/component by class
def getInitializer[T <: NeutrinoLifecycle](connection: NeutrinoRequest)(implicit ct: ClassTag[T]): Option[T] =
connection.session.service.core.component[T](ct.runtimeClass.asInstanceOf[Class[T]])
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoNode.scala
================================================
package com.ebay.neutrino
import com.ebay.neutrino.config.{HealthState, TimeoutSettings, VirtualServer}
import com.ebay.neutrino.handler.ops.{ChannelStatisticsHandler, ChannelTimeoutHandler, NeutrinoAuditHandler}
import com.ebay.neutrino.handler.{NeutrinoClientDecoder, NeutrinoClientHandler}
import com.ebay.neutrino.metrics.{Instrumented, Metrics, MetricsKey}
import com.ebay.neutrino.util.{DifferentialStateSupport, Utilities}
import com.typesafe.scalalogging.slf4j._
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.Unpooled
import io.netty.channel._
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.http.HttpRequestEncoder
import scala.collection.concurrent.TrieMap
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success}
/**
* A stateful runtime wrapper around a VirtualServer, encapsulating all the state
* necessary to manage a downstream node (server).
*
* Note that we have an asymmetric relationship with the LoadBalancer component;
* - The Pool delegates allocate() calls to the Balancer
* - The Balancer is responsible for selecting and calling connect() on the node to
* establish the connection
* - The Channel's close-listener calls release() on the node on completion
* - The Node notifies the balancer of the release.
*
* Otherwise, the channel has to maintain an unnecessary link back to the balancer
* that assigned it, and the load-balancer in turn has to maintain channel->node
* syncronized state.
*
* TODO convert to cached InetAddress to speed up resolution
* TODO implement available this stack using a mature concurrent manager
*/
case class NeutrinoNode(pool: NeutrinoPool, settings: VirtualServer)
extends ChannelFutureListener
with StrictLogging
with Instrumented
{
import Utilities._
import com.ebay.neutrino.handler.NeutrinoClientHandler._
import com.ebay.neutrino.util.AttributeSupport._
// Member datum
val initializer = new NeutrinoNodeInitializer(this)
val available = new java.util.concurrent.ConcurrentLinkedDeque[Channel]()
val allocated = new TrieMap[ChannelId, Channel]()
/**
* Update the node settings.
*/
def update(setting: VirtualServer) = {
// Update current health
if (settings.healthState != HealthState.Unknown) settings.healthState = settings.healthState
}
/**
* Make the connection attempt.
*
* The challenge here is that we want to inject a fully-formed EndpointHandler
* into the initial pipeline, which requires an EndpointConnection object.
* Unfortunately, this object is created by the caller only after the
* connect has completed.
*/
import pool.service.context
def connect(): Future[Channel] =
Option(available.poll()) match
{
case None =>
logger.info("Initializing fresh endpoint")
Metrics.PooledCreated.mark()
initializer.connect()
case Some(endpoint) if (endpoint.isAvailable()) =>
logger.info("Assigning existing endpoint")
Metrics.PooledAssigned.mark()
Future.successful(endpoint)
case Some(endpoint) if (endpoint.isActive()) =>
logger.info("Existing endpoint unavailable (inconsistent state); initializing fresh endpoint")
Metrics.PooledRecreated.mark()
endpoint.close()
connect()
case Some(endpoint) =>
logger.info("Existing endpoint inactive; initializing fresh endpoint")
Metrics.PooledRecreated.mark()
endpoint.close()
connect()
}
/**
* Make the connection attempt, using pooled-connection support.
*
* The challenge here is that we want to inject a fully-formed EndpointHandler
* into the initial pipeline, which requires an EndpointConnection object.
* Unfortunately, this object is created by the caller only after the
* connect has completed.
*
* TODO convert to cached InetAddress to speed up resolution
*/
def resolve(): Future[Channel] = {
// Attempt to resolve, rejecting inactive/closed connections
// Do we need to explicitly clean them up?
val channel = connect()
// Cache the channel's allocation
channel andThen {
case Success(channel) =>
require(!allocated.contains(channel.id), s"Attempt to assign an already-allocated endpoint $channel")
allocated(channel.id) = channel
channel.statistics.allocations.incrementAndGet()
case Failure(ex) =>
Metrics.PooledFailed += 1
logger.info("Channel connection failed with {}", ex.getMessage)
}
}
/**
* Release the allocated endpoint back to the 'allocated' list.
*
* TODO support closing connection if too many cached
* TODO Retire overused connections
*/
def release(channel: Channel) =
// Try and resolve existing
allocated remove(channel.id) match
{
case Some(ch) if (channel.isReusable()) =>
// We want to make sure this is a valid; push through an empty write to force any pending closes
channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(this)
case Some(ch) if (channel.isActive()) =>
// Downstream channel has requested a close (ie: keep-alive false). Close and release now.
channel.close()
Metrics.PooledClosed += 1
logger.info("Releasing and closing endpoint {}", channel)
case Some(ch) =>
Metrics.PooledClosed += 1
logger.info("Releasing closed endpoint {}", channel)
case None =>
metrics.counter(MetricsKey.PoolReleased, "mismatch") += 1
logger.warn("Attempted to return a channel which was not allocated: {}", channel.toStringExt)
}
/**
* Handle retry/release priming operations.
* @param future
*/
override def operationComplete(future: ChannelFuture): Unit =
if (future.isSuccess) {
Metrics.PooledRetained += 1
available.push(future.channel)
logger.info("Releasing endpoint {}", future.channel)
}
else {
Metrics.PooledClosed += 1
logger.info("Flush failed; closing endpoint {}", future.channel)
}
// TODO - implement correct/custom shutdown behavior
def shutdown() = {}
}
/**
* Downstream connection initializer.
*
* @param node
*/
class NeutrinoNodeInitializer(node: NeutrinoNode)
extends ChannelInitializer[SocketChannel]
with ChannelFutureListener
with Instrumented
{
import com.ebay.neutrino.metrics.Metrics._
import com.ebay.neutrino.util.AttributeSupport._
import com.ebay.neutrino.util.Utilities._
// Customized timeouts for server-clients; use only the channel-specific values and ensure a
// valid connection-timeout
val timeouts = {
val defaults = node.pool.settings.timeouts
val connect = defaults.connectionTimeout
defaults.copy(
requestCompletion = Duration.Undefined,
sessionCompletion = Duration.Undefined,
connectionTimeout = if (connect.isFinite) connect else TimeoutSettings.Default.connectionTimeout
)
}
val bootstrap = new Bootstrap()
.channel(classOf[NioSocketChannel])
.group(node.pool.service.core.workers)
.option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true)
.option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, timeouts.connectionTimeout.toMillis.toInt)
.handler(this)
// Static handlers
val timeout = new ChannelTimeoutHandler(timeouts)
val stats = new ChannelStatisticsHandler(false)
val audit = new NeutrinoAuditHandler()
// Framing decoders; extract initial HTTP connect event from HTTP stream
protected override def initChannel(channel: SocketChannel): Unit = {
val client = new NeutrinoClientHandler()
// Configure our codecs
channel.pipeline.addLast(timeout)
channel.pipeline.addLast(stats)
channel.pipeline.addLast(new HttpRequestEncoder())
channel.pipeline.addLast(new NeutrinoClientDecoder(client.queue))
channel.pipeline.addLast(audit)
channel.pipeline.addLast(client)
// Update our downstream metrics
Metrics.DownstreamTotal.mark
Metrics.DownstreamOpen += 1
// Prime the statistics object and hook channel close for proper cleanup
channel.statistics
channel.closeFuture().addListener(this)
}
/**
* Connect to the downstream.
* @return
*/
def connect() = bootstrap.connect(node.settings.socketAddress).future
/**
* Customized channel-close listener for Neutrino IO channels.
* Capture and output IO data.
*/
override def operationComplete(future: ChannelFuture) = {
DownstreamOpen -= 1
}
}
/**
* Implementation of a state wrapper around a NeutrinoNode
* (and its contained VirtualServer settings).
*
* We implement state in a TrieMap to provide concurrent access support.
* An alternative would be to restrict access to this class and mediate concurrent
* access externally.
*/
class NeutrinoNodes(pool: NeutrinoPool)
extends DifferentialStateSupport[String, VirtualServer] with StrictLogging
{
// Cache our nodes here (it should make it easier to cleanup on lifecycle here)
val nodes = new TrieMap[String, NeutrinoNode]()
// Extract just the node-values, in read-only mode
def apply() = nodes.readOnlySnapshot.values
// Required methods
override protected def key(v: VirtualServer): String = v.id
override protected def addState(settings: VirtualServer) =
nodes put (key(settings), new NeutrinoNode(pool, settings))
override protected def removeState(settings: VirtualServer) =
nodes remove(key(settings)) map { node => node.shutdown() }
// Update the server's status
override protected def updateState(pre: VirtualServer, post: VirtualServer) =
nodes get (key(pre)) match {
case Some(node) => node.update(post)
case None => logger.warn("Unable to resolve a node for key {}", key(pre).toString)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoPool.scala
================================================
package com.ebay.neutrino
import java.net.NoRouteToHostException
import com.ebay.neutrino.balancer.Balancer
import com.ebay.neutrino.channel.{NeutrinoSession, NeutrinoService}
import com.ebay.neutrino.config.{Transport, VirtualPool}
import com.ebay.neutrino.util.DifferentialStateSupport
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.Channel
import scala.collection.concurrent.TrieMap
import scala.concurrent.Future
/**
* An extremely simple shim between a pool 'implementation' and the underlying
* configuration as imported from our settings.
*
* Good model for this...
* @see https://github.com/twitter/commons/tree/master/src/java/com/twitter/common/net/loadbalancing
*
*
* State management:
* - This object requires a set of 'default' settings, both mutable and immutable
*
* - A caller can update the mutable settings using the update() method, which will
* replace the current settings.
*
* - The following properties are immutable and attempts to update will cause an
* exception:
* - id
* - protocol
* - balancer settings (in particular, balancer-type currently can't be changed)
*
* - The following properties are mutable and may be changed with update()
* - servers
* - addresses
* - others
*/
class NeutrinoPool(val service: NeutrinoService, defaultSettings: VirtualPool) extends StrictLogging
{
import com.ebay.neutrino.util.AttributeSupport._
import service.context
// Active settings; prevent external change
private var currentSettings = defaultSettings
// Create our load-balancing algorithm for this pool
val balancer = Balancer(settings.balancer)
// Cache our nodes here (it should make it easier to cleanup on lifecycle here)
val nodes = new NeutrinoNodes(this)
// Complete initialization of settings
update(defaultSettings)
// Settings accessor; prevent callers from changing the active-settings
def settings: VirtualPool = currentSettings
/**
* HTTP EndpointResolver support
* Register a new incoming request, and attempt to aøllocate a new peer endpoint
*
* TODO resolve an endpoint-service for this connect...
* TODO implement load-balancing ...
*/
def resolve(request: NeutrinoRequest): Future[Channel] =
{
def failed() = Future.failed(new NoRouteToHostException(s"Unable to connect to ${request.uri}"))
val assigned = balancer.assign(request)
val allocated = assigned map (node => node.resolve map ((node, _)))
val channel = allocated getOrElse failed()
// On success, store our components in the request
channel map { case (node, channel) =>
request.node = node
request.balancer = balancer
request.downstream = channel
channel.session = request.session
channel
}
}
def release(request: NeutrinoRequest) = {
require(request.balancer.isEmpty || request.balancer.get == balancer, "Request balancer belongs to different pool")
(request.node, request.downstream) match {
case (Some(node), Some(channel)) =>
logger.info("Response completed for {} - releasing downstream.", request)
// Clear out downstream and request
channel.session = None
request.node = None
request.balancer = None
request.downstream = None
// Release node first (balancer release can trigger additional pull from node)
node.release(channel)
balancer.release(request, node)
case (None, None) =>
// Channel/Node was never allocated (connection was never successful). Nothing to return.
case (node, channel) =>
logger.warn("Attempted to release a request with invalid node and/or downstream channel set: {}, {}", node, channel)
}
}
/**
* Update the pool's settings with the provided.
*/
def update(settings: VirtualPool) =
{
// Validate incoming settings against our default
require(settings.id == defaultSettings.id)
require(settings.protocol == defaultSettings.protocol)
require(settings.balancer == defaultSettings.balancer)
// Update the cached settings
currentSettings = settings
// Diff the current nodes and updated
nodes.update(settings.servers:_*)
// After everything is updated, push the new config to the load-balancer
// TODO can this call be more efficient? We already know the size
balancer.rebuild(nodes().toArray)
}
/**
* Handle pool shutdown; peform required quiescence.
* TODO; make better
*/
def shutdown() = {}
/**
*
*/
override def toString() = super.toString()
}
case class NeutrinoPoolId(id: String, transport: Transport) {
require(!id.contains("::"), "ID should not contain the :: characters")
override val toString = id+"::"+transport
}
object NeutrinoPoolId {
// Create a neutrino pool ID out of a serialized string representation
def apply(id: String): NeutrinoPoolId =
id.split("::") match {
case Array(poolid, trans) => NeutrinoPoolId(poolid, Transport(trans))
case _ => throw new IllegalArgumentException(s"Unable to extract NeutrinoPoolId from $id")
}
// Create a neutrino pool ID out of id/session
def apply(id: String, session: NeutrinoSession): NeutrinoPoolId =
NeutrinoPoolId(id, session.service.settings.protocol)
}
/**
* Implementation of a state wrapper around a NeutrinoPool
* (and its contained VirtualPool settings).
*
* We implement state in a TrieMap to provide concurrent access support.
* An alternative would be to restrict access to this class and mediate concurrent
* access externally.
*/
class NeutrinoPools(val service: NeutrinoService)
extends DifferentialStateSupport[NeutrinoPoolId, VirtualPool]
with Iterable[VirtualPool]
with StrictLogging
{
val pools = new TrieMap[NeutrinoPoolId, NeutrinoPool]()
// Required methods
@inline override protected def key(pool: VirtualPool): NeutrinoPoolId =
NeutrinoPoolId(pool.id, pool.protocol)
override protected def addState(settings: VirtualPool) = {
// Initialize the startup nodes and configure the balancer
val pool = new NeutrinoPool(service, settings)
logger.info("Configuring new pool {}", pool)
pools.put(key(settings), pool)
}
override protected def removeState(settings: VirtualPool) =
pools.remove(key(settings)) map { pool =>
logger.info("Removing existing pool {}", pool)
pool.shutdown()
}
override protected def updateState(pre: VirtualPool, post: VirtualPool) = {
pools.get(key(pre)) match {
case Some(pool) =>
logger.info("Updating existing pool {}", pool)
pool.update(post)
case None =>
// Pool was removed in the time between calculation of the state and this
logger.error("Error upsting existing pool {} - was removed concurrently.", pre.id)
}
}
/**
* Public accessor for NeutrinoPool.
* Callers should use iterator() unless the underlying NeutrinoPools are required.
* This is immutable so changes to the underlying collection will not be visible.
*/
def apply() = pools.readOnlySnapshot().values
/**
* Public accessor for VirtualPool settings.
* This is immutable so changes to the underlying collection will not be visible.
*/
override def iterator: Iterator[VirtualPool] =
pools.readOnlySnapshot().values.iterator map (_.settings)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoPoolResolver.scala
================================================
package com.ebay.neutrino
import com.ebay.neutrino.balancer.{L7AddressResolver, CNameWildcardResolver, CNameResolver}
import com.typesafe.scalalogging.slf4j.StrictLogging
/**
* A plugin interface for Pool resolution.
*
* This provides a way to configure default pool resolution for incoming request processing.
*
* Some common variants are:
* - Default pool for interface
* - First configured pool
* - By request name
*/
trait PoolResolver {
/**
* Attempt to resolve a pool using this request.
*
* @param pools the source pool-set to resolve on
* @param request the source of request
* @return a valid pool, or None if unable to resolve
*/
def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool]
}
object PoolResolver {
/**
* Create a new PoolResolver based on the type/description provided.
*
* @return a valid resolver, or Unavailable if not provided
*/
def apply(name: String): PoolResolver =
name.toLowerCase match {
case "none" | "" => NoResolver
case "default" => DefaultResolver
case "cname" => new CNameResolver
case "layerseven" => new L7AddressResolver
case classname => Class.forName(name).newInstance().asInstanceOf[PoolResolver]
}
}
// Supported resolvers
//
object DefaultResolver extends NamedResolver("default")
/**
* Resolve the provided pool
*/
case class StaticResolver(pool: NeutrinoPool) extends PoolResolver {
// Attempt to resolve a pool using this request.
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] = Option(pool)
}
/**
* No-op pool resolver; this will never resolve a pool.
*/
object NoResolver extends PoolResolver {
// Attempt to resolve a pool using this request.
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] = None
}
/**
* Resolve a pool by its ID (and transport).
*/
class NamedResolver(poolname: String) extends PoolResolver {
// Attempt to resolve a pool using this request.
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] =
pools.pools get (NeutrinoPoolId(poolname, request.session))
}
object NamedResolver extends StrictLogging {
// Retrieve the pool by name/id.
def get(pools: NeutrinoPools, poolid: String): Option[NeutrinoPool] =
pools() find (_.settings.id == poolid)
// Retrieve the pool for this request, by poolid
def get(request: NeutrinoRequest, poolid: String): Option[NeutrinoPool] =
get(request.session.service.pools, poolid)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/NeutrinoRequest.scala
================================================
package com.ebay.neutrino
import java.net.UnknownServiceException
import java.util.concurrent.TimeUnit
import com.ebay.neutrino.channel.NeutrinoSession
import com.ebay.neutrino.config.{CompletionStatus, Host}
import com.ebay.neutrino.metrics.Metrics
import com.ebay.neutrino.util.AttributeClassMap
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.ByteBuf
import io.netty.channel.{Channel, ChannelFuture}
import io.netty.handler.codec.DecoderResult
import io.netty.handler.codec.http._
import io.netty.util.ReferenceCounted
import io.netty.util.concurrent.{Future => NFuture, GenericFutureListener}
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Try
/**
* Connection specifics.
* HttpRequest-specific fields separated from the connection.
*
* This class is responsible for maintaining:
* - the high-level request/response connection (ie: connected endpoints)
* - the low-level state (ie: negotiated state, options)
*
*
* Note that request-handling within this object is not immutable; making changes to
* the enclosed request may/may not apply changes to the underlying request object.
*
* In deference to heap management, we elect to not make a copy on initial use.
*
* TODO also, HTTP Spec supposed to parse connection:: token and remove all matching connection-token headers
*/
class NeutrinoRequest(val session: NeutrinoSession, private[neutrino] val delegate: HttpRequest)
extends AttributeClassMap
with CachedPoolSupport
with HttpRequest
with GenericFutureListener[ChannelFuture]
with StrictLogging
{
import com.ebay.neutrino.util.AttributeSupport._
import com.ebay.neutrino.util.HttpRequestUtils._
// Cached pool; we cache it here so it's not exposed to cross-request
val start = System.currentTimeMillis()
val pool = new PoolCache()
val requestUri = delegate.URI()
val requestKeepalive = HttpHeaderUtil.isKeepAlive(delegate)
// Deferred resolvers
lazy val host: Option[Host] = delegate.host(requestUri)
// Cached response-data
var response: Option[HttpResponse] = None
/** Request completion future support.
*
*/
private val completePromise = session.channel.newPromise().addListener(this)
def addListener(listener: GenericFutureListener[_ <: NFuture[_ >: Void]]) =
completePromise.addListener(listener)
def addListeners(listeners: GenericFutureListener[_ <: NFuture[_ >: Void]]*) =
completePromise.addListeners(listeners:_*)
/**
* Process the request-completion.
* @param future
*/
override def operationComplete(future: ChannelFuture): Unit =
{
// Connection Pool/Node release
pool.release()
val micros = elapsed.toMicros
Metrics.RequestsOpen.dec()
Metrics.RequestsCompleted.update(micros, TimeUnit.MICROSECONDS)
Metrics.RequestsCompletedType(CompletionStatus(response)).update(micros, TimeUnit.MICROSECONDS)
logger.info("Request completed: {}", this)
}
/**
* Register a new incoming request, and attempt to allocate a new peer endpoint.
* If not established, or force is provided, renegotiate the endpoint.
*
* Since we can't assume that a connect attempt will be successful or will fail,
* we actually need to defer the connection-setup to the future success.
* In the short-term, however, we want to preserve the same deterministic method
* return value, so we'll use the future but wait on the response (which is a
* SORT OF BAD THING)
*
* TODO convert the return type to a Future[EndpointConnection]
* TODO convert the whole typing to Scala-specific typing
*/
def connect() = {
// Ensure we're not already-established
require(this.downstream.isEmpty, "Currently, downstream is reestablished for each request. Shouldn't be already set")
// Store pool selection (we'll need to return the node back to the selected pool)
pool() match {
case None =>
Future.failed(new UnknownServiceException(s"Unable to resolve pool for request."))
case Some(pool) =>
// If not established, or force is provided, renegotiate the endpoint
// Grab the connection's current pool, and see if we have a resolver available
// If downstream not available, attempt to connect one here
logger.info("Establishing downstream to {}", pool.settings.id)
pool.resolve(this)
}
}
/**
* Close the outstanding request.
*
* This is intended to be idempotic; it can be called more than once and should
* guarantee that the close-events are only executed once.
*/
def complete() =
completePromise.synchronized {
// Complete the request promise
if (!completePromise.isDone) completePromise.setSuccess
}
// Calculate elapsed duration of the current request
def elapsed = (System.currentTimeMillis-start) millis
/** Delegate HttpRequest methods
*
*/
@Deprecated
def getMethod(): HttpMethod = delegate.method()
def method(): HttpMethod = delegate.method()
def setMethod(method: HttpMethod): HttpRequest = { delegate.setMethod(method); this }
@Deprecated
def getUri(): String = delegate.uri()
def uri(): String = delegate.uri()
def setUri(uri: String): HttpRequest = { delegate.setUri(uri); this }
@Deprecated
def getProtocolVersion(): HttpVersion = delegate.protocolVersion()
def protocolVersion(): HttpVersion = delegate.protocolVersion()
def setProtocolVersion(version: HttpVersion): HttpRequest = { delegate.setProtocolVersion(version); this }
@Deprecated
def getDecoderResult(): DecoderResult = delegate.decoderResult()
def decoderResult(): DecoderResult = delegate.decoderResult()
def setDecoderResult(result: DecoderResult): Unit = delegate.setDecoderResult(result)
def headers(): HttpHeaders = delegate.headers()
}
/**
* Neutrino Pipeline only: endpoint pool for request.
*
* Note that this class is not synchronized; callers should provide their own
* synchronization as required.
*/
sealed trait CachedPoolSupport { self: NeutrinoRequest =>
import com.ebay.neutrino.util.AttributeSupport.RequestAttributeSupport
class PoolCache {
// Cached pool-resolver; this supports the notion of a per-request resolver
private var pool: Option[NeutrinoPool] = None
// Is the pool current cached?
def isEmpty() = pool.isEmpty
/** Delegate the resolve() request to the connection's pools */
def resolve() = session.service.resolvePool(self)
// Return the cached pool, resolving if necessary
def apply(): Option[NeutrinoPool] = {
if (pool.isEmpty) pool = resolve()
pool
}
// Set pool by name
def set(poolname: String): Option[NeutrinoPool] = {
// If replacing exisiting pool, we need to release any outstanding connections to it's node
release()
// Attempt to set a pool with the name provided
pool = Option(poolname) flatMap (NamedResolver.get(self, _))
pool
}
// Release the endpoint.
// Note that we don't actually need to return to the right pool; it'll take care of getting
// it to the right place.
def release(clearPool: Boolean=true) = {
require(self.asInstanceOf[NeutrinoRequest].downstream.isEmpty || pool.isDefined, "If downstream is set, must have a valid pool")
pool map (_.release(self))
if (clearPool) pool = None
}
}
}
/**
* Static request methods.
*
*/
object NeutrinoRequest extends StrictLogging {
import com.ebay.neutrino.util.AttributeSupport._
def apply(channel: Channel, request: HttpRequest): NeutrinoRequest = {
// Grab the session out of the channel's attributes
val session = channel.session.get
request match {
case request: FullHttpRequest => new NeutrinoRequest(session, request) with NeutrinoFullRequest
case request: LastHttpContent => new NeutrinoRequest(session, request) with NeutrinoLastHttpContent
case request: ReferenceCounted => new NeutrinoRequest(session, request) with NeutrinoReferenceCounted
case request => new NeutrinoRequest(session, request)
}
}
/**
* Create the connection-object and attempt to associate it with a valid pool.
* @param channel
* @param httprequest
*/
def create(channel: Channel, httprequest: HttpRequest): Try[NeutrinoRequest] = {
// Attempt to create request (catching any URI format exceptions)
val request = Try(NeutrinoRequest(channel, httprequest))
val statistics = channel.statistics
// Track request-session statistics
request map { request =>
// Update our metrics
Metrics.RequestsOpen.inc()
Metrics.RequestsTotal.mark()
// Update session metrics
if (statistics.requestCount == 0) {
Metrics.SessionActive += 1
Metrics.SessionTotal.mark
logger.info("Starting user-session {}", this)
}
// Resolve by host (if available) or fallback to the incoming VIP's default pool
// Register the request-level connection entity on the new channel with the Balancer
statistics.requestCount.incrementAndGet()
}
request
}
trait NeutrinoReferenceCounted extends ReferenceCounted { self: NeutrinoRequest =>
@inline val typed = delegate.asInstanceOf[ReferenceCounted]
override def refCnt(): Int = typed.refCnt()
override def retain(): ReferenceCounted = typed.retain()
override def retain(increment: Int): ReferenceCounted = typed.retain(increment)
override def touch(): ReferenceCounted = typed.touch()
override def touch(hint: AnyRef): ReferenceCounted = typed.touch(hint)
override def release(): Boolean = typed.release()
override def release(decrement: Int): Boolean = typed.release(decrement)
}
trait NeutrinoLastHttpContent extends NeutrinoReferenceCounted with LastHttpContent { self: NeutrinoRequest =>
@inline override val typed = delegate.asInstanceOf[LastHttpContent]
override def content(): ByteBuf = typed.content()
override def copy(): LastHttpContent = typed.copy()
override def duplicate(): LastHttpContent = typed.duplicate()
override def retain(): LastHttpContent = typed.retain()
override def retain(increment: Int): LastHttpContent = typed.retain(increment)
override def touch(): LastHttpContent = typed.touch()
override def touch(hint: AnyRef): LastHttpContent = typed.touch(hint)
override def trailingHeaders(): HttpHeaders = typed.trailingHeaders()
}
trait NeutrinoFullRequest extends NeutrinoLastHttpContent with FullHttpRequest { self: NeutrinoRequest =>
@inline override val typed = delegate.asInstanceOf[FullHttpRequest]
override def copy(content: ByteBuf): FullHttpRequest = typed.copy(content)
override def copy(): FullHttpRequest = typed.copy()
override def retain(increment: Int): FullHttpRequest = typed.retain(increment)
override def retain(): FullHttpRequest = typed.retain()
override def touch(): FullHttpRequest = typed.touch()
override def touch(hint: AnyRef): FullHttpRequest = typed.touch(hint)
override def duplicate(): FullHttpRequest = typed.duplicate()
override def setProtocolVersion(version: HttpVersion): FullHttpRequest = typed.setProtocolVersion(version)
override def setMethod(method: HttpMethod): FullHttpRequest = typed.setMethod(method)
override def setUri(uri: String): FullHttpRequest = typed.setUri(uri)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/SLB.scala
================================================
package com.ebay.neutrino
/**
* Created by blpaul on 11/13/2015.
*/
import com.ebay.neutrino.api.SLBApi
import com.ebay.neutrino.config.{Configuration, LoadBalancer}
import com.typesafe.scalalogging.slf4j.StrictLogging
import akka.actor.{ActorSystem, Props}
import com.ebay.neutrino.cluster.SLBLoader
import com.ebay.neutrino.cluster.SystemConfiguration
import scala.concurrent.Future
/**
* Create an HTTP SLB application
*
*
*/
class SLB(system: ActorSystem) extends StrictLogging
{
// Start a configuration-loader
val config = SystemConfiguration(system)
val api = system.actorOf(Props(classOf[SLBApi]), "api")
val reloader = system.actorOf(Props(classOf[SLBLoader]), "loader")
// Start running. This will run until the process is interrupted or stop is called
def start(): Future[_] = {
logger.warn("Starting SLB...")
config.core.start()
}
def shutdown(): Future[_] = {
logger.info(s"Stopping SLB Service")
system.shutdown()
config.core.shutdown()
}
}
object SLB extends StrictLogging {
/**
* Initialize a new SLB instance, using the configuration file provided.
*
* @param filename
* @return
*/
def apply(filename: String = "/etc/neutrino/slb.conf"): SLB =
new SLB(SystemConfiguration.system(filename))
def main(args: Array[String]): Unit = {
// If running as a stand-alone application, start
//new SLBLifecycle().start(args)
SLB().start()
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/api/ApiData.scala
================================================
package com.ebay.neutrino.api
import java.util.Date
import com.ebay.neutrino.config.CompletionStatus
import com.ebay.neutrino.metrics.Metrics
import spray.json.DefaultJsonProtocol
trait ApiData {
import com.ebay.neutrino.api.ApiData._
// Faked start time (should be moved to core)
val apiStartTime = new Date()
def generateStatus(): ApiData.Status = {
import Metrics._
import nl.grons.metrics.scala.{Timer => TimerData}
def toTimer(data: TimerData) = {
val snapshot = data.snapshot
Timer(
data.count,
snapshot.getMin,
snapshot.getMax,
snapshot.getMean,
snapshot.get95thPercentile,
data.oneMinuteRate,
data.fiveMinuteRate,
data.fifteenMinuteRate
)
}
Status(
HostInfo(
"localhost",
"127.0.0.1",
apiStartTime.toString
),
Traffic(
UpstreamBytesRead.count, UpstreamBytesWrite.count, UpstreamPacketsRead.count, UpstreamPacketsWrite.count, UpstreamOpen.count, UpstreamTotal.count
),
Traffic(
DownstreamBytesRead.count, DownstreamBytesWrite.count, DownstreamPacketsRead.count, DownstreamPacketsWrite.count, DownstreamOpen.count, DownstreamTotal.count
),
Requests( // Sessions
SessionActive.count,
toTimer(Metrics.SessionDuration)
),
Requests( // Requests
RequestsOpen.count,
toTimer(Metrics.RequestsCompleted)
),
Seq(
Responses("2xx", toTimer(Metrics.RequestsCompletedType(CompletionStatus.Status2xx))),
Responses("4xx", toTimer(Metrics.RequestsCompletedType(CompletionStatus.Status4xx))),
Responses("5xx", toTimer(Metrics.RequestsCompletedType(CompletionStatus.Status5xx))),
Responses("incomplete", toTimer(Metrics.RequestsCompletedType(CompletionStatus.Incomplete))),
Responses("other", toTimer(Metrics.RequestsCompletedType(CompletionStatus.Other)))
)
)
}
}
object ApiData {
// Supported metric types
sealed trait MetricInt
case class Metric(key: String, `type`: String, values: MetricInt)
case class Counter(count: Long) extends MetricInt
case class Timer(count: Long, min: Long, max: Long, mean: Double, `95th`: Double, `1min`: Double, `5min`: Double, `15min`: Double) extends MetricInt
case class Meter(count: Long) extends MetricInt
case class HostInfo(hostname: String, address: String, lastRestart: String)
case class Traffic(bytesIn: Long, bytesOut: Long, packetsIn: Long, packetsOut: Long, currentConnections: Long, totalConnections: Long)
//case class RequestStats(active: Long, total: Long, minElapsed: Long, avgElapsed: Long, lastRate: Long)
case class Requests(active: Long, stats: Timer)
case class Responses(responseType: String, stats: Timer)
case class Status(host: HostInfo, upstreamTraffic: Traffic, downstreamTraffic: Traffic, sessions: Requests, requests: Requests, responses: Seq[Responses])
object JsonImplicits extends DefaultJsonProtocol {
implicit val timerJson = jsonFormat8(Timer)
implicit val hostinfoJson = jsonFormat3(HostInfo)
implicit val trafficJson = jsonFormat6(Traffic)
implicit val requestsJson = jsonFormat2(Requests)
implicit val responsesJson = jsonFormat2(Responses)
implicit val statusJson = jsonFormat6(Status)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/api/SLBApi.scala
================================================
package com.ebay.neutrino.api
import akka.actor.{Actor, ActorSystem, Props}
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.www.WebService
import com.ebay.neutrino.www.ui.ResourceServices
import com.typesafe.scalalogging.slf4j.StrictLogging
import spray.routing.SimpleRoutingApp
import com.ebay.neutrino.cluster.SystemConfiguration
import scala.util.{Failure, Success}
import scala.concurrent.duration._
import akka.util.Timeout
class SLBApi
extends Actor
with SimpleRoutingApp with ResourceServices with WebService
with StrictLogging
with Instrumented
{
implicit override val system = context.system
// Set the timeout for starting the api server
implicit val timeout: Timeout = 30 seconds
import context.dispatcher
val config = SystemConfiguration(system)
// Our web-application routes
def routes = resourceRoutes ~ webRoutes
if (config.settings.enableApi) {
// TODO pull these from configuration
val host = "0.0.0.0"
val port = 8079
logger.info("Starting API on port ")
//val simpleCache = routeCache(maxCapacity = 1000, timeToIdle = Duration("30 min"))
startServer(interface = host, port = port)(routes) onComplete {
case Success(b) =>
println(s"Successfully bound to ${b.localAddress}")
case Failure(ex) =>
println(ex.getMessage)
}
}
def receive: Receive = {
case msg =>
logger.warn("Unexpected message received: {}", msg.toString)
}
}
/**
* Standalone app wrapper around the SLB API.
*/
object SLBApi extends App {
ActorSystem("slb-api").actorOf(Props(classOf[SLBApi]))
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/Balancer.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.{NeutrinoRequest, NeutrinoNode}
import com.ebay.neutrino.config.BalancerSettings
trait Balancer {
/*
// Expose our scheduler's endpoint-statistics
def statistics: Traversable[(ChannelId, EndpointStatistics)]
// Update the status of an endpoint on completion of a response
def update(capacity: Capacity)
// Add a new endpoint to our set
def register(endpoint: ChannelId)
// Remove an endpoint from our set
def remove(endpoint: ChannelId)
*/
// Re(set) the current membership of the load-balancer
def rebuild(members: Array[NeutrinoNode])
// Assign an endpoint for request processing
def assign(request: NeutrinoRequest): Option[NeutrinoNode]
// Release an endpoint from request processing
def release(request: NeutrinoRequest, node: NeutrinoNode)
}
/**
*
* TODO - add agent support for synchronous read/asynchronous update
*
* Ensure class are available in the system
* import scala.reflect.runtime.{universe => ru}
* lazy val mirror = ru.runtimeMirror(classOf[Scheduler].getClassLoader)
* val schedulerType = mirror.classSymbol(schedulerClass).toType
* val scheduler = mirror.create(schedulerType)
*/
object Balancer {
import com.ebay.neutrino.NeutrinoLifecycle._
/**
* Select a load-selection mechanism.
* @param settings
*/
def apply(settings: BalancerSettings): Balancer =
// Resolve a constructor
settings.config match {
case Some(config) => (create[Balancer](settings.clazz, config) orElse create[Balancer](settings.clazz)).get
case None => (create[Balancer](settings.clazz)).get
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/BalancerNodes.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.NeutrinoNode
import com.ebay.neutrino.config.HealthState
class BalancerNodes[T] {
import com.ebay.neutrino.balancer.BalancerNodes._
// Only requires synchronization on structural changes.
private var servers = Map[NeutrinoNode, T]()
// Set the values
def set(members: Array[NeutrinoNode], creator: NeutrinoNode => T) =
servers = members map (node => node -> creator(node)) toMap
// Get the node
def get(node: NeutrinoNode): Option[T] = servers.get(node)
// Determine if we have any downstream servers available for balancing
def isEmpty() = servers.isEmpty
// Return a view of available servers
def available: Iterable[(NeutrinoNode, T)] = servers.view filter {
case (n, e) => isAvailable(n.settings.healthState)
}
def available(f: T => Boolean): Iterable[(NeutrinoNode, T)] =
available filter { case (n, e) => f(e) }
def minBy[B](f: T => B)(implicit cmp: Ordering[B]): Option[(NeutrinoNode, T)] = available match {
case iter if iter.isEmpty => None
case iter => Option(iter minBy { case (n, e) => f(e) })
}
def find(f: T => Boolean): Option[(NeutrinoNode, T)] =
available find { case (_, e) => f(e) }
}
object BalancerNodes {
// Helper method; determine if this health-state is available
@inline def isAvailable(state: HealthState): Boolean = state match {
case HealthState.Maintenance => false
case _ => true
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/CNameResolver.scala
================================================
package com.ebay.neutrino.balancer
import javax.annotation.Nullable
import com.ebay.neutrino._
import com.ebay.neutrino.config.{VirtualAddress, VirtualPool, CanonicalAddress}
import com.ebay.neutrino.metrics.Instrumented
/**
* A customized PoolResolver supporting an efficient CNAME-based pool selection.
*
* This allows a pool to be selected using the request's host value, resolving
* a matching pool from the pool's configured CNAME and port mapping.
*
* Previously, we assumed the NeutrinoPools provided covered the whole set of
* pools and we were required to filter accordingly. Since V0.5.6 this behaviour
* has changed so we no longer have to filter out non-matches. Now, we can
* assume the NeutrinoPools provided is the full set of pools 'available' to
* the resolver.
*
* In particular, the following changes have been made:
* - Previously: matched by hostname, port, protocol
* - Currently: match by hostname (NeutrinoPools has been filtered by port and protocol)
**
* TODO make data-structure more efficient for lookups
* TODO fix parse order/data-structure
* TODO add config as ctor-parameter
*/
class CNameResolver extends CachingResolver[String] with Instrumented {
import scala.collection.JavaConversions._
// Our UUID for matching
object HostKey {
def apply(address: CanonicalAddress): String = address.host.toLowerCase
def apply(request: NeutrinoRequest): Option[String] = request.host map (h => h.host.toLowerCase)
}
// Our loader version; is incremented on every update
private[balancer] var ports = Seq.empty[Int]
/**
* Hook our parent's rebuild to cache our associated ports.
* @param pools
* @return a new pool-set, as generated by our parent
*/
protected override def rebuild(pools: NeutrinoPools): Map[String, NeutrinoPool] = {
ports = pools.service.settings.sourcePorts
super.rebuild(pools)
}
/**
* Helper function to determine if the address is a CNAME and the port specified matches
* one of ours.
*
* @param address
* @return
*/
@inline def isCanonicalPool(address: VirtualAddress): Boolean =
address match {
case cname: CanonicalAddress if ports.contains(cname.port) => true
case _ => false
}
/**
* Rebuild the cached map on change of underlying source pools.
*
* This will generate one K/V per address/port/protocol tuple
*
* @param pools source-set of new replacement pools
*/
def rebuild(pools: java.util.Iterator[VirtualPool]): java.util.Map[String, VirtualPool] =
{
// Helper method; determines if any of the addresses are mapped by our listener's sourcePort
val relevant = pools filter { pool => pool.addresses find (isCanonicalPool) isDefined }
// Split out the relevant (match on protocol/source-port)
// Extract host-key
val hosts = relevant flatMap { pool => pool.addresses collect {
case addr: CanonicalAddress => (HostKey(addr), pool)
}
}
// Build a java-map out of this
val hostmap = new java.util.HashMap[String, VirtualPool]()
hosts foreach { case (addr, pool) => hostmap.put(addr, pool) }
hostmap
}
/**
* This implements a simplified Java-style interface, which allows null-return value.
*
* @return a valid NeutrinoPool, or null if not resolved
*/
@Nullable
def resolve(request: NeutrinoRequest): String = HostKey(request) getOrElse null
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/CNameWildcardResolver.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.config.{CanonicalAddress, Host}
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino._
import com.google.common.cache.{CacheBuilder, CacheLoader}
import io.netty.handler.codec.http.HttpRequest
/**
* A customized PoolResolver supporting an efficient CNAME-based pool selection.
*
* This allows a pool to be selected using the request's host value, resolving
* a matching pool from the pool's configured CNAME and port mapping.
*
*
* This implementation makes a wildcard-based match, matching the first CNAME
* to make a postfix match. THIS MAY NOT BE WHAT YOU ARE LOOKING FOR...
* For dynamic cases, you're probably better off using the full-match CNAME resolver.
*
*
* TODO make data-structure more efficient for lookups
* TODO fix parse order/data-structure
* TODO add config as ctor-parameter
*/
class CNameWildcardResolver extends PoolResolver with Instrumented {
import com.ebay.neutrino.util.HttpRequestUtils._
// Constants (TODO - move to settings)
val MAX_SIZE = 1023
// Always build the cache from the 'current' poolset.
private[balancer] val cache = CacheBuilder.newBuilder().maximumSize(MAX_SIZE).build(new HostLoader())
// Cached pools; if the pools structure changes we need to pick up the changes here (and flush the cache)
private[balancer] var pools: Seq[NeutrinoPool] = Seq.empty
// Our loader version; is incremented on every update
private[balancer] var version = 0
/**
* Determine if this pool matches the request provided; find the first matching host/CNAME
*
* Currently, just checks for postfix match.
* For example:
* host = "www.ebay.com"
* address.host = "ebay.com" (match)
* address.host = "123.www.ebay.com" (not match)
*
* TODO move this find out to caller to cache host
* TODO add port support
* TODO profile this; might benefit from an outer toIterator()
*/
class HostLoader extends CacheLoader[Host, Option[NeutrinoPool]] {
override def load(host: Host) = {
val hostname = host.host
val hostpairs = pools flatMap (pool => pool.settings.addresses collect { case addr:CanonicalAddress => (addr.host, pool)})
val foundpair = hostpairs.find (pair => hostname.endsWith(pair._1))
foundpair map (_._2)
}
}
// Check for a valid poolset, updating the cache as required
// TODO what kind of locking do we want here??
// TODO if this was a result of an async setPools, we should do refresh() instead of invalidateAll
@inline def cachepools(newpools: NeutrinoPools) = {
if (version != newpools.version) {
version = newpools.version
pools = newpools().toSeq /*toList*/ // Not sure why we need to cache this here
cache.invalidateAll()
}
cache
}
/**
* Return the pool corresponding to the request provided, if available.
* Extract and cache the host
*
* TODO _ support PORT -> maybe through balancerpoools?
*/
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] = {
val cache = cachepools(pools)
val host = request.host
host flatMap (cache.get(_))
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/CachingResolver.scala
================================================
package com.ebay.neutrino.balancer
import javax.annotation.Nullable
import com.ebay.neutrino.config.VirtualPool
import com.ebay.neutrino.{NeutrinoRequest, NeutrinoPools, NeutrinoPool, PoolResolver}
/**
* Implements a simple Map-caching layer and simplified pool resolver over top of
* the PoolResolver.
*
* This is useful because of the design of the resolver.
*
* Since NeutrinoCore and each NeutrinoPools container is immutable, it is replaced on
* each configuration update. This container does not provide any direct mechansim to
* optimize lookup, and so if we want to optimize type-specific operations (ie:
* segregation by port, or lookup by host) we have to implement it externally.
*
* The idempotic design provides the pool by parameter, rather than through some type
* of stateful mechanisim (ie: constructor) so we need some way to determine the
* source pools have changed. NeutrinoPools provides this through an incremental
* version ID.
*
* This class provides helper wrappers around the refresh process, insulating the
* resolver implementation from the internals of state management.
*
*
* @tparam K unique key-type for hash-map
*/
abstract class CachingResolver[K] extends PoolResolver {
import scala.collection.JavaConversions._
/*
// Instrument the cache
metrics.safegauge("cache.size") { cnames.size }
metrics.safegauge("cache.hitCount") { cnames.stats.hitCount }
metrics.safegauge("cache.missCount") { cnames.stats.missCount }
metrics.safegauge("cache.totalLoadTime") { cnames.stats.totalLoadTime }
metrics.safegauge("cache.evictionCount") { cnames.stats.evictionCount }
*/
// Cached pools; if the pools structure changes we need to pick up the changes here
// (and flush the cache)
private[balancer] var cached: Map[K, NeutrinoPool] = Map()
// Our loader version; is incremented on every update
private[balancer] var version: Int = 0
/**
* Return the pool corresponding to the request provided, if available.
* Extract and cache the host
*
* TODO build caching around underlying settings rather than host/port
* TODO handle concurrency case to prevent rebuilding of updated version multiple times
*/
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] = {
// Check for version change
if (version != pools.version) {
// Update the version to minimize regeneration
version = pools.version
cached = rebuild(pools)
}
// Delegate to the one-parameter version and handle null-results
Option(resolve(request)) flatMap (cached get(_))
}
protected def rebuild(pools: NeutrinoPools): Map[K, NeutrinoPool] = {
// Generate a reference-set (configs to nodes) and rebuild our set
val configs = pools() map (pool => pool.settings -> pool) toMap
val rebuilt = mapAsScalaMap(rebuild(configs.keys.toIterator))
val newpool = rebuilt mapValues (configs(_)) toMap
newpool
}
/**
* Rebuild the cached map on change of underlying source pools.
*
* Subclasses should implement functionality to both extract a key from the
* pool-configuration, and filter out any non-matching pools from the
* resulting pool-set.
*
* @param pools source-set of new replacement pools
*/
def rebuild(pools: java.util.Iterator[VirtualPool]): java.util.Map[K, VirtualPool]
/**
* This implements a simplified Java-style interface, which allows null-return value.
*
* @return a valid NeutrinoPool, or null if not resolved
*/
@Nullable
def resolve(request: NeutrinoRequest): K
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/L7AddressResolver.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino._
import com.ebay.neutrino.config.WildcardAddress
import com.ebay.neutrino.metrics.Instrumented
/**
* A customized PoolResolver supporting pool-selection based on the CMS
* RoutingPolicy/RouteMap configuration.
*
* TODO make data-structure more efficient for lookups
* TODO fix parse order/data-structure
* TODO add config as ctor-parameter
*/
class L7AddressResolver extends PoolResolver with Instrumented {
// Specify our types for clarity
// TODO this could be a Map[String, NeutrinoPool] if we didn't care about ordering
type RouteMap = List[(String, NeutrinoPool)]
type HostCache = Map[String, RouteMap]
// Cached pools; if the pools structure changes we need to pick up the changes here
// (and flush the cache)
private[balancer] var cached: HostCache = Map()
// Our loader version; is incremented on every update
private[balancer] var version: Int = 0
/**
* Build a multi-map of Host => (path, pool) that we can use to resolve.
*
* @param pools
* @return
*/
protected def rebuild(pools: NeutrinoPools): HostCache =
synchronized {
// Generate a reference-set (configs to nodes) and rebuild our set
val configs = pools() map (pool => pool.settings -> pool) toMap
// Extract relevant address-configurations into tuples
val tuples = configs flatMap { case (poolcfg, pool) =>
poolcfg.addresses collect {
case route: WildcardAddress if (route.host.nonEmpty && route.path.nonEmpty) =>
// Group the tuples back out to our cache format
(route.host.toLowerCase, route.path, pool)
}
} toList
// Group tuples by host-name and aggregate the path/pool pairs
tuples groupBy (_._1) mapValues {
_ map { case (_, path, pool) => path -> pool }
}
}
/**
* Return the pool corresponding to the request provided, if available.
* Extract and cache the host
*
* TODO build caching around underlying settings rather than host/port
* TODO handle concurrency case to prevent rebuilding of updated version multiple times
*/
override def resolve(pools: NeutrinoPools, request: NeutrinoRequest): Option[NeutrinoPool] = {
// Check for version change
if (version != pools.version) {
// Update the version to minimize regeneration
version = pools.version
cached = rebuild(pools)
}
// Identify our 'relevant' portions of the reuqest
val host = request.host map (_.host.toLowerCase)
val path = request.requestUri.getPath
// Resolve all pools that match hosts
val routemap = host flatMap (cached get (_))
// If found, further try and match against possible pool-prefix => COULD BE REGEXES
routemap flatMap (_ collectFirst {
case (prefix, pool) if path.startsWith(prefix) => pool
})
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/LeastConnection.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.{NeutrinoNode, NeutrinoRequest}
import com.typesafe.scalalogging.slf4j.StrictLogging
/**
* An algorithmically naive implementation of a least-connection operation.
*
* This implementation currently is O(n) to resolve 'lowest'.
*
* TODO Efficient Data Structure
* - The ideal would be some form of Red/Black tree (ie: TreeMap[Node, Int])
* - Unfortunately, we can only order based on Key, not Value
* - Recommend custom data structure based on LinkedList:
* - Parent structure a linked-array of "per-count buckets" (List[(count, List[Entry[NeutrinoNode]])]
* - Assign comes from front of parent list (to determine lowest 'count' bucket)
* - Assign from bucket = first in bucket
* - Move to next bucket, inserting if not contiguous
* - On move, add to front of new/next bucket
* - Shift remaining items in old bucket back to root
* - Release from bucket is reverse
* - Secondary HashMap[NeutrinoNode, Entry[NeutrinoNode]] maps back to node and resolve down
* - *** Locking required on every operation; scalability issue
*/
class LeastConnectionBalancer extends Balancer with StrictLogging {
private case class Entry(node: NeutrinoNode, var count: Int = 0)
// Only requires synchronization on structural changes.
private val members = new BalancerNodes[Entry]()
// Re(set) the current membership of the load-balancer
// ?? Should rebuild just do a new map replacement? Need to mark/sweep/copy ints
override def rebuild(members: Array[NeutrinoNode]) = this.members.set(members, Entry(_))
/**
* Resolve an endpoint for request processing.
*
* @param request
* @return
*/
def assign(request: NeutrinoRequest): Option[NeutrinoNode] =
if (members.isEmpty)
None
else {
// INEFFICIENT = THIS NEEDS TO BE FIXED WITH INTERNAL SORTED LIST
// SEE NOTE IN CLASS DESCRIPTION
// NOTE: This requires either entry synchronization, or tolerance of over-assignment
// - Synchronization of whole data structure prevents inconsistent writes
// - Over-assignment would allow double assignment to the smallest resolved on
// concurrent access
members.minBy(_.count) map { case (node, entry) =>
entry.synchronized(entry.count += 1)
node
}
}
// Release an endpoint from request processing
def release(request: NeutrinoRequest, node: NeutrinoNode) =
members.get(node) match {
case Some(entry) => entry.synchronized(entry.count -= 1)
case None => logger.warn("Attempt to release a node which was not found in the servers: {}", node)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/RoundRobin.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.{NeutrinoNode, NeutrinoRequest}
class RoundRobinBalancer extends Balancer {
type Entry = NeutrinoNode
private val members = new BalancerNodes[Entry]()
private var iter = members.available.iterator
// Re(set) the current membership of the load-balancer
override def rebuild(members: Array[NeutrinoNode]) =
this.members.set(members, identity)
// Resolve an endpoint for request processing
def assign(request: NeutrinoRequest): Option[NeutrinoNode] =
if (members.isEmpty)
None
else
// Store the active iterator
members.synchronized {
if (iter.isEmpty) iter = members.available.iterator
if (iter.hasNext) Option(iter.next._1)
else None
}
// Release an endpoint from request processing
def release(request: NeutrinoRequest, node: NeutrinoNode) = {}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/Statistics.scala
================================================
package com.ebay.neutrino.balancer
import java.io._
import scala.concurrent.duration.FiniteDuration
import scala.util.Try
import com.typesafe.scalalogging.slf4j.StrictLogging
/**
* Interesting Statistics to collect
*
* - measure how many capacity measurements are piggybacked on data packets
* (ie: packets had enough room to send capacity + framing in same TCP)
*/
// @see http://man7.org/linux/man-pages/man5/proc.5.html
object CpuStatistics extends StrictLogging {
// Data-type for samples
case class CpuSample(user: Double, nice: Double, system: Double, idle: Double, iowait: Double, irq: Double, softirq: Double, steal: Double, guest: Double, guest_nice: Double) {
lazy val total = (user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice)
// Calculate the usage between this sample and the sample provided
def usage(other: CpuSample) = {
val idleDiff = Math.abs(other.idle - idle)
val totalDiff = Math.abs(other.total - total)
1 - (idleDiff / totalDiff)
}
}
case class UptimeSample(uptime: Double, idle: Double)
// Constants
val PROC_STAT = new File("/proc/stat") //proc_stat.txt
val PROC_UPTIME = new File("/proc/uptime") //proc_uptime.txt
val ProcessorCount = Runtime.getRuntime.availableProcessors
/**
* Opens a stream from a existing file and return it. This style of
* implementation does not throw Exceptions to the caller.
*
* @param path is a file which already exists and can be read.
* @throws IOException
*/
def cpuSample(): Option[CpuSample] = {
val reader = Try(new BufferedReader(new FileReader(PROC_STAT))).toOption
val values = reader map { _.readLine.split("\\s+").tail map (_.toDouble) }
// Clean up our reader
reader map { _.close() }
values map { array =>
val Array(user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice) = array
CpuSample(user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice)
}
}
def uptimeSample(): Option[UptimeSample] = {
val reader = Try(new BufferedReader(new FileReader(PROC_UPTIME))).toOption
val values = reader map { _.readLine.split("\\s+") map (_.toDouble) }
// Clean up our reader
reader map { _.close() }
values map { array =>
val Array(uptime, idletime) = array
UptimeSample(uptime, idletime)
}
}
/**
* Parse /proc/stat file and fill the member values list
*
* Line example:
* cpu 144025136 134535 43652864 42274006316 2718910 6408 1534597 0 131907272 0
*
* This implementation provided by Ashok (@asmurthy) and S.R (@raven)
*
* Another possible implementation can be found at:
* @see http://stackoverflow.com/questions/1420426/calculating-cpu-usage-of-a-process-in-linux
*/
def cpuUsage(delayInMilliseconds: Int=10): Option[Double] = {
// TODO just use time since last sample (if ongoing)
val sampleA = cpuSample()
val sampleB = sampleA flatMap {_ => Thread.sleep(delayInMilliseconds); cpuSample()}
// Calculate CPU %
(sampleA, sampleB) match {
case (Some(a), Some(b)) if b.total > a.total => Option(a.usage(b))
case _ => None
}
}
/**
* Determine if the CPU measurement is available or not.
* Naive solution - just see if a sample works.
*/
def isCpuAvailable() = cpuUsage() isDefined
}
object CpuStatisticsApp extends App with StrictLogging {
println (s"Processors: ${CpuStatistics.ProcessorCount}")
println (s"CPU Usage: ${CpuStatistics.cpuUsage()}")
}
================================================
FILE: src/main/scala/com/ebay/neutrino/balancer/WeightedRoundRobinBalancer.scala
================================================
package com.ebay.neutrino.balancer
import com.ebay.neutrino.{NeutrinoNode, NeutrinoRequest}
import com.typesafe.scalalogging.slf4j.StrictLogging
class WeightedRoundRobinBalancer extends Balancer with StrictLogging {
private case class Entry(node: NeutrinoNode, var load: Int = 0)
private val members = new BalancerNodes[Entry]()
//Re(set) the current membership of the load - balancer
override def rebuild(members: Array[NeutrinoNode]) =
this.members.set(members, Entry(_))
// Resolve an endpoint for request processing
def assign(request: NeutrinoRequest): Option[NeutrinoNode] =
if (members.isEmpty)
None
else
members.find(e => check(e)) map { case (node, entry) =>
entry.synchronized(entry.load += 1)
node
}
private def check(entry: Entry): Boolean =
entry.node.settings.weight.get > entry.load
// Release an endpoint from request processing
def release(request: NeutrinoRequest, node: NeutrinoNode) =
members.get(node) match {
case Some(entry) => entry.synchronized(entry.load -= 1)
case None => logger.warn("Attempt to release a node which was not found in the servers: {}", node)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoChannelId.scala
================================================
package com.ebay.neutrino.channel
import java.util
import io.netty.buffer.{ByteBuf, ByteBufUtil}
import io.netty.channel.ChannelId
/**
* Cut and paste "re" implementation of the DefaultChannelId.
*
* This class provides globally-unique IDs, but unfortunately is both final and
* package-private, and hence can't be serialized or used for cross-instance IDs
* as is required by UUID.
*
* @see io.netty.channel.DefaultChannelId
*/
case class NeutrinoChannelId(data: Array[Byte]) extends ChannelId {
import com.ebay.neutrino.channel.DefaultChannelUtil._
assert(data.length == DATA_LEN)
def machineId() = readLong(data, 0)
def processId() = readInt (data, MACHINE_ID_LEN)
def sequenceId() = readInt (data, MACHINE_ID_LEN+PROCESS_ID_LEN)// SEQUENCE_LEN
def timestamp() = readLong(data, MACHINE_ID_LEN+PROCESS_ID_LEN+SEQUENCE_LEN) // TIMESTAMP_LEN
def random() = readInt (data, MACHINE_ID_LEN+PROCESS_ID_LEN+SEQUENCE_LEN+TIMESTAMP_LEN)
override val asShortText: String =
ByteBufUtil.hexDump(data, MACHINE_ID_LEN + PROCESS_ID_LEN + SEQUENCE_LEN + TIMESTAMP_LEN, RANDOM_LEN);
override val asLongText: String = {
val buf = new StringBuilder(2 * data.length + 5)
var i = 0
buf.append(ByteBufUtil.hexDump(data, i, MACHINE_ID_LEN)); i += MACHINE_ID_LEN
buf.append('-')
buf.append(ByteBufUtil.hexDump(data, i, PROCESS_ID_LEN)); i += PROCESS_ID_LEN
buf.append('-')
buf.append(ByteBufUtil.hexDump(data, i, SEQUENCE_LEN)); i += SEQUENCE_LEN
buf.append('-')
buf.append(ByteBufUtil.hexDump(data, i, TIMESTAMP_LEN)); i += TIMESTAMP_LEN
buf.append('-')
buf.append(ByteBufUtil.hexDump(data, i, RANDOM_LEN)); i += RANDOM_LEN
assert(i == data.length)
buf.toString
}
override lazy val hashCode: Int = random()
override def compareTo(o: ChannelId): Int = 0
override val toString = s"Id[$asLongText]"
override def equals(rhs: Any): Boolean =
rhs match {
case id: NeutrinoChannelId => (id canEqual this) && util.Arrays.equals(data, id.data)
case _ => false
}
}
object NeutrinoChannelId {
/**
* Initialize our ChannelId from its component parts.
*
* @see io.netty.channel.DefaultChannelId#init()
*/
def apply(): NeutrinoChannelId = {
val bytes = DefaultChannelUtil.newInstanceBytes()
NeutrinoChannelId(bytes)
}
def apply(buffer: ByteBuf): NeutrinoChannelId = {
require(DefaultChannelUtil.DATA_LEN == buffer.readableBytes)
val bytes = new Array[Byte](DefaultChannelUtil.DATA_LEN)
buffer.getBytes(0, bytes)
NeutrinoChannelId(bytes)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoChannelLoop.scala
================================================
package com.ebay.neutrino.channel
import java.net.SocketAddress
import java.util
import java.util.concurrent.TimeUnit
import io.netty.channel.ChannelHandlerInvokerUtil._
import io.netty.channel._
import io.netty.util.concurrent.{EventExecutor, Future => NettyFuture}
/**
* This was stolen directly from EmbeddedEventLoop, which unfortunately was package scoped and
* couldn't be used directly.
*
* @see io.netty.channel.embedded.EmbeddedEventLoop
*/
final class NeutrinoChannelLoop extends AbstractEventLoop with ChannelHandlerInvoker {
private final val tasks = new util.ArrayDeque[Runnable](2)
def execute(command: Runnable) {
if (command == null) {
throw new NullPointerException("command")
}
tasks.add(command)
}
// ?? Can we cache the iterator and call takeWhile on each iteration?
private[neutrino] def runTasks() =
Iterator.continually(tasks.poll) takeWhile(_ != null) foreach (_.run)
def shutdownGracefully(quietPeriod: Long, timeout: Long, unit: TimeUnit): NettyFuture[_] =
throw new UnsupportedOperationException
def terminationFuture: NettyFuture[_] =
throw new UnsupportedOperationException
@Deprecated
def shutdown = throw new UnsupportedOperationException
def isShuttingDown: Boolean = false
def isShutdown: Boolean = false
def isTerminated: Boolean = false
def awaitTermination(timeout: Long, unit: TimeUnit): Boolean = false
def register(channel: Channel): ChannelFuture =
register(channel, new DefaultChannelPromise(channel, this))
def register(channel: Channel, promise: ChannelPromise): ChannelFuture = {
channel.unsafe.register(this, promise)
promise
}
override def inEventLoop: Boolean = true
def inEventLoop(thread: Thread): Boolean = true
def asInvoker: ChannelHandlerInvoker = this
def executor: EventExecutor = this
def invokeChannelRegistered(ctx: ChannelHandlerContext) =
invokeChannelRegisteredNow(ctx)
def invokeChannelUnregistered(ctx: ChannelHandlerContext) =
invokeChannelUnregisteredNow(ctx)
def invokeChannelActive(ctx: ChannelHandlerContext) =
invokeChannelActiveNow(ctx)
def invokeChannelInactive(ctx: ChannelHandlerContext) =
invokeChannelInactiveNow(ctx)
def invokeExceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) =
invokeExceptionCaughtNow(ctx, cause)
def invokeUserEventTriggered(ctx: ChannelHandlerContext, event: AnyRef) =
invokeUserEventTriggeredNow(ctx, event)
def invokeChannelRead(ctx: ChannelHandlerContext, msg: AnyRef) =
invokeChannelReadNow(ctx, msg)
def invokeChannelReadComplete(ctx: ChannelHandlerContext) =
invokeChannelReadCompleteNow(ctx)
def invokeChannelWritabilityChanged(ctx: ChannelHandlerContext) =
invokeChannelWritabilityChangedNow(ctx)
def invokeBind(ctx: ChannelHandlerContext, localAddress: SocketAddress, promise: ChannelPromise) =
invokeBindNow(ctx, localAddress, promise)
def invokeConnect(ctx: ChannelHandlerContext, remoteAddress: SocketAddress, localAddress: SocketAddress, promise: ChannelPromise) =
invokeConnectNow(ctx, remoteAddress, localAddress, promise)
def invokeDisconnect(ctx: ChannelHandlerContext, promise: ChannelPromise) =
invokeDisconnectNow(ctx, promise)
def invokeClose(ctx: ChannelHandlerContext, promise: ChannelPromise) =
invokeCloseNow(ctx, promise)
def invokeDeregister(ctx: ChannelHandlerContext, promise: ChannelPromise) =
invokeDeregisterNow(ctx, promise)
def invokeRead(ctx: ChannelHandlerContext) =
invokeReadNow(ctx)
def invokeWrite(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise) =
invokeWriteNow(ctx, msg, promise)
def invokeFlush(ctx: ChannelHandlerContext) =
invokeFlushNow(ctx)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoEvent.scala
================================================
package com.ebay.neutrino.channel
import com.ebay.neutrino.NeutrinoRequest
// Marker interface for Balancer lifecycle events
sealed trait NeutrinoEvent {
def request: NeutrinoRequest
}
object NeutrinoEvent {
// HTTP-specific
// These are pretty simple/generic; might need to tighten these up for utility and/or brevity
case class RequestCreated(request: NeutrinoRequest) extends NeutrinoEvent
case class ResponseReceived(request: NeutrinoRequest) extends NeutrinoEvent
case class ResponseCompleted(request: NeutrinoRequest) extends NeutrinoEvent
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoPipelineChannel.scala
================================================
package com.ebay.neutrino.channel
import java.net.SocketAddress
import com.ebay.neutrino.channel.NeutrinoPipelineChannel.ChannelState
import com.ebay.neutrino.metrics.Instrumented
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel._
import io.netty.util.{Attribute, AttributeKey, ReferenceCountUtil}
/**
* Base class for {@link Channel} NeutrinoChannel implementations; built on the EmbeddedChannel
* framework.
*
* NeutrinoChannel (EmbeddedChannel) is:
* - container for pipeline
* - dedicated to protocol (ie: HTTP)
* - tightly coupled with the "request" (not connection).
* - Knows about each endpoint's framing protocol
* - is responsible for framing down into the endpoints' frames
*
* For simplicity, writing into the EmbeddedChannel handles memory lifecycle and manages virtual
* session containing...
*/
// TODO support user-pipeline thread
class NeutrinoPipelineChannel(parentctx: ChannelHandlerContext)
extends AbstractChannel(parentctx.channel, NeutrinoChannelId())
with StrictLogging
with Instrumented
{
import ChannelFutureListener._
class ForwardingHandler extends ChannelHandlerAdapter with ChannelInboundHandler {
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) =
parentctx.fireExceptionCaught(cause)
override def channelRegistered(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelRegistered()
override def channelUnregistered(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelUnregistered()
override def channelActive(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelActive()
override def channelInactive(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelInactive()
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit =
parentctx.fireChannelRead(msg)
override def channelWritabilityChanged(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelWritabilityChanged()
override def userEventTriggered(ctx: ChannelHandlerContext, evt: AnyRef): Unit =
parentctx.fireUserEventTriggered(evt)
override def channelReadComplete(ctx: ChannelHandlerContext): Unit =
parentctx.fireChannelReadComplete()
}
private class DefaultUnsafe extends AbstractUnsafe {
override def connect(remote: SocketAddress, local: SocketAddress, promise: ChannelPromise): Unit =
safeSetSuccess(promise)
//override protected def flush0() = super.flush0()
}
// Immutable data
val config = new DefaultChannelConfig(this)
// Mutable data
private var parentPromise: ChannelPromise = null
@transient private var state: ChannelState = ChannelState.Open
/**
* Register the provided event-loop and, after registration, mark ourselves as active.
*/
private[channel] def register() = {
// Register ourselves to our event-loop (forcing notification of the channel)
require(state == ChannelState.Open)
parent.eventLoop.register(this)
require(state == ChannelState.Registered)
// 'Activate' our channel
state = ChannelState.Active
}
override protected def newUnsafe: AbstractChannel#AbstractUnsafe =
new DefaultUnsafe
override protected def isCompatible(loop: EventLoop): Boolean =
loop.isInstanceOf[SingleThreadEventLoop]
override protected def doBind(localAddress: SocketAddress) =
{}
override protected def doDisconnect =
{}
override protected def doClose = {
state = ChannelState.Closed
parentPromise match {
case null =>
parentctx.channel.close()
case valid =>
parentctx.close(parentPromise)
parentPromise = null
}
}
override def metadata: ChannelMetadata = NeutrinoPipelineChannel.METADATA
override protected def doBeginRead = parentctx.read() //if (isOpen) (this.upstream getOrElse parent).read()
override protected def doRegister() = (state = ChannelState.Registered)
// Channel is open if we haven't been closed and our parent channel is open
override def isOpen: Boolean = (state != ChannelState.Closed && parentctx.channel.isOpen)
// Channel is active if we haven't been closed and our parent channel is active
override def isActive: Boolean = (state == ChannelState.Active && parentctx.channel.isActive)
/**
* Get the {@link Attribute} for the given {@link AttributeKey}.
* This method will never return null, but may return an {@link Attribute} which does not have a value set yet.
*/
override def attr[T](key: AttributeKey[T]): Attribute[T] = parent.attr(key)
/**
* Returns {@code} true if and only if the given {@link Attribute} exists in this {@link AttributeMap}.
*/
override def hasAttr[T](key: AttributeKey[T]): Boolean = parent.hasAttr(key)
/**
* Override the embedded channel's local address to map to the underlying channel.
* @return
*/
override protected def localAddress0(): SocketAddress = parent.localAddress()
/**
* Override the embedded channel's remote address to map to the underlying channel.
* @return
*/
override protected def remoteAddress0(): SocketAddress = parent.remoteAddress()
/**
* This is called by the AbstractChannel after write/flush through the pipeline
* is completed.
*
* Instead of using outbound message paths, we wire directly back to the upstream
* context (which should delegate to the next handler in the upstream-parent.
*
* @param in
*
*
* NOTE - we currently do this functionality in the UpstreamHandler instead of here.
*/
override protected def doWrite(in: ChannelOutboundBuffer): Unit = {
val outbound = Iterator.continually(in.current()) takeWhile (_ != null)
outbound foreach { msg =>
ReferenceCountUtil.retain(msg)
// TODO handle entry promise; hook promise on future
// Do the write and attach an appropriate future to it
val listener = if (isOpen) CLOSE_ON_FAILURE else CLOSE
val future = parentctx.write(msg).addListener(listener)
// Signal removal from the current channel.
in.remove()
// Flush as required
if (in.isEmpty) parentctx.flush()
}
}
/**
* Hack a custom promise to carry our parent framework's promise through and dispatch the close
* properly back, allowing differential behaviour between close() invoked internally vs externally.
*
* @param parent
* @return
*/
override def close(parent: ChannelPromise): ChannelFuture =
if (parent.channel() == this) {
super.close(parent)
}
else {
parentPromise = parent
super.close()
}
}
object NeutrinoPipelineChannel {
import com.ebay.neutrino.util.AttributeSupport._
// Constants
private val METADATA = new ChannelMetadata(false)
/**
* Channel state constants; these track the allowable states.
*/
sealed private[channel] trait ChannelState
private[channel] object ChannelState {
case object Closed extends ChannelState
case object Open extends ChannelState
case object Active extends ChannelState
case object Registered extends ChannelState
}
/**
* Create a new pipeline-channel and do the initial channel setup.
*
*/
def apply(parentctx: ChannelHandlerContext, handlers: Seq[ChannelHandler]): NeutrinoPipelineChannel =
{
// Create the custom channel
val channel = new NeutrinoPipelineChannel(parentctx)
// Add the handlers.
channel.pipeline.addFirst(handlers:_*)
// Register ourselves to our event-loop (forcing notification of the channel)
channel.register()
// Finally, add a relaying handler to dispatch out from the bottom of the pipeline
channel.pipeline.addLast("user-loopback", new channel.ForwardingHandler)
// Hook the parents' close-future for force closing of our new channel
parentctx.channel.closeFuture.addListener(new ChannelFutureListener {
override def operationComplete(future: ChannelFuture): Unit =
if (channel.isOpen) channel.close()
})
channel
}
def apply(parentctx: ChannelHandlerContext): Option[NeutrinoPipelineChannel] = {
// Extract handler settings and determine if pipeline is required
val settings = parentctx.service map (_.settings)
val handlers = settings map (_.handlers) filter (_.nonEmpty)
// If configured, create pipeline channel around it
handlers map { apply(parentctx, _) }
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoService.scala
================================================
package com.ebay.neutrino.channel
import com.ebay.neutrino.{NeutrinoServiceInitializer, _}
import com.ebay.neutrino.config._
import com.ebay.neutrino.metrics.Instrumented
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.channel.socket.nio.NioServerSocketChannel
import scala.concurrent.Future
/**
* Channel management service/wrapper around incoming VIPs
*
* Incoming channel needs to open like this:
* 1) New Channel
* 2) Figure out remote endpoint
* 3) If Endpoint selected, create endpoint handler last
* 4) If endpoint is not proxy-only, create pipeline handler
*/
@Sharable
class NeutrinoService(val core: NeutrinoCore, val settings: ListenerSettings)
extends StrictLogging
with Instrumented
{
implicit val context = core.context
// Initialized connections
private[this] var incomingConnections = Map[VirtualAddress, Future[Channel]]()
// Configure Listeners
private[this] var established = Map[ListenerAddress, Future[ServerChannel]]()
// Shared handlers
val factory = new NeutrinoServiceInitializer(this)
val pools = new NeutrinoPools(this)
// Listen on all configured addresses.
// These will be all-successful or all-failure.
def listen(): Seq[(ListenerAddress, Future[Channel])] = {
require(established.isEmpty, "Service already started; can't listen() twice")
// Initialize individual ports
val addresses = settings.addresses map ((_, settings))
addresses collect { case (address, settings) =>
require(!established.isDefinedAt(address), s"Address $address already defined; can't initialize twice")
// Create a service to handle the addresses
val pair = (address -> listen(address))
// Build a map-tuple out of this
established += pair
pair
}
}
def shutdown() = {
// TODO close established
}
/**
* Update the service topology/configuration.
*
* We prefilter to remove any pools that are configured for an incompatible protocol
* as our own.
*/
def update(pools: VirtualPool*) =
{
// Split out the relevant (by protocol)
val relevant = pools filter (pool => pool.protocol == settings.protocol)
// Update the internal pool-state
this.pools.update(relevant:_*)
}
/**
* Make the connection attempt.
*
* The challenge here is that we want to inject a fully-formed EndpointHandler
* into the initial pipeline, which requires an EndpointConnection object.
* Unfortunately, this object is created by the caller only after the
* connect has completed.
*
* TODO handle bind() failure; should probably kill the server ASAP
*/
def listen(address: VirtualAddress): Future[ServerChannel] = {
import com.ebay.neutrino.util.Utilities._
logger.info("Creating listener on {}", address)
// Do the socket-bind
val socket = address.socketAddress
val channel = new ServerBootstrap()
.channel(classOf[NioServerSocketChannel])
.group(core.supervisor, core.workers)
.option[java.lang.Integer](ChannelOption.SO_BACKLOG, 512)
.childOption[java.lang.Boolean](ChannelOption.TCP_NODELAY, true)
.childHandler(factory)
.bind(socket)
.future()
.asInstanceOf[Future[ServerChannel]]
logger.info(s"Starting HTTP on port $socket")
// Cache the bound socket (not yet complete)
incomingConnections += (address -> channel)
channel
}
/** Delegate the resolve() request to the connection's pools, using any available resolvers */
def resolvePool(request: NeutrinoRequest): Option[NeutrinoPool] = {
// Iterate any available resolvers, matching the first resolved pool
settings.poolResolvers.toStream flatMap (_.resolve(pools, request)) headOption
}
}
/**
* Implementation of a state wrapper around a set of NeutrinoServices
*
* Should we allow dynamic configuration??
* handle quiescence...
*
* def configure(listeners: ListenerSettings*) = {
* listeners map (new NeutrinoService(this, _))
* }
*
* ?? Should we check for duplicate ports?
*/
class NeutrinoServices(val core: NeutrinoCore) extends Iterable[NeutrinoService] with StrictLogging {
val listeners: Map[ListenerSettings, NeutrinoService] =
core.settings.interfaces map (setting => setting -> new NeutrinoService(core, setting)) toMap
// Get service by port
def forPort(port: Int): Option[NeutrinoService] =
listeners find (_._1.sourcePorts.contains(port)) map (_._2)
// Return an iterator over our configured services
override def iterator: Iterator[NeutrinoService] = listeners.values.iterator
}
================================================
FILE: src/main/scala/com/ebay/neutrino/channel/NeutrinoSession.scala
================================================
package com.ebay.neutrino.channel
import io.netty.channel._
/**
* Create a new instance with the pipeline initialized with the specified handlers.
*
* Note initializers need to be in inner-to-outer order
*/
case class NeutrinoSession(channel: Channel, service: NeutrinoService)
================================================
FILE: src/main/scala/com/ebay/neutrino/cluster/DataSourceSettings.scala
================================================
package com.ebay.neutrino.cluster
import akka.actor._
import com.ebay.neutrino.config.Configuration
import com.ebay.neutrino.config.Configuration._
import com.typesafe.config.Config
import com.ebay.neutrino.datasource.DataSource
import scala.concurrent.duration.{Duration, FiniteDuration}
/**
* Extension providing all settings available to the application:
* - Monitor
*
*/
case class DataSourceSettings(
refreshPeriod: FiniteDuration,
datasourceReader : Class[DataSource]
)
extends Extension
{
def isEnabled() = (refreshPeriod != Duration.Zero)
}
object DataSourceSettings {
// Initialization Constructor
def apply(c: Config): DataSourceSettings =
DataSourceSettings(
c getOptionalDuration "refresh-period" getOrElse Duration.Zero,
c getClass "datasource-reader"
)
def getDataSourceObject(): Unit = {
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/cluster/SLBLoader.scala
================================================
package com.ebay.neutrino.cluster
import akka.actor.Actor
import com.ebay.neutrino.config.{LoadBalancer, Configuration}
import com.typesafe.scalalogging.slf4j.StrictLogging
import scala.concurrent.duration._
import scala.util.{Failure, Success}
import com.ebay.neutrino.datasource.DataSource
class SLBLoader extends Actor with StrictLogging {
import context.dispatcher
// Create a new SLB Configuration based off the file
// Note that the system configuration is pulled from common.conf
val config = SystemConfiguration(context.system)
val dataSourceReader = config.settings.dataSource.datasourceReader.getConstructor().newInstance()
// Schedule a configuration reload
override def preStart() {
context.system.scheduler.schedule(5 seconds, config.settings.dataSource.refreshPeriod, self, "reload")
}
def receive: Receive = {
case "reload" =>
// Create a new SLB configuration
val results = dataSourceReader.load();
logger.info("Reloading the configuration: {}")
config.topology.update(results)
sender ! "complete"
case "complete" =>
logger.info("Reloading of configuration complete")
case msg =>
logger.warn("Unexpected message received: {}", msg.toString)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/cluster/SLBTopology.scala
================================================
package com.ebay.neutrino.cluster
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.{NeutrinoPoolId, NeutrinoCore}
import com.ebay.neutrino.channel.NeutrinoService
import com.ebay.neutrino.config.{LoadBalancer, CanonicalAddress, VirtualPool, Transport}
import com.ebay.neutrino.util.DifferentialStateSupport
class SLBTopology(val core: NeutrinoCore)
extends DifferentialStateSupport[NeutrinoPoolId, VirtualPool]
with Iterable[VirtualPool]
with Instrumented {
type Port = Int
type Key = (Port, Transport)
protected override def key(pool: VirtualPool): NeutrinoPoolId =
NeutrinoPoolId(pool.id.toLowerCase, pool.protocol)
protected override def addState(added: VirtualPool): Unit = {}
protected override def removeState(remove: VirtualPool): Unit = {}
protected override def updateState(pre: VirtualPool, post: VirtualPool): Unit = {}
// Iterator over available pools
override def iterator: Iterator[VirtualPool] = state.iterator
// Split our services into (source-port, protocol) tuples
val services: Map[Key, NeutrinoService] =
core.services flatMap { service =>
val proto = service.settings.protocol
val ports = service.settings.sourcePorts
val pairs = ports map ((_, proto))
// Finally, map pairs to services
pairs map (_ -> service)
} toMap
// Create a gauge for each service...
core.services map { service =>
// Grab the port as a descriptor of the service
val portstr = service.settings.addresses.head.port //map (_.port) mkString "_"
val pools = service.pools
// Create pools- and servers-gauge for the service
metrics.gauge("pools", portstr.toString) { pools.size }
metrics.gauge("servers", portstr.toString) { pools.foldLeft(0) { (sum, pair) => sum + pair.servers.size }}
}
/**
* Hook our parent update to delegate the update-call to the core as well.
*
* We currently use a naive implementation:
* - just find the first matching from-port/protocol
* (eventually, we want to be able to handle multiple matches)
*/
override def update(values: VirtualPool*) = {
// Update our internal state first
super.update(values:_*)
// Split per service and update accordingly, using configured CNAMEs
val resolved = values flatMap { pool =>
//
pool.addresses collectFirst {
case addr: CanonicalAddress if services isDefinedAt ((addr.port, pool.protocol)) =>
services((pool.port, pool.protocol)) -> pool
}
}
// Group per-service
val grouped = resolved groupBy (_._1) mapValues (_ map (_._2))
// Update the services directly with their relevant pools
grouped map {
case (service, pools) => service.update(pools:_*)
}
}
def update(lb: LoadBalancer): Unit = {
// Update the pool configuration (subclasses will cascade changes)
update(lb.pools:_*)
}
def getPool(id: NeutrinoPoolId): Option[VirtualPool] =
state collectFirst {
case pool @ VirtualPool(id.id, _, id.transport, _, _, _, _, _) => pool }
}
================================================
FILE: src/main/scala/com/ebay/neutrino/cluster/SystemSettings.scala
================================================
package com.ebay.neutrino.cluster
import java.io.File
import akka.actor._
import com.ebay.neutrino.NeutrinoCore
import com.ebay.neutrino.config.Configuration._
import com.ebay.neutrino.config.{Configuration, NeutrinoSettings}
import com.typesafe.config.{ConfigFactory, Config}
case class SystemSettings(
enableApi: Boolean,
neutrino: NeutrinoSettings,
dataSource: DataSourceSettings
)
object SystemSettings {
// This config is already at 'ebay.neutrino'
def apply(config: Config): SystemSettings =
SystemSettings(
config getBoolean "enable-api",
NeutrinoSettings(config),
DataSourceSettings(config getConfig "datasource")
)
}
case class SystemConfigurationExtension(system: ExtendedActorSystem) extends Extension
{
// Extract 'ebay.neutrino' config
val config = Configuration.load(system.settings.config, "resolvers")
// Load system-settings (including all component settings)
val settings = SystemSettings(config)
// Initialize our Neutrino-core
val core = new NeutrinoCore(settings.neutrino)
// Our use-specific state cluster topology (customized for SLB)
val topology = {
new SLBTopology(core)
}
}
/**
* System-configuration extension for both Monitor and SLB.
*/
object SystemConfiguration extends ExtensionId[SystemConfigurationExtension] with ExtensionIdProvider {
/** Cache our common configuration */
private val common = ConfigFactory.load("slb.conf")
override def lookup() = SystemConfiguration
override def createExtension(system: ExtendedActorSystem) = SystemConfigurationExtension(system)
def load(filename: String): Config =
filename match {
case null => common
case file => val slbFile = new File(filename)
val slbConfig = ConfigFactory.parseFile(slbFile)
if (slbConfig.isEmpty) {
common
} else {
ConfigFactory.load(slbConfig)
}
}
def system(filename: String): ActorSystem =
ActorSystem("slb-cluster", load(filename))
// Create an actor-system and return the attached configuration, all in one
def apply(filename: String): SystemConfigurationExtension =
SystemConfiguration(system(filename))
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/Configuration.scala
================================================
package com.ebay.neutrino.config
import java.io.File
import java.lang.reflect.Constructor
import java.net.{URI, URL}
import java.util.concurrent.TimeUnit
import com.typesafe.config.{Config, ConfigFactory, ConfigList, ConfigValue}
import com.typesafe.scalalogging.slf4j.StrictLogging
import scala.util.{Failure, Success, Try}
/**
* Configuration static helper methods.
*/
trait HasConfiguration { def config: Config }
object Configuration extends StrictLogging {
import scala.collection.JavaConversions._
import scala.concurrent.duration._
/**
* Load the configuration from the filename and environment provided.
*
* @param filename
* @param environment
*/
def load(filename: String = null, environment: String = null, loader: ClassLoader = null): Config = {
val basecfg = (filename, loader) match {
case (null, null) =>
logger.info("Loading default configuration.")
ConfigFactory.load()
case (null, _) =>
logger.info("Loading default configuration with provided ClassLoader.")
ConfigFactory.load(loader)
case (_, null) =>
logger.info("Loading configuration from file {}", filename)
val slbFile = new File(filename)
val slbConfig = ConfigFactory.parseFile(slbFile)
if (slbConfig.isEmpty) {
ConfigFactory.load(filename)
} else {
ConfigFactory.load(slbConfig)
}
case _ =>
logger.info("Loading configuration from file {} using provided ClassLoader", filename)
ConfigFactory.load(loader, filename)
}
load(basecfg, environment)
}
/**
* Load the configuration from the concrete config-object and environment provided.
*
* @param basecfg
* @param environment
*/
def load(basecfg: Config, environment: String): Config = {
// If environment is provided, attempt to extract the environment-specific subtree
val envcfg = Option(environment) match {
case Some(envpath) if basecfg.hasPath(envpath) =>
logger.warn("Merging with environmental configuration {}", envpath)
basecfg.getConfig(envpath) withFallback basecfg
case Some(envpath) =>
logger.error("Unable to merge with environmental configuration {}; not found", envpath)
basecfg
case None =>
basecfg
}
// Extract our concrete configuration from the ebay-neutrino tree
val neutrino = envcfg.getConfig("neutrino")
// Finally, re-stitch together the timeout defaults
val timeout = neutrino.getConfig("timeout")
neutrino
.withValue("pool.timeout", neutrino.getValue("pool.timeout").withFallback(timeout))
}
implicit class ConfigSupport(val self: Config) extends AnyVal {
// Resolve the constructor provided, skipping unavailable ones
def constructor[T](ctor: => Constructor[T]): Option[Constructor[T]] =
Try(ctor) match {
case Success(ctor) => Option(ctor)
case Failure(x: NoSuchMethodException) => None
case Failure(x) => throw x
}
def optional[T](path: String, f: String => T): Option[T] = self hasPath path match {
case true => Option(f(path))
case false => None
}
def getIntOrList(path: String): Seq[Int] = self getValue path match {
case list: ConfigList => (self getIntList path map (_.toInt)).toList
case _ => Seq(self getInt path)
}
def getStringOrList(path: String): Seq[String] = self getOptionalValue path match {
case None => Seq()
case Some(list: ConfigList) => (self getStringList path).toList
case Some(_) => Seq(self getString path)
}
def getMergedConfigList(path: String, defaultPath: String): List[_ <: Config] = {
val default = self getConfig defaultPath
self getConfigList(path) map (_ withFallback default) toList
}
def getDuration(path: String): FiniteDuration =
self getDuration(path, TimeUnit.NANOSECONDS) nanos
def getProtocol(path: String): Transport =
Transport(self getString path)
def getTimeouts(path: String): TimeoutSettings =
TimeoutSettings(self getConfig path)
def getUrl(path: String): URL =
new URL(self getString path)
def getUri(path: String): URI =
URI.create(self getString path)
def getClass[T](path: String): Class[T] =
Class.forName(self getString path).asInstanceOf[Class[T]]
def getClassList[T](path: String): List[Class[T]] =
self getStringList path map (Class.forName(_).asInstanceOf[Class[T]]) toList
def getClassInstances[T](path: String): List[T] =
self getStringOrList path map (Class.forName(_).newInstance().asInstanceOf[T]) toList
def getInstance[T](path: String): Option[T] =
constructor(Class.forName(self getString path).getConstructor()) map (_.newInstance().asInstanceOf[T])
def getConfigInstance[T](path: String): Option[T] = // Configuration-aware instance
constructor(Class.forName(self getString path).getConstructor(classOf[Config])) map (_.newInstance(self).asInstanceOf[T])
def getOptionalValue(path: String): Option[ConfigValue] =
optional(path, self getValue)
def getOptionalConfig(path: String) =
optional(path, self getConfig)
def getOptionalInt(path: String) =
optional(path, self getInt)
def getOptionalString(path: String) =
optional(path, self getString)
def getOptionalConfigList(path: String): List[_ <: Config] =
optional(path, self getConfigList) map (_.toList) getOrElse List()
def getOptionalDuration(path: String): Option[FiniteDuration] =
optional(path, getDuration) filterNot (_ == Duration.Zero)
def getOptionalClass[T](path: String): Option[Class[T]] =
optional(path, self getString) map (Class.forName(_).asInstanceOf[Class[T]])
def getOptionalClassList[T](path: String): List[Class[T]] =
optional(path, getClassList[T]) getOrElse List[Class[T]]()
def getOptionalInstance[T](path: String): Option[T] =
optional(path, self getInstance).flatten
def getOptionalConfigInstance[T](path: String): Option[T] =
optional(path, self getConfigInstance).flatten
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/ListenerAddress.scala
================================================
package com.ebay.neutrino.config
import java.net.InetSocketAddress
import com.ebay.neutrino.PoolResolver
import com.ebay.neutrino.config.Configuration._
import com.typesafe.config.Config
import io.netty.channel.ChannelHandler
import scala.concurrent.duration.Duration
/**
* An individual listener/port/transport tuple.
* Note that listener-interfaces are transport specific; we duplicate for ease of resolution.
*/
case class ListenerAddress(host: String, port: Int, protocol: Transport) extends VirtualAddress
{
// Expose VirtualAddress as socket-address (?? Does this cache ??)
lazy val socketAddress = new InetSocketAddress(host, port)
}
/**
* Representation of a VIP/Interface Address/Listener.
*
* Note that LBaaS models LoadBalancer with the address and Listener (VIP) as port/protocol
* only LBMS models VIP as containing both address/port.
*
* Validation:
* - Host: Needs to be an IP or DNS address, but is expensive.
* @see http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
* - Port: 0..65535
*
* @param addresses
* @param protocol
* @param handlers
* @param poolResolver
* @param timeouts
*/
case class ListenerSettings(
addresses: Seq[ListenerAddress],
protocol: Transport,
sourcePorts: Seq[Int],
handlers: Seq[_ <: ChannelHandler],
poolResolvers: Seq[_ <: PoolResolver],
channel: ChannelSettings,
timeouts: TimeoutSettings
)
object ListenerSettings {
import com.ebay.neutrino.config.Configuration._
// ListenerSettings factory type; this is the preferred construction
def apply(cfg: Config) = {
// Build a set of addresses around
val hosts = cfg getStringOrList "host"
val ports = cfg getIntOrList "port"
val alias = cfg getIntOrList "port-alias"
val proto = cfg getProtocol "protocol"
// Get the cross-product of host:port
val addresses = for { host <- hosts; port <- ports } yield ListenerAddress(host, port, proto)
new ListenerSettings(
addresses,
proto,
ports ++ alias,
cfg getClassInstances "pipeline-class",
cfg getStringOrList "pool-resolver" map (PoolResolver(_)),
cfg getOptionalConfig "channel-options" map (ChannelSettings(_)) getOrElse ChannelSettings.Default,
cfg getTimeouts "timeout"
) with
HasConfiguration { override val config: Config = cfg }
}
// Factory type; create with some defaults.
def apply(
addresses: Seq[ListenerAddress]=Seq(),
protocol: Transport=Transport.HTTP,
sourcePorts: Seq[Int]=Seq(),
handlers: Seq[_ <: ChannelHandler]=Seq(),
poolResolvers: Seq[_ <: PoolResolver]=Seq()): ListenerSettings =
{
ListenerSettings(addresses, protocol, sourcePorts, handlers, poolResolvers, ChannelSettings.Default, TimeoutSettings.Default)
}
}
/**
* Representation of downstream channel settings.
*
*/
case class ChannelSettings(
forceKeepAlive: Boolean,
auditThreshold: Option[Duration]
)
object ChannelSettings {
val Default = ChannelSettings(true, None)
def apply(cfg: Config) =
new ChannelSettings(
cfg getBoolean "force-keepalive",
cfg getOptionalDuration "audit-threshold"
)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/LoadBalancer.scala
================================================
package com.ebay.neutrino.config
import com.typesafe.config.Config
/**
* LoadBalancer ~= VIP object
* we depict merged with 'Listener' type from DB
*
*
* Missing attributes from LoadBalancer:
* - id: Long not persisted yet; will be rquired for DB backing
* - name: String
* - description: String
* - vip_port_id
* - vip_subnet_id
* - vip_address
* - tenant_id
* -
*/
case class LoadBalancer(id: String, pools: Seq[VirtualPool])
object LoadBalancer {
import scala.collection.JavaConversions._
// LoadBalancer configuration factory; create a group of VIP configurations
def apply(config: Seq[Config]): Seq[LoadBalancer] = config map (LoadBalancer(_))
// LoadBalancer configuration factory; create a single VIP configuration
// TODO support for 'default'
def apply(cfg: Config): LoadBalancer = {
// Build out the subconfig lists, merging with defautls
val name = "default" //config getString "name"
val pool = cfg getConfig "pool"
val pools = cfg getConfigList "pools" map (c => VirtualPool(c withFallback pool))
// Initialize the lifecycle-inits and wrap the load-balancer
new LoadBalancer(name, pools) with HasConfiguration { def config = cfg }
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/README.md
================================================
# Neutrino Load Balancer - Configuration
[Project Documentation - Including design and features](wiki.vip.corp.ebay.com/display/neutrino)
## Configuring SLB or ESB
We use (primarily) the [Typesafe Configuration library](https://github.com/typesafehub/config), which uses a cascading configuration resolved by classpath.
In essence, the core provides a set of default configuration values in reference.conf, and each application can override with:
1) Their own reference.conf or application.conf files in the root of their classpath
2) An application.conf file in the runtime working directory
3) A custom .conf at a user-defined location within the user's classpath.
================================================
FILE: src/main/scala/com/ebay/neutrino/config/Settings.scala
================================================
package com.ebay.neutrino.config
import java.lang.reflect.Constructor
import java.net.URI
import com.ebay.neutrino.NeutrinoLifecycle
import com.ebay.neutrino.balancer.{Balancer, LeastConnectionBalancer, RoundRobinBalancer, WeightedRoundRobinBalancer}
import com.ebay.neutrino.metrics.HealthMonitor
import com.ebay.neutrino.util.Utilities._
import com.typesafe.config.Config
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success, Try}
/**
* Extension providing all settings available to the application:
* - HttpServer/Frontend Server
* - LoadBalancer
* - Daemon
* - Echo-Server
*
* Note that these settings do not include the 'runtime' pool configuration. (LoadBalancer)
* They need to be populated separately (as they can be set dynamically)
*/
case class NeutrinoSettings(
interfaces: Seq[ListenerSettings],
lifecycleListeners: Seq[NeutrinoLifecycle],
defaultTimeouts: TimeoutSettings,
supervisorThreadCount:Int,
workerThreadCount: Int
)
/**
* Core Load-Balancer settings
*/
object NeutrinoSettings {
import com.ebay.neutrino.config.Configuration._
import scala.collection.JavaConversions._
// Try a one-parameter ctor for settings
def createLifecycle(lifecycleClass: Class[_ <: NeutrinoLifecycle], c: Config): Option[NeutrinoLifecycle] =
constructor(lifecycleClass.getConstructor(classOf[Config])) map (_.newInstance(c))
// Try a default (no-param) setting
def createLifecycle(lifecycleClass: Class[_ <: NeutrinoLifecycle]): Option[NeutrinoLifecycle] =
constructor(lifecycleClass.getConstructor()) map (_.newInstance())
// Resolve the constructor provided, skipping unavailable ones
def constructor[T](ctor: => Constructor[T]): Option[Constructor[T]] =
Try(ctor) match {
case Success(ctor) => Option(ctor)
case Failure(x: NoSuchMethodException) => None
case Failure(x) => throw x
}
// Empty configuration for mocking/empty setup
val Empty = NeutrinoSettings(Seq(), Seq(), TimeoutSettings.Default, 1, 4)
// Create a new global LoadBalancer application setting object.
// For now, just support 'default'
def apply(cfg: Config) = {
// Load config-wide defaults
val listener = cfg getConfig("listener")
val classes: List[Class[NeutrinoLifecycle]] = cfg getOptionalClassList "initializers"
val instances = classes flatMap (cls => createLifecycle(cls, cfg) orElse createLifecycle(cls))
new NeutrinoSettings(
cfg getConfigList "listeners" map (_ withFallback listener) map (ListenerSettings(_)),
instances,
cfg getTimeouts "timeout",
cfg getInt "supervisorThreadCount",
cfg getInt "workerThreadCount"
)
}
}
/**
* Daemon/End-Node Settings
*
*
*/
case class DaemonSettings(endpoint: URI)
{
val host = endpoint.validHost
val port = endpoint.validPort(80)
val isSecure = endpoint.isSecure()
}
object DaemonSettings {
def apply(c: Config): DaemonSettings =
DaemonSettings(
URI.create(c getString "endpoint")
)
}
// Representation of a Health Monitor/Settings
case class HealthSettings(monitorType: String, path: URI, monitor: Option[HealthMonitor]) {
require(path.getHost == null && path.getPort == -1 && path.getAuthority == null,
"URL Path/Port/Authority not supported - only path should be set")
}
object HealthSettings {
import com.ebay.neutrino.config.Configuration._
// Try a one-parameter ctor for settings
def createMonitor(monitorClass: Class[_ <: HealthMonitor], c: Config): Try[HealthMonitor] =
Try(monitorClass.getConstructor(classOf[HealthSettings]).newInstance(c))
// Try a default (no-param) setting
def createMonitor(monitorClass: Class[_ <: HealthMonitor]): Try[HealthMonitor] =
Try(monitorClass.newInstance())
// HealthMonitor configuration factory
def apply(config: Config): HealthSettings = {
val clazz: Option[Class[HealthMonitor]] = config getOptionalClass "monitor-class"
HealthSettings(
"type",
config getUri "url",
clazz flatMap (cls => (createMonitor(cls, config) orElse createMonitor(cls)).toOption)
)
}
}
case class TimeoutSettings(
readIdle: Duration,
writeIdle: Duration,
writeCompletion: Duration,
requestCompletion: Duration,
sessionCompletion: Duration,
connectionTimeout: Duration
)
{
require(!readIdle.isFinite() || readIdle != Duration.Zero)
require(!writeIdle.isFinite() || writeIdle != Duration.Zero)
require(!writeCompletion.isFinite() || writeCompletion != Duration.Zero)
require(!requestCompletion.isFinite() || requestCompletion != Duration.Zero)
require(!sessionCompletion.isFinite() || sessionCompletion != Duration.Zero)
require(!sessionCompletion.isFinite() || sessionCompletion != Duration.Zero)
}
object TimeoutSettings {
import com.ebay.neutrino.config.Configuration._
import scala.concurrent.duration.Duration.Undefined
import scala.concurrent.duration._
val Default = TimeoutSettings(60 seconds, 60 seconds, 10 seconds, 2 minutes, 2 minutes, 5 seconds)
val NoTimeouts = TimeoutSettings(Undefined, Undefined, Undefined, Undefined, Undefined, Undefined)
def apply(config: Config): TimeoutSettings = TimeoutSettings(
config getOptionalDuration "read-idle-timeout" getOrElse Undefined,
config getOptionalDuration "write-idle-timeout" getOrElse Undefined,
config getOptionalDuration "write-timeout" getOrElse Undefined,
config getOptionalDuration "request-timeout" getOrElse Undefined,
config getOptionalDuration "session-timeout" getOrElse Undefined,
config getOptionalDuration "connection-timeout" getOrElse Undefined
)
}
import scala.language.existentials
case class BalancerSettings(clazz: Class[_ <: Balancer], config: Option[Config])
{
require(NeutrinoLifecycle.hasConstructor(clazz),
"Balancer class must expose either a no-arg constructor or a Config constructor.")
}
object BalancerSettings {
// Static balancers available
val RoundRobin = BalancerSettings(classOf[RoundRobinBalancer], None)
val WeightedRoundRobin = BalancerSettings(classOf[WeightedRoundRobinBalancer], None)
val LeastConnection = BalancerSettings(classOf[LeastConnectionBalancer], None)
// Default to Round-Robin scheduling with no additional configuation
val Default = RoundRobin
/**
* Select a load-selection mechanism.
*
* Attempt to resolve class for scheduler.
* If it's known, use the class directly. Otherwise, attempt to resolve.
*
* @param balancer
*/
def apply(balancer: String): BalancerSettings =
balancer toLowerCase match {
case "rr" | "round-robin" => RoundRobin
case "wrr" | "weighted-round-robin" => WeightedRoundRobin
case "lc" | "least-connection" => LeastConnection
case className =>
BalancerSettings(
Class.forName(balancer).asInstanceOf[Class[Balancer]],
None
)
}
// def apply(config: Config): BalancerSettings = ...
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/Types.scala
================================================
package com.ebay.neutrino.config
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.handler.codec.http.HttpResponse
/**
* This file provides enumeration types.
*
*/
// Transport protocol support
sealed trait Transport
object Transport extends StrictLogging {
// Subtype
case object HTTP extends Transport { override val toString = "http" }
case object HTTPS extends Transport { override val toString = "https" }
case object ZMQ extends Transport { override val toString = "zeromq" }
case class Unknown(value: String) extends Transport { override val toString = value }
// Resolve a transport-protocol type from the string provided.
def apply(protocol: String): Transport =
protocol.toLowerCase match {
case "http" => HTTP
case "https" | "ssl" => HTTPS
case "zmq" => ZMQ
case lc =>
logger.error("Protocol '{}' not supported", protocol)
Unknown(protocol)
}
}
/**
* Encapsulated request-completion status enumeration/bucketing support.
*/
sealed trait CompletionStatus
object CompletionStatus {
// Supported types
case object Status2xx extends CompletionStatus
case object Status4xx extends CompletionStatus
case object Status5xx extends CompletionStatus
case object Other extends CompletionStatus
case object Incomplete extends CompletionStatus
// Extract/bucket a response
def apply(response: Option[HttpResponse]): CompletionStatus =
(response map (_.status.code / 100) getOrElse 0) match {
case 0 => Incomplete
case 2 => Status2xx
case 4 => Status4xx
case 5 => Status5xx
case _ => Other
}
}
// Heath-state types
sealed trait HealthState
object HealthState {
// Supported types
case object Healthy extends HealthState
case object Unhealthy extends HealthState
case object Probation extends HealthState
case object Maintenance extends HealthState
case object Error extends HealthState
case object Unknown extends HealthState
case class Other(value: String) extends HealthState
}
/**
* Data wrapper classes
*/
case class Host(host: String, port: Int)
{
require(port >= 0 && (port >> 16) == 0, "Illegal port: " + port)
require(host == host.toLowerCase, "Host should be normalized (to lower case)")
def hasPort() = port > 0
}
object Host {
// Create a host-object out of the message provided
def apply(host: String): Host =
host.split(":") match {
case Array(host) => Host(host, 0)
case Array(host, port) => Host(host, port.toInt)
case array => throw new IllegalArgumentException(s"Bad split for host $host: $array")
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/VirtualAddress.scala
================================================
package com.ebay.neutrino.config
import java.net.{InetSocketAddress, SocketAddress}
import com.typesafe.config.Config
/**
* Representation of a VIP/Interface Address/Listener.
*
*/
trait VirtualAddress {
def socketAddress: SocketAddress
}
object VirtualAddress {
// Borrowed from http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
val ValidIpAddressRegex = """^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$""".r
val ValidHostnameRegex = """^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$""".r
// Determine if the IP provided is valid
@inline def isIPValid(ip: String) = ValidIpAddressRegex.pattern.matcher(ip).matches
// Determine if the hostname provided is valid (note - will match IPs loosely too)
@inline def isHostnameValid(hostname: String) = ValidHostnameRegex.pattern.matcher(hostname).matches
}
/**
* Note that LBaaS models LoadBalancer with the address and Listener (VIP) as port/protocol
* only LBMS models VIP as containing both address/port.
*
* Validation:
* - Host: Needs to be an IP or DNS address, but is expensive.
* @see http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
* - Port: 0..65535
*
* @param host
* @param port
* @param protocol
* @param handlers
* @param timeouts
*/
case class CanonicalAddress(host: String, port: Int, protocol: Transport, timeouts: TimeoutSettings)
extends VirtualAddress
{
require(host.nonEmpty, "Host is required and was not provided")
require(VirtualAddress.isHostnameValid(host), s"Host provided '$host' is not valid")
lazy val socketAddress: SocketAddress = new InetSocketAddress(host, port)
}
object CanonicalAddress {
import com.ebay.neutrino.config.Configuration._
// VIP parent-configuation factory; this is the preferred way
def apply(cfg: Config, port: Int, protocol : Transport): CanonicalAddress =
new CanonicalAddress(
cfg getString "host",
port,
protocol,
TimeoutSettings.Default
) with
HasConfiguration { override val config: Config = cfg }
// VIP configuration factory; factory initializer
def apply(host: String="localhost", port: Int=8080, protocol: Transport=Transport.HTTP) =
new CanonicalAddress(host, port, protocol, TimeoutSettings.Default)
}
/**
* VIP Address representation that can be applied to URI-based filtering.
*
* @param host
* @param path
*/
case class WildcardAddress(host: String, path: String, port : Int)
extends VirtualAddress
{
require(host.nonEmpty, "Host is required and was not provided")
require(VirtualAddress.isHostnameValid(host), s"Host provided '$host' is not valid")
lazy val socketAddress: SocketAddress = new InetSocketAddress(host, port)
}
object WildcardAddress {
import com.ebay.neutrino.config.Configuration._
def apply(cfg: Config, port: Int): WildcardAddress =
new WildcardAddress(
cfg getString "host",
cfg getString "path",
port
) with
HasConfiguration { override val config: Config = cfg }
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/VirtualPool.scala
================================================
package com.ebay.neutrino.config
import com.ebay.neutrino.config.Configuration._
import com.typesafe.config.Config
import scala.collection.mutable
/**
* Representation of a Pool
*/
case class VirtualPool(
id: String,
port: Int,
protocol: Transport,
servers: Seq[VirtualServer],
addresses: Seq[VirtualAddress],
health: Option[HealthSettings],
balancer: BalancerSettings,
timeouts: TimeoutSettings
)
{
require(servers.map(_.id).distinct.size == servers.size, "Server IDs must be unique")
}
object VirtualPool {
import scala.collection.JavaConversions._
// Pool configuration factory
def apply(servers: VirtualServer*): VirtualPool =
VirtualPool("default", Transport.HTTP, servers)
// Pool/Protocol configuration factory
def apply(protocol: Transport, servers: VirtualServer*): VirtualPool =
VirtualPool("default", protocol, servers)
// Defaulted values configuration factor
def apply(id: String="default", protocol: Transport=Transport.HTTP, servers: Seq[VirtualServer]=Seq(), address: Seq[CanonicalAddress]=Seq(), port: Int=80): VirtualPool =
new VirtualPool(id, port, protocol, servers, address, None, BalancerSettings.Default, TimeoutSettings.Default)
// Create with parent configuration deafults; this is the preferred way
def apply(cfg: Config): VirtualPool = {
var addresses: mutable.Buffer[CanonicalAddress] = mutable.Buffer()
// Default the port to 80 if it is not specified
val port = if (cfg hasPath "port") cfg getInt "port" else 80
// Default the protocol to http if it is not specified
val protocol = if (cfg hasPath "protocol") cfg getProtocol "protocol" else Transport.HTTP
if (cfg hasPath "addresses") {
addresses = cfg getConfigList "addresses" map (CanonicalAddress(_, port, protocol))
}
var wildcardAddresses: mutable.Buffer[WildcardAddress] = mutable.Buffer()
if (cfg hasPath "wildcard") {
wildcardAddresses = cfg getConfigList "wildcard" map (WildcardAddress(_, port))
}
new VirtualPool(
cfg getString "id",
port,
protocol,
cfg getConfigList "servers" map (VirtualServer(_)),
addresses ++ wildcardAddresses,
cfg getOptionalConfig "health" map (HealthSettings(_)),
cfg getOptionalString "balancer" map (BalancerSettings(_)) getOrElse BalancerSettings.Default,
cfg getTimeouts "timeout"
) with
HasConfiguration {
override val config: Config = cfg
}
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/config/VirtualServer.scala
================================================
package com.ebay.neutrino.config
import java.net.InetSocketAddress
import com.typesafe.config.Config
// Representation of a Server
case class VirtualServer(
id: String,
host: String,
port: Int,
weight: Option[Int] = None,
health: Option[HealthSettings] = None
)
extends VirtualAddress {
// Expose VirtualAddress as socket-address (?? Does this cache ??)
lazy val socketAddress = new InetSocketAddress(host, port)
// Mutable health state
@transient var healthState: HealthState = HealthState.Unknown
}
object VirtualServer {
import Configuration._
/**
* VirtualServer configuration factory.
*/
def apply(cfg: Config): VirtualServer =
new VirtualServer(
cfg getOptionalString "id" getOrElse (cfg getString "host"), // fallback to 'host'
cfg getString "host",
cfg getInt "port",
if (cfg hasPath "weight") Option(cfg getInt "weight") else None
) with
HasConfiguration {
override val config: Config = cfg
}
}
// Representation of a Backend Service
//case class Service()
================================================
FILE: src/main/scala/com/ebay/neutrino/datasource/DataSource.scala
================================================
package com.ebay.neutrino.datasource
import com.ebay.neutrino.config.{LoadBalancer, Configuration}
import com.typesafe.config.Config
/**
* Created by blpaul on 2/24/2016.
*/
trait DataSource {
// refresh the datasource
def load() : LoadBalancer
}
class FileReader extends DataSource {
override def load(): LoadBalancer = {
val results = Configuration.load("/etc/neutrino/slb.conf", "resolvers")
LoadBalancer(results)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/NeutrinoClientHandler.scala
================================================
package com.ebay.neutrino.handler
import java.io.IOException
import java.util.ArrayDeque
import com.ebay.neutrino.channel.NeutrinoSession
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel._
import io.netty.handler.codec.http._
/**
* A downstream/client handler for the post-pipeline version of the outer channel handler.
*
* In its current form, this handler exists only to manage the current state of the downstream
* channel, and facilitate management from the outside (ie: during allocation/release)
*
*/
class NeutrinoClientHandler extends ChannelDuplexHandler with StrictLogging
{
import ChannelFutureListener.CLOSE_ON_FAILURE
import com.ebay.neutrino.util.AttributeSupport._
/** Keep-Alive pending flag */
@volatile var keepAlive = true
/** A queue that is used for correlating a request and a response. */
val queue = new ArrayDeque[HttpMethod]()
/**
* Check current state for 'available' and no outstanding request/responses.
*/
@inline def isAvailable(channel: Channel) =
channel.isActive() && keepAlive && queue.isEmpty && !inProgress(channel)
/**
* Determine if the current handler has any outstanding/in-progress requests or responses.
*/
@inline def inProgress(channel: Channel) = {
val stats = channel.statistics
stats.requestCount.get() != stats.responseCount.get()
}
/**
* Determine if a close is pending on this channel
* (ie: close has been requested and should be performed on downstream completion)
*/
@inline def isClosePending() = queue.isEmpty && !keepAlive
/**
* Handle incoming response data coming decoded from our response-decoder.
*
* Normal case is session-established; just pass thru.
* If not established, we should fail unless it's the last packet of the response, in which case
* we can just discard.
*/
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
// Track our message state/completion
if (msg.isInstanceOf[HttpResponse]) {
// Update our keep-alive flag to false if the response indicates a downstream intention to close
require(queue.poll() != null)
keepAlive &= HttpHeaderUtil.isKeepAlive(msg.asInstanceOf[HttpResponse])
}
if (msg.isInstanceOf[LastHttpContent]) {
val stats = ctx.statistics
require(stats.responseCount.incrementAndGet() == stats.requestCount.get())
}
ctx.session match {
case Some(NeutrinoSession(channel, _)) =>
channel.write(msg).addListener(CLOSE_ON_FAILURE)
case None if (msg.isInstanceOf[LastHttpContent]) =>
// Just discard
case None if (isAvailable(ctx.channel)) =>
logger.warn("Read data on unallocated session: {}", msg)
case None =>
logger.info("Read data on closed session; closing")
ctx.close()
}
}
override def channelReadComplete(ctx: ChannelHandlerContext): Unit =
ctx.session match {
//case Some(session) if state.isAvailable() =>
// shouldn't need to flush; downstream handler will on 'last'
case Some(NeutrinoSession(channel, _)) => channel.flush()
case None => // Fairly common; just ignore
}
/**
* Handle outgoing HTTP requests.
*/
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise): Unit =
{
msg match {
case data: HttpRequest =>
queue.offer(data.method)
ctx.statistics.requestCount.incrementAndGet()
case _ =>
}
ctx.write(msg, promise)
}
override def close(ctx: ChannelHandlerContext, promise: ChannelPromise): Unit = {
ctx.session match {
case Some(NeutrinoSession(channel, _)) if channel.isOpen() => channel.close(promise)
case _ =>
}
ctx.close(promise)
}
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit =
(cause, ctx.session) match {
case (_: IOException, Some(NeutrinoSession(channel, _))) =>
channel.close()
ctx.close()
case (_: IOException, None) =>
ctx.close()
case (_, Some(NeutrinoSession(channel, _))) =>
channel.pipeline.fireExceptionCaught(cause)
case (_, None) =>
logger.warn("Unhandled exception on unallocated session", cause)
ctx.fireExceptionCaught(cause)
}
}
object NeutrinoClientHandler {
// Helper methods for downstream channel management
implicit class NeutrinoClientSupport(val self: Channel) extends AnyVal /*with AttributeKeySupport */{
//def attr[T](key: AttributeKey[T]): Attribute[T] = self.attr(key)
//def channel = self
def handler = self.pipeline.get(classOf[NeutrinoClientHandler])
/**
* Stateful introspection into our downstream channel.
* Determine whether or not it's open and in a consistent state for receiving new connections.
*/
def isAvailable(): Boolean = self.isActive() && handler.isAvailable(self)
/**
*
*/
def isReusable(): Boolean = self.isActive && !handler.isClosePending()
}
}
/**
* We leverage the majority of Netty's original HttpClientCodec to help facilitate some edge
* cases, including HttpPipelining and HEAD/CONNECT.
*
* A combination of {@link HttpRequestEncoder} and {@link HttpResponseDecoder}
* which enables easier client side HTTP implementation. {@link HttpClientCodec}
* provides additional state management for HEAD and CONNECT
* requests, which {@link HttpResponseDecoder} lacks. Please refer to
* {@link HttpResponseDecoder} to learn what additional state management needs
* to be done for HEAD and CONNECT and why
* {@link HttpResponseDecoder} can not handle it by itself.
*
* If the {@link Channel} is closed and there are missing responses,
* a {@link PrematureChannelClosureException} is thrown.
*
*
* Constants; these should be moved to settings.
* val maxInitialLineLength = 4096
* val maxHeaderSize = 8192
* val maxChunkSize = 8192
* val validateHeaders = true
*/
class NeutrinoClientDecoder(queue: ArrayDeque[HttpMethod]) extends HttpResponseDecoder(4096, 8192, 8192, true)
with StrictLogging
{
override protected def isContentAlwaysEmpty(msg: HttpMessage): Boolean =
{
val statusCode = msg.asInstanceOf[HttpResponse].status.code
// 100-continue response should be excluded from paired comparison.
if (statusCode == 100) return true
// Get the getMethod of the HTTP request that corresponds to the
// current response.
if (queue.isEmpty) {
logger.warn("Empty queue... Error")
}
val method = queue.peek()
val firstChar = method.name.charAt(0)
firstChar match {
case 'H' if (HttpMethod.HEAD.equals(method)) =>
// According to 4.3, RFC2616:
// All responses to the HEAD request getMethod MUST NOT include a
// message-body, even though the presence of entity-header fields
// might lead one to believe they do.
true
// The following code was inserted to work around the servers
// that behave incorrectly. It has been commented out
// because it does not work with well behaving servers.
// Please note, even if the 'Transfer-Encoding: chunked'
// header exists in the HEAD response, the response should
// have absolutely no content.
//
//// Interesting edge case:
//// Some poorly implemented servers will send a zero-byte
//// chunk if Transfer-Encoding of the response is 'chunked'.
////
//// return !msg.isChunked();
// Successful CONNECT request results in a response with empty body.
case 'C' if (statusCode == 200 && HttpMethod.CONNECT.equals(method)) =>
// Proxy connection established - Not HTTP anymore.
//done = true;
true
case _ =>
super.isContentAlwaysEmpty(msg)
}
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/NeutrinoDownstreamHandler.scala
================================================
package com.ebay.neutrino.handler
import java.io.IOException
import java.net.{ConnectException, NoRouteToHostException, UnknownServiceException}
import java.nio.channels.ClosedChannelException
import com.ebay.neutrino.NeutrinoRequest
import com.ebay.neutrino.handler.NeutrinoDownstreamHandler.DownstreamConnection
import com.ebay.neutrino.handler.ops.{AuditActivity, NeutrinoAuditHandler}
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.util.{Preconditions, ResponseGenerator, Utilities}
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.{ByteBuf, ByteBufHolder}
import io.netty.channel._
import io.netty.handler.codec.http.HttpResponseStatus._
import io.netty.handler.codec.http.{HttpContent, LastHttpContent}
import scala.util.{Failure, Success, Try}
/**
* Base class for {@link Channel} NeutrinoChannel implementations; built on the EmbeddedChannel
* framework.
*
* NeutrinoChannel (EmbeddedChannel) is:
* - container for pipeline
* - dedicated to protocol (ie: HTTP)
* - tightly coupled with the "request" (not connection).
* - Knows about each endpoint's framing protocol
* - is responsible for framing down into the endpoints' frames
*
* For simplicity, writing into the EmbeddedChannel handles memory lifecycle and manages virtual
* session containing...
*/
object NeutrinoDownstreamHandler {
// Allowed FSM States for the downstream handler
// - Available: Ready to attempt downstream connection
// - Connecting: Downstream connection in progress
// - Connected: Downstream connection established; packets can be sent through as-is
// - Closed: Connection has been closed and can't handle new requests/events
private sealed trait State
private case object Available extends State
private case class Connecting(attempt: NeutrinoDownstreamAttempt) extends State
private case class Connected(request: NeutrinoRequest, downstream: Channel) extends State
private case object Closed extends State
// Supported events
case class DownstreamConnection(request: NeutrinoRequest, downstream: Try[Channel])
// This could be externalized
val generator = new ResponseGenerator()
}
/**
* Create a new instance with the pipeline initialized with the specified handlers.
*
* Note initializers need to be in inner-to-outer order
*/
class NeutrinoDownstreamHandler extends ChannelDuplexHandler with Instrumented with StrictLogging
{
import com.ebay.neutrino.handler.NeutrinoDownstreamHandler._
import com.ebay.neutrino.util.AttributeSupport._
// Mutable data
private var state: State = Available
/**
* Close the underlying session and clean up any outstanding state.
*/
private def close(ctx: ChannelHandlerContext) = {
state = Closed
if (ctx.channel.isOpen) ctx.close()
}
// These are intended to cascade down through any remaining incoming (upstream) handlers
override def channelInactive(ctx: ChannelHandlerContext): Unit = {
// Handle state cleanup
state match {
case Connecting(attempt) if (!attempt.pending.isEmpty) =>
case _ =>
}
state = Closed
ctx.fireChannelInactive
}
override def channelWritabilityChanged(ctx: ChannelHandlerContext): Unit =
ctx.fireChannelWritabilityChanged
/**
* Process reads against the bottom of our pipeline. These will be sent across to the downstream.
*
* Our possible states during execution of this are:
* - Downstream connected, messages pending: Flush pending, Send packet through
* - Downstream connected, no pending: Send packet through
* - Not connected,
*/
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
// Establish/terminate downstream connections here
(state, msg) match {
case (Available, data: NeutrinoRequest) =>
// Initiate our downstream connection attempt
state = Connecting(new NeutrinoDownstreamAttempt(ctx, data))
metrics.counter("created") += 1
case (Connecting(attempt), data: HttpContent) =>
attempt.pending = attempt.pending :+ data
case (Connected(request, downstream), data: HttpContent) =>
downstream.write(msg).addListener(new NeutrinoDownstreamListener(ctx, data))
case _ =>
logger.warn("Received an unexpected message for downstream when channel was {} - ignoring: {}", state, msg)
}
}
override def channelReadComplete(ctx: ChannelHandlerContext): Unit = {
state match {
case Connected(request, downstream) => downstream.flush()
case _ => // ?? error here??
}
ctx.fireChannelReadComplete()
}
/**
* Handle our request/downstream-channel lifecycle events.
*
* This is pretty hacky; see if we can find a better way...
*/
override def userEventTriggered(ctx: ChannelHandlerContext, event: AnyRef): Unit = {
event match {
case DownstreamConnection(request, Success(channel)) if (request.downstream.isEmpty) =>
// Channel was connected, but is no longer active; either side timed out
case DownstreamConnection(request, Success(channel)) =>
state match {
/**
* Flush the pending operations.
*
* Note that this is externally-called only (not called from within this class).
* This allows us to rely on the caller to provide required synchronization, and
* implicitly rely on the calling channel's thread-safely.
*/
case Connecting(attempt) =>
require(request.downstream.isDefined, "Downstream should be set")
logger.debug("Downstream established. Flushing {} message(s) from {}", attempt.pending.size.toString, request.requestUri)
metrics.counter("established") += 1
// Flush any pending messages to the channel provided
if (attempt.pending.nonEmpty) {
attempt.pending foreach { msg =>
channel.write(msg).addListener(new NeutrinoDownstreamListener(ctx, msg))
}
channel.flush()
attempt.pending = Seq.empty
}
// Store the successful downstream
state = Connected(request, channel)
case Closed =>
// Channel was closed between connect-request and now.
// Do we have to explicitly release the request and clean up? Or can we just ignore and implicitly do it
case other =>
throw new IllegalStateException(s"Should have been Connecting or Closed, was $state")
}
case DownstreamConnection(request, Failure(ex)) =>
// Generate additional diagnostics on unexpected failures
val metricname = ex match {
case ex: UnknownServiceException => "rejected.no-pool"
case ex: ConnectTimeoutException => "rejected.timeout"
case ex: ConnectException => "rejected.no-connect"
case _ => "rejected"
}
// Reset our state to take future connections
state = Available
// Propagate the failure to our exception handling
metrics.counter(metricname) += 1
ctx.pipeline.fireExceptionCaught(ex)
case _ =>
ctx.fireUserEventTriggered(event)
}
}
/**
* Intercept the write; release our downstream on completion of the response.
*
* @param ctx
* @param msg
* @param promise
*/
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise): Unit =
msg match {
case _: LastHttpContent =>
require(state.isInstanceOf[Connected], "State should be connected")
val request = Preconditions.checkDefined(ctx.request, "Request should be active in the session")
// On completion of response, release the current request
request.pool.release(false)
// Write and flush the downstream message
// (explicitly flush here, because our pool-release may prevent downstream flush
// from finding our way up)
ctx.writeAndFlush(msg, promise)
// Ensure it's in-progress
state match {
case Available | _:Connected =>
// Normal operation; return state to 'ready'
state = Available
metrics.counter("completed.") += 1
case Connecting(attempt) =>
// Terminated abnormally; we won't be able to recover so just close
close(ctx)
metrics.counter("completed.premature") += 1
case Closed =>
// Skip - already closed
case _ =>
throw new IllegalStateException(s"ResponseCompleted should only occur when Available/Connecting/Connected (was $state)")
}
case _ =>
// All other message types
ctx.write(msg, promise)
}
/**
* These may not be necessary; in lieu of anything better to do, we'll send the eventing
* back to our original upstream (inbound).
*
* Recognized exception:
* - UnknownServiceException: No pool available/resolved
* - NoRouteToHostException: No nodes available in pool
* - ConnectException: Unable to connect to downstream
*
* @param ctx
* @param cause
*/
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit =
cause match {
case ex: NoSuchElementException if (ex.getMessage() == "None.get") =>
// TODO fix this case; see NEUTRINO-JIRA
metrics.counter("exception.NoSuchElementException") += 1
case ex: UnknownServiceException =>
// Unable to resolve a real pool. May be able to recover; ignore and propagate
val response = generator.generate(SERVICE_UNAVAILABLE, cause.getMessage)
ctx.writeAndFlush(response)
case ex: NoRouteToHostException =>
// Unable to get a healthy member from the resolved pool.
val response = generator.generate(SERVICE_UNAVAILABLE, cause.getMessage)
ctx.writeAndFlush(response)
case ex: ConnectException =>
// Unable to connect to downstream (including timeout)
// Do we need to close on this this??
val response = generator.generate(GATEWAY_TIMEOUT, cause.getMessage)
ctx.writeAndFlush(response)
case ex: ClosedChannelException =>
// Additional write attempts on a closed connection; just ignore and close
close(ctx)
case ex: IOException =>
// We won't be able to recover, so close the session/channel
close(ctx)
case ex =>
logger.warn("Unhandled exception in channel session", cause)
ctx.fireExceptionCaught(cause)
}
}
/**
* This supports a three-stage connection establishment process.
*
* 1) Request downstream from pool
* 2) On invalid downstream, quit (unable to send)
* 3) On valid downstream, attempt to send the pending payload message
* 4) On invalid send return to 1)
* 5) On valid send, channel is open and we can continue
*
* Note that 3) is required to determine whether the channel is actually open, or just hasn't
* detected the close yet. A send-attempt will fail if the channel is actually closed.
*
* @param ctx
* @param request
*/
class NeutrinoDownstreamAttempt(ctx: ChannelHandlerContext, request: NeutrinoRequest) extends ChannelFutureListener with StrictLogging {
import NeutrinoAuditHandler._
import com.ebay.neutrino.util.Utilities._
// Cache outstanding messages pending completion of our connection
var pending = Seq.empty[HttpContent]
// Apply back-pressure to the incoming channel
// Should we mess around with the other settings to minimize in-progress inbound buffer too?
ctx.channel.config.setAutoRead(false)
// Kick off the negotiation
establish()
// If not established, or force is provided, renegotiate the endpoint
// Grab the connection's current pool, and see if we have a resolver available
// If downstream not available, attempt to connect one here
import request.session.service.context
def establish() =
request.connect() onComplete {
case Success(endpoint) =>
// Grab our first available result, if possible, and hook configuration of endpoint on success
// Attempt to send the pending payload over the channel.
endpoint.writeAndFlush(request).addListener(this)
case failure @ Failure(ex) =>
// Flat-out failed to return a viable channel. This is a pool-resolver failure
// Resend a user-event notification of completion (to process within the thread-env)
//?? upstream.setAutoRead(true)
ctx.pipeline.fireUserEventTriggered(DownstreamConnection(request, failure))
}
/**
* Handle any downstream channel-errors here.
* @param future
*/
override def operationComplete(future: ChannelFuture): Unit = {
import future.channel
future.isSuccess match {
case true =>
// Audit-log the completion
channel.audit(AuditActivity.ChannelAssigned(channel))
// Resend a user-event notification of completion (to process within the thread-env)
ctx.pipeline.fireUserEventTriggered(DownstreamConnection(request, Success(channel)))
// Downstream endpoint resolved; turn the tap back on
ctx.channel.config.setAutoRead(true)
case false =>
// Audit-log the completion
channel.audit(AuditActivity.ChannelException(channel, future.cause))
logger.info("Downstream failed on connect - closing channel {} and bound to {} and retrying", channel.toStringExt, ctx.channel.toStringExt)
channel.close
// Release the request back to the pool (but leave the pool resolved)
request.pool.release(false)
// Reattempt to establish a connection with a new channel
establish()
}
}
}
class NeutrinoDownstreamListener(ctx: ChannelHandlerContext, val payload: HttpContent) extends ChannelFutureListener with StrictLogging
{
import Utilities._
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler.NeutrinoAuditSupport
import scala.concurrent.duration._
val start = System.nanoTime()
val size =
payload match {
case data: ByteBuf => data.readableBytes()
case data: ByteBufHolder => data.content.readableBytes
case data => 0
}
/**
* Handle any downstream channel-errors here.
* @param future
*/
override def operationComplete(future: ChannelFuture): Unit = {
import future.channel
channel.audit(
AuditActivity.Detail(s"DownstreamWrite: $size = ${future.isSuccess}, cause ${future.cause}"))
future.cause match {
case null =>
// Successful send; ignore
// TODO add audit record
case ex: ClosedChannelException if (!channel.isActive && payload.isInstanceOf[LastHttpContent]) =>
// Downstream channel was already closed; ensure we're closed too
logger.debug("Downstream write failed - channel was already closed before final packet. Closing our upstream.")
ctx.close()
case ex: ClosedChannelException if (!channel.isActive) =>
// Downstream channel was already closed; ensure we're closed too
logger.info("Downstream write failed - channel was already closed. Closing our upstream.")
ctx.close() //ctx.pipeline.fireExceptionCaught(future.cause)
case ex =>
// Handle downstream write-failure
val elapsed = ((System.nanoTime()-start)/1000) micros;
logger.info("Downstream write failed - closing channel {} (Failed with {} in {}, size {}, packet # {})", channel.toStringExt, future.cause, elapsed, size.toString)
channel.close
// Also propagate the IO-failure event to the session for better handling
ctx.pipeline.fireExceptionCaught(future.cause)
}
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/NeutrinoPipelineHandler.scala
================================================
package com.ebay.neutrino.handler
import com.ebay.neutrino.channel.NeutrinoPipelineChannel
import com.ebay.neutrino.util.AttributeSupport
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.util.AttributeKey
/**
* Stateless user-pipeline handler bridge between our parent (system) channel and
* the user-pipeline, which is intended to be self-containe.
*
* On register, this handler will add a user-pipeline to the parent channel context
* if required (if there are user-handlers). Subsequent events (both inbound and
* outbound) will be routed through the user-pipeline channel if it's present,
* and skipped if not.
*
*
* Incomplete/outstanding items:
* - TODO handle propagation of close from NeutrinoChannel to parent channel
*/
@Sharable
class NeutrinoPipelineHandler extends ChannelDuplexHandler with StrictLogging
{
import com.ebay.neutrino.handler.NeutrinoPipelineHandler._
override def channelRegistered(ctx: ChannelHandlerContext) = {
// Create the custom channel, if we have user handlers
ctx.userpipeline = NeutrinoPipelineChannel(ctx)
// Secondly, dispatch the channel-registered to our children
// (creation would have already registered the internal handlers)
ctx.fireChannelRegistered()
}
override def channelUnregistered(ctx: ChannelHandlerContext) = {
// If user-pipeline is configured, deregister it and remove
ctx.userpipeline map { user =>
user.pipeline.fireChannelUnregistered()
ctx.userpipeline = None
}
ctx.fireChannelUnregistered()
}
override def channelActive(ctx: ChannelHandlerContext): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireChannelActive()
case None => ctx.fireChannelActive()
}
override def channelInactive(ctx: ChannelHandlerContext): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireChannelInactive()
case None => ctx.fireChannelInactive()
}
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireChannelRead(msg)
case None => ctx.fireChannelRead(msg)
}
override def channelWritabilityChanged(ctx: ChannelHandlerContext): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireChannelWritabilityChanged()
case None => ctx.fireChannelWritabilityChanged()
}
override def channelReadComplete(ctx: ChannelHandlerContext): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireChannelReadComplete()
case None => ctx.fireChannelReadComplete()
}
override def userEventTriggered(ctx: ChannelHandlerContext, msg: AnyRef): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.fireUserEventTriggered(msg)
case None => ctx.fireUserEventTriggered(msg)
}
override def flush(ctx: ChannelHandlerContext): Unit = ctx.userpipeline match {
case Some(user) => user.pipeline.flush()
case None => ctx.flush()
}
override def close(ctx: ChannelHandlerContext, promise: ChannelPromise): Unit = ctx.userpipeline match {
case Some(user) => user.close(promise)
case None => ctx.close(promise)
}
//override def read(ctx: ChannelHandlerContext): Unit =
// outbound(ctx).read()
// We'll cheat a little here; we'll assume the downstream handler (shoudl just be 'Downstream')
// are not going to put a promise on this
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise) =
ctx.userpipeline match {
case Some(user) =>
logger.info("Writing outbound message to user-pipeline channel: {}", msg)
//require(promise.isEmpty())
//user.pipeline.write(msg, promise)
user.pipeline.write(msg)
case None =>
ctx.write(msg, promise)
}
}
/**
* Static helpers for User-pipeline support methods, including Attribute getter/setter.
*
* These are localized to this file instead of the stock AttributeSupport utilties as we don't
* need external visibility.
*
*/
object NeutrinoPipelineHandler {
import AttributeSupport.AttributeMapSupport
// Constants
private val UserPipelineKey = AttributeKey.valueOf[NeutrinoPipelineChannel]("user-pipeline")
// Neutrino user-pipeline (optional) getter and setter
implicit private class NeutrinoPipelineSupport(val self: ChannelHandlerContext) extends AnyVal {
def userpipeline = self.get(UserPipelineKey)
def userpipeline_=(value: NeutrinoPipelineChannel) = self.set(UserPipelineKey, Option(value))
def userpipeline_=(value: Option[NeutrinoPipelineChannel]) = self.set(UserPipelineKey, value)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/NeutrinoRequestHandler.scala
================================================
package com.ebay.neutrino.handler
import com.codahale.metrics.annotation.Timed
import com.ebay.neutrino._
import com.ebay.neutrino.channel.NeutrinoEvent
import com.ebay.neutrino.handler.ops.AuditActivity
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler._
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.util.Preconditions
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.codec.http._
import scala.util.{Failure, Success}
/**
* A handler for managing the HTTP request state of the channel.
*
* Since we're using stateful HTTP codecs on both sides of our pipeline channel, we need
* to sandwich a HTTP-specific handler between them to:
* 1) Track HTTP state and request/response lifecycle events
* 2) Perform any channel conditioning.
*
* TODO move http-state/modification/logic into a context object
*/
@Sharable
class NeutrinoRequestHandler extends ChannelDuplexHandler with StrictLogging with Instrumented {
import com.ebay.neutrino.handler.NeutrinoRequestHandler._
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler._
import com.ebay.neutrino.util.AttributeSupport._
import io.netty.handler.codec.http.HttpHeaderUtil.isKeepAlive
// Composite channel-future listener
class ResponseCompleteListener(request: NeutrinoRequest) extends ChannelFutureListener {
// Handle upstream keep-alive; if not keepAlive, kill it
override def operationComplete(future: ChannelFuture): Unit = {
val channel = future.channel()
// Clean up channel, if request is outstanding
if (channel.request.isDefined && channel.request.get == request) channel.request = None
// Handle output write exception
future.cause match {
case null =>
// Close the underlying channel, unless keep-alive is specified
if (channel.isActive && !isKeepAlive(request.response.get)) channel.close()
case ex =>
logger.warn("Unable to write response", future.cause)
channel.close()
}
// Clear the current request and remove references to this request on our channel
request.complete()
}
}
/**
* Set the current keep-alive value.
*
* Upstream:
* Only use if both upstream and downstream permit keepalive
*
* Downstream:
* We use the response, if provided, and default to the request's value.
*
* @param channel
* @param request
* @param response
*/
def setResponseHeaders(channel: Channel, request: NeutrinoRequest, response: HttpResponse) =
{
// Set keep-alive headers; if connection not available just drop packet
// Also close the underlying channel if it has timed-out
val elapsed = channel.statistics.elapsed
val timeout = (elapsed > request.session.service.settings.timeouts.sessionCompletion)
val downstream = isKeepAlive(response)
val upstream = request.requestKeepalive && downstream && !timeout
// Update request/response state with the downstream response copy
request.response = Option(response)
// Reset upstream keepalive to match it's expectations, and close downstream if not used anymore
if (downstream != upstream) setKeepAlive(response, upstream, request.protocolVersion)
// We're done with the response on the downstream channel; close it if no keepalive
// TODO move this to the downstream channel stateful pipeline
if (!downstream) request.downstream map (_.close())
}
/**
* Connect downstream connection.
*
* On valid HttpRequest, create a new Connection and add it to the channel provided.
* Handle transport-tier proxying issues
*
* UNUSED - to remove if reference not needed:
*
* I think we can just pass everything through.
* case _ if ctx.request.isDefined =>
* // Ensure we have a valid connection
*
* case _: LastHttpContent =>
* // Don't care about these; may be generated after either side has closed the connection
* // return
*
* case _ =>
* // Connection not valid; unable to handle msg downstream
* ctx.close()
* logger.warn("Connection is expected but missing from context; closing channel and swallowing {}", msg)
*
* Notes:
* TODO Transport-convert the incoming request; reset the correct downstream headers
* TODO decrease max-forwards by 1, if 0 then what?
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
* @param ctx
* @param msg
*/
@Timed
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
import com.ebay.neutrino.util.Utilities._
logger.info("Message read: {}", msg)
msg match {
case http: HttpRequest =>
// Register the request-level connection entity on the new channel with the Balancer
val created = NeutrinoRequest.create(ctx.channel, http)
// Clean up any existing request
// (ie: if the previous request's final write hasn't completed yet
ctx.request map (_.complete())
// Register the new request
ctx.request = created.toOption
created match {
case Success(request) =>
// Ensure our downstream uses keepalive, if applicable
if (request.session.service.settings.channel.forceKeepAlive) HttpHeaderUtil.setKeepAlive(request, true)
// Register the request-level connection entity on the new channel with the Balancer
// Create a Neutrino request-object and event it
val event = NeutrinoEvent.RequestCreated(request)
ctx.channel.audit(AuditActivity.Event(event))
ctx.fireUserEventTriggered(event)
// Register the request completion event; will notify on request completion
request.addListener(new NeutrinoAuditLogger(request))
// Delegate the request downstream
ctx.fireChannelRead(request)
case Failure(ex) =>
// Handle invalid requests by failure type
val message = (ex, ex.getCause) match {
case (_: IllegalArgumentException, se: java.net.URISyntaxException) =>
s"Invalid request URI provided: ${http.uri}\n\n${se.getMessage}"
case _ =>
logger.warn("Unable to create NeutrinoRequest", ex)
s"Invalid request: ${ex.toString}"
}
// Generate an error message
ctx.sendError(HttpResponseStatus.BAD_REQUEST, message).addListener(ChannelFutureListener.CLOSE)
}
case last: LastHttpContent if (ctx.request.isEmpty) =>
// Probably a pending write after a request-pipeline close. Just swallow
case _ =>
ctx.fireChannelRead(msg)
}
}
/**
* Auto-close unless keep-alive.
*
* Update the connection state with the response/headers, setting any appropriate
* shared state.
*/
override def write(ctx: ChannelHandlerContext, msg: Object, promise: ChannelPromise): Unit = {
// Grab the start of the response
msg match {
case response: HttpResponse =>
val session = Preconditions.checkDefined(ctx.session)
val request = Preconditions.checkDefined(ctx.request)
logger.info("Writing response: {}", response)
// Reset upstream keepalive to match it's expectations, and close downstream if not used anymore
setResponseHeaders(ctx.channel, request, response)
// Notify a user-event on the response (allows the pipeline to handle...)
val event = NeutrinoEvent.ResponseReceived(request)
request.audit(ctx.channel, AuditActivity.Event(event))
ctx.fireUserEventTriggered(NeutrinoEvent.ResponseReceived(request))
case _ =>
}
// Handle 'last packet'
msg match {
case _: LastHttpContent =>
logger.info("Setting close-listener on last content")
// Close the underlying channel, unless keep-alive is specified
val listener = new ResponseCompleteListener(ctx.request.get)
val unvoid = promise.unvoid().addListener(listener)
ctx.write(msg, unvoid)
case _ =>
ctx.write(msg, promise)
}
}
/**
* Called when backpressure options are set.
*/
override def channelWritabilityChanged(ctx: ChannelHandlerContext): Unit = {
logger.info("Downstream writeablility changed... is now {}", ctx.channel.isWritable.toString)
ctx.fireChannelWritabilityChanged()
}
}
object NeutrinoRequestHandler extends StrictLogging {
/**
* Set an appropriate request/response keep-alive based on the version and value
* provided.
*
* We override the default implementation in HttpHeaders.setKeepAlive(self, keepalive)
* to ensure we send Connection.KeepAlive on HTTP1.1-response to HTTP1.0-request.
* (as required by apache-bench)
*
* @see http://serverfault.com/questions/442960/nginx-ignoring-clients-http-1-0-request-and-respond-by-http-1-1
*
* @param message
* @param keepalive
* @param version
*/
def setKeepAlive(message: HttpMessage, keepalive: Boolean, version: HttpVersion) = {
import HttpHeaderNames.CONNECTION
import message.headers
logger.info("Setting keepalive to {} for request version {}", keepalive.toString, version)
(keepalive, version) match {
case (true, HttpVersion.HTTP_1_0) => headers.set(CONNECTION, HttpHeaderValues.KEEP_ALIVE)
case (true, HttpVersion.HTTP_1_1 | _) => headers.remove(CONNECTION)
case (false, HttpVersion.HTTP_1_0) => headers.remove(CONNECTION)
case (false, HttpVersion.HTTP_1_1 | _) => headers.set(CONNECTION, HttpHeaderValues.CLOSE)
}
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/NeutrinoServiceInitializer.scala
================================================
package com.ebay.neutrino
import java.util.concurrent.TimeUnit
import com.ebay.neutrino.channel.{NeutrinoSession, NeutrinoService}
import com.ebay.neutrino.handler._
import com.ebay.neutrino.handler.ops.{ChannelStatisticsHandler, ChannelTimeoutHandler, NeutrinoAuditHandler}
import com.ebay.neutrino.metrics.{Instrumented, Metrics}
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.codec.http.HttpServerCodec
/**
* Neutrino-specific handlers that allow separation of the framework/endpoint's handlers from
* user-defined handlers (ie: Pipeline Handlers)
*
* Ideally, the user would have control over their pipeline customization, and the
* framework would be able to wrap both operational and transport/framing handlers
* around in a way that insulates the endpoints.
*
* We can solve this one of a couple of ways:
*
* 1) Reimplement an interface around ChannelHandlerContext, which in turn reimplements
* the ChannelPipeline implementation, to sandbox calls to addFirst() and addLast()
* to within a bounded internal subset.
*
* Would call a version of initChannel(customCtx) which would allow the user to
* register their own handlers and condition their own application needs, without
* gaining access to the underlying pipeline implementation (and consequently the
* operational/framework handlers.
*
* 2) Invoke the initChannel() method first, and then apply our framework handlers
* to the list after the fact.
*
*
* Version 2 is easier in the short term, and is thus implemented. Users may elect from
* replacing this with Version-1 to provide a truer 'sandbox'
*
*
* Initialize framing handlers.
* Subclasses should implement, or mix-in, framing implementations
* For Version-2 (see above) it's critical these implementations install handlers at the
* front of the pipeline and not rely on addLast()
*/
@Sharable
class NeutrinoServiceInitializer(service: NeutrinoService)
extends ChannelInboundHandlerAdapter
with ChannelFutureListener
with Instrumented
with StrictLogging
{
import com.ebay.neutrino.util.AttributeSupport._
import service.settings
// Shared handlers
val stats = new ChannelStatisticsHandler(true)
val request = new NeutrinoRequestHandler()
val audit = new NeutrinoAuditHandler()
val user = new NeutrinoPipelineHandler()
val timeout = new ChannelTimeoutHandler(settings.timeouts)
// Stateful handler factories
@inline def server = new HttpServerCodec()
@inline def downstream = new NeutrinoDownstreamHandler()
override def channelRegistered(ctx: ChannelHandlerContext): Unit =
{
// Extract the underlying neutrino pipeline channel
val channel = ctx.channel
val pipeline = ctx.pipeline
var success = false
// Update our downstream metrics
Metrics.UpstreamTotal.mark
Metrics.UpstreamOpen += 1
// TODO support NeutrinoPipeline for annotating handler calls.
// TODO move into NeutrinoChannel??
//val pipeline = new NeutrinoPipeline(channel.pipeline())
// Create new Neutrino managed pipeline for the channel's user-handlers
// TODO add traffic shaping/management support here
try {
// Initialize our framing
// pipeline.addLast(new io.netty.handler.logging.LoggingHandler(io.netty.handler.logging.LogLevel.INFO))
// pipeline.addLast("logging", new HttpDiagnosticsHandler())
//
// Note; we can only use our underlying pipeline, since some of the handlers are composites
// and don't handle the NeutrinoPipeline well
pipeline.addLast("timeout", timeout)
pipeline.addLast("statistics", stats)
pipeline.addLast("http-decoder", server)
pipeline.addLast("http-state", request)
// Add audit pipeline, if configured
settings.channel.auditThreshold map (_ => pipeline.addLast("audit", audit))
// Initialize the user-pipeline handlers... (including ChannelInitializers)
pipeline.addLast("user-pipeline", user)
// Final handler will reroute inbound to our downstream, as available
pipeline.addLast("downstream", downstream)
// Prime the statistics object and hook channel close for proper cleanup
channel.service = service
channel.session = NeutrinoSession(channel, service)
channel.statistics
// Cleanup on channel close
channel.closeFuture.addListener(this)
// Apply our own
ctx.fireChannelRegistered()
success = true
}
catch {
case th: Throwable =>
// Not successful
logger.warn("Failed to initialize channel {}. Closing {}", ctx.channel(), th)
ctx.close()
}
finally {
if (pipeline.context(this) != null) pipeline.remove(this)
}
}
/**
* Handle channel-completion events; cleanup any outstanding channel resources.
* @param future
*/
override def operationComplete(future: ChannelFuture): Unit = {
val stats = future.channel.statistics
// Cleaning up any outstanding requests
future.channel.request map (_.complete())
future.channel.request = None
// Register a close-listener on the session (to clean up the session state)
Metrics.UpstreamOpen -= 1
if (stats.requestCount.get > 0) {
Metrics.SessionActive -= 1
Metrics.SessionDuration update(System.nanoTime()-stats.startTime, TimeUnit.NANOSECONDS)
}
logger.info("Releasing session {}", this)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/ChannelStatisticsHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}
import com.ebay.neutrino.metrics.Instrumented
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.{ByteBuf, ByteBufHolder}
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import scala.concurrent.duration._
/**
* This handler does rudimentary channel-based accounting.
*
*
*/
@Sharable
class ChannelStatisticsHandler(upstream: Boolean) extends ChannelDuplexHandler with StrictLogging with Instrumented
{
import com.ebay.neutrino.util.AttributeSupport._
import com.ebay.neutrino.util.Utilities.AtomicLongSupport
import com.ebay.neutrino.metrics.Metrics._
// Global statistics
val readBytes = if (upstream) UpstreamBytesRead else DownstreamBytesRead
val writeBytes = if (upstream) UpstreamBytesWrite else DownstreamBytesWrite
val readPackets = if (upstream) UpstreamPacketsRead else DownstreamPacketsRead
val writePackets = if (upstream) UpstreamPacketsWrite else DownstreamPacketsWrite
@inline def calculateSize(msg: AnyRef) = msg match {
case data: ByteBuf => data.readableBytes()
case data: ByteBufHolder => data.content.readableBytes
case data => 0
}
// Log to global (and local) statistics
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
val bytes = calculateSize(msg)
readPackets.mark
readBytes += bytes
ctx.statistics.readPackets += 1
ctx.statistics.readBytes += bytes
ctx.fireChannelRead(msg)
}
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise): Unit = {
val bytes = calculateSize(msg)
writePackets.mark
writeBytes += bytes
ctx.statistics.writePackets += 1
ctx.statistics.writeBytes += bytes
ctx.write(msg, promise)
}
}
class ChannelStatistics {
// Collected traffic statistics
val readBytes = new AtomicLong()
val writeBytes = new AtomicLong()
val readPackets = new AtomicLong()
val writePackets = new AtomicLong()
// Channel usage statistics
val startTime = System.nanoTime()
val allocations = new AtomicInteger()
// Request statistics
val requestCount = new AtomicInteger()
val responseCount = new AtomicInteger()
// Helper methods
def elapsed = (System.nanoTime()-startTime).nanos
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/ChannelTimeoutHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import java.util.concurrent.{ConcurrentLinkedQueue, TimeUnit}
import com.ebay.neutrino.config.TimeoutSettings
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.util.Utilities
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.timeout.{ReadTimeoutException, WriteTimeoutException}
import io.netty.util.concurrent.ScheduledFuture
import scala.concurrent.duration.Duration
object ChannelTimeout {
/**
* Timeout types.
*/
sealed trait TimeoutType
case object ReadIdle extends TimeoutType
case object WriteIdle extends TimeoutType
case object WriteIncomplete extends TimeoutType
case object RequestMax extends TimeoutType
case object SessionMax extends TimeoutType
/**
* Factor constructor.
* Create a ChannelTimeout object if (and only if) related timeouts are defined in the settings.
*/
def apply(ctx: ChannelHandlerContext, settings: TimeoutSettings): Option[ChannelTimeout] = {
val min = Seq(settings.readIdle, settings.writeIdle, settings.writeCompletion, settings.requestCompletion).min
if (min < Duration.Undefined)
Option(new ChannelTimeout(ctx, settings))
else
None
}
}
/**
* Generic timeout task for supporting read, write, or combined idle timeouts, and a modified
* write-completion timeout.
*
* Idle timeouts:
* - simply poll at a minimal interval and check on each interval to see if our time
* elapsed has been exceeded.
*
* Write completion:
* - Different than the existing Netty write-completion timeout
* - Traditionally, would be implemented with a one-task per write; each write would schedule
* a timeout callback on itself, and then check to see if it had been cancelled (by a
* subsequent write completion)
* - This incurs a very large cost in terms of GC pressure, as each task and task-schedule
* both create objects
* - Instead, treat in the same way as the idle-timeout
* - Store 'pending' timeouts in a queue, and pop them as writes complete (we assume in-order
* completion)
* - On scheduled timeout trigger, we check to see if the oldest pending k has exceeded
* our maximum write threshold.
*
* @param ctx
*/
class ChannelTimeout(ctx: ChannelHandlerContext, settings: TimeoutSettings)
extends Runnable
with ChannelFutureListener
with StrictLogging
{
import ChannelTimeout._
import com.ebay.neutrino.util.AttributeSupport._
import scala.concurrent.duration.Duration.Undefined
import scala.concurrent.duration._
val maxDelay = 2500 millis
// Track our pending writes, ordered by write-time
val pendingWrites = new ConcurrentLinkedQueue[Long]()
// Track our pending timeout (to allow cancellation)
@volatile var pendingTask: ScheduledFuture[_] = null
@volatile var lastRead = System.nanoTime
@volatile var lastWrite = System.nanoTime
// Start schedule immediate
schedule(defaultDelay)
def schedule(delay: Duration) = {
require(pendingTask == null, "task should not be pending")
if (delay.isFinite) pendingTask = ctx.executor.schedule(this, delay.toNanos, TimeUnit.NANOSECONDS)
}
@inline def elapsed(eventTime: Long) =
(System.nanoTime - eventTime) nanoseconds
def sinceLast =
elapsed(Math.max(lastRead, lastWrite))
def nextDelay =
Seq(readDelay, writeDelay, requestDelay).min
def defaultDelay =
Seq(settings.readIdle, settings.writeIdle, settings.requestCompletion, settings.sessionCompletion, maxDelay).min
def readDelay =
settings.readIdle-elapsed(lastRead)
def writeDelay =
settings.writeIdle-elapsed(lastWrite)
def requestDelay =
settings.requestCompletion-(ctx.request map (_.elapsed) getOrElse Undefined)
def completionDelay = pendingWrites.isEmpty match {
case true => Duration.Undefined
case false => settings.writeCompletion-elapsed(pendingWrites.peek())
}
/**
* Write-completion notification.
*
* On write-completion:
* - record our last-write time
* - remove a pending write-completion (the oldest)
*/
override def operationComplete(future: ChannelFuture) = {
lastWrite = System.nanoTime
require(pendingWrites.poll().asInstanceOf[Any] != null, "expected a write pending")
}
// TODO if session is valid, smaller timeout, otherwise bigger timeout
override def run(): Unit =
if (ctx.channel.isOpen) {
// Cache our delay
val (read, write, request, completion) = (readDelay, writeDelay, requestDelay, completionDelay)
val next = Seq(read, write, request, completion).min
// Clear our pending task
pendingTask = null
logger.debug("Executing a timeout task with delays ({}, {}, {}, {}), {} pending writes.",
read, write, request, completion, pendingWrites.size().toString)
next match {
case delay if (delay.isFinite && delay > Duration.Zero) =>
// Operation occurred before the timeout
// Set a new timeout with shorter delay, OR handle idle
schedule(delay.min(maxDelay))
if (sinceLast >= maxDelay) {
// Force a write-check on the channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE)
import Utilities._
logger.debug("Channel {} is idle for {}ms", ctx.channel.toStringExt, sinceLast.toMillis.toString)
}
case delay if (delay.isFinite) =>
// Timed out - set a new timeout and notify the callback.
schedule(defaultDelay)
val timeoutType = delay match {
case delay if (request <= Duration.Zero) => RequestMax
case delay if (read <= Duration.Zero) => ReadIdle
case delay if (write <= Duration.Zero) => WriteIdle
case delay if (completion <= Duration.Zero) => WriteIncomplete
case _ =>
import Utilities._
logger.warn("Unrecognized delay {} ({}, {}, {}, {}) on channel {}",
delay, read, write, request, completion, ctx.channel.toStringExt
)
ReadIdle
}
ctx.pipeline.fireUserEventTriggered(timeoutType)
case _ =>
}
}
/**
* Cancel any outstanding operations and release the task.
*/
def cancel() =
if (pendingTask != null) {
// Clear the pending task
pendingTask.cancel(false)
pendingTask = null
// Clear any outstanding writes
pendingWrites.clear()
}
/**
* Notify the timeout-handler of a read.
*/
def notifyRead() = lastRead = System.nanoTime
/**
* Notify the timeout-handler of a pending write, and indicate whether or not
* we are interested in listening for a callback on write-completion.
*
* @return true if we should be registered as a write-listener, false otherwise
*/
def notifyWrite(): Boolean = {
// Determine our interest in write-operations
val listen = (settings.writeCompletion.isFinite || settings.writeIdle.isFinite)
// Register ourself as a pending write
if (listen) pendingWrites.add(System.nanoTime())
listen
}
}
/**
* An amalgamated timeout handler supporting both Netty's stock timeouts and custom timeouts:
* - read idle: How long since last read operation
* - write idle: How long since last write operation
* - write completion: How long until a write has been completed (ack's by other side)
* - request completion: How long until an HTTP Request has been completed
* - session completion: How long until an HTTP session has been completed
*
* @param settings
*/
@Sharable
class ChannelTimeoutHandler(settings: TimeoutSettings, exceptionOnTimeout: Boolean=false)
extends ChannelDuplexHandler
with Instrumented
with StrictLogging
{
import com.ebay.neutrino.handler.ops.ChannelTimeout._
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler._
import com.ebay.neutrino.util.AttributeSupport._
// Context-cached
val counter = metrics.counterset("timeout")
/**
* Clear any pending timeout tasks on this channel.
* @param ctx
*/
private def clearTimeout(ctx: ChannelHandlerContext) = {
ctx.timeout map { timeout =>
timeout.cancel()
ctx.timeout = None
}
}
/**
* Register and activate the timeout on channel activation.
*
* There is some debate as to where this creation should actually happen.
* Other possibilities are channelRegistered() or handlerAdded().
* For our specific use-case, we expect this handler will be added prior to the
* channel activation, and want it to be as close as possible to the activation.
*
* @param ctx
*/
override def channelActive(ctx: ChannelHandlerContext): Unit = {
require(ctx.timeout.isEmpty)
ctx.timeout = ChannelTimeout(ctx, settings)
ctx.fireChannelActive()
}
override def channelInactive(ctx: ChannelHandlerContext): Unit = {
clearTimeout(ctx)
ctx.fireChannelInactive()
}
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
ctx.timeout map (_.notifyRead())
ctx.fireChannelRead(msg)
}
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise): Unit =
ctx.timeout match {
// If write-timeouts are specified, put a completion listener to mark our write timers
case Some(timeout) if timeout.notifyWrite() =>
// Cancel the scheduled timeout if the flush future is complete.
val completed = promise.unvoid()
completed.addListener(timeout)
ctx.write(msg, completed)
case _ =>
ctx.write(msg, promise)
}
/**
* Is called when any supported timeout was detected.
*/
override def userEventTriggered(ctx: ChannelHandlerContext, evt: AnyRef): Unit =
evt match {
case timeout: TimeoutType if ctx.timeout.isDefined =>
// Handle timeout notification
counter(timeout.getClass) += 1
val cause = timeout match {
case WriteIdle | WriteIncomplete => WriteTimeoutException.INSTANCE
case ReadIdle => ReadTimeoutException.INSTANCE
case _ => ReadTimeoutException.INSTANCE
}
// Store the audit record on the request, if available
ctx.channel.audit(AuditActivity.ChannelException(ctx.channel, cause))
// If required, throw the timeout exception
if (exceptionOnTimeout) ctx.fireExceptionCaught(cause)
ctx.close()
clearTimeout(ctx)
case timeout: TimeoutType =>
// Already closed; just ignore
case _ =>
ctx.fireUserEventTriggered(evt)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/HeaderDiagnosticsHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.codec.http.HttpMessage
/**
* Simple diagnostics shim to examine HTTP traffic passing through the channel.
*
*/
@Sharable
class HeaderDiagnosticsHandler(headersOnly: Boolean=true) extends ChannelDuplexHandler with StrictLogging {
import scala.collection.JavaConversions._
@inline def format(message: HttpMessage): String =
message.headers() map { header => s"\t\t${header.getKey} => ${header.getValue}" } mkString("\n")
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
msg match {
case message: HttpMessage =>
if (headersOnly)
logger.info("{} Headers received:\n{}", msg.getClass.getName, format(message))
else
logger.info("Message received: {}", msg)
case _ =>
}
ctx.fireChannelRead(msg)
}
override def write(ctx: ChannelHandlerContext, msg: AnyRef, promise: ChannelPromise): Unit = {
msg match {
case message: HttpMessage =>
if (headersOnly)
logger.info("{} Headers sent:\n{}", msg.getClass.getName, format(message))
else
logger.info("Message sent: {}", msg)
case _ =>
}
ctx.write(msg, promise)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/HttpDiagnosticsHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
/**
* Simple diagnostics shim to examine HTTP traffic passing through the channel.
*
*/
@Sharable
class HttpDiagnosticsHandler extends ChannelDuplexHandler with StrictLogging {
import com.ebay.neutrino.util.AttributeSupport._
override def close(ctx: ChannelHandlerContext, future: ChannelPromise): Unit = {
logger.info("Channel {} Closing.", ctx.session)
ctx.close(future)
}
override def channelRegistered(ctx: ChannelHandlerContext): Unit = {
logger.info("Channel {} Registered.", ctx.session)
ctx.fireChannelRegistered()
}
override def channelUnregistered(ctx: ChannelHandlerContext): Unit = {
logger.info("Channel {} Un-registered.", ctx.session)
ctx.fireChannelUnregistered()
}
override def channelActive(ctx: ChannelHandlerContext): Unit = {
logger.info("Channel {} active.", ctx.session)
ctx.fireChannelActive()
}
override def channelInactive(ctx: ChannelHandlerContext): Unit = {
logger.info("Channel {} Inactive.", ctx.session)
ctx.fireChannelInactive()
}
override def write(ctx: ChannelHandlerContext, msg: Object, promise: ChannelPromise): Unit = {
logger.info("Channel {} writing msg {}", ctx.session, msg)
ctx.write(msg, promise)
}
override def flush(ctx: ChannelHandlerContext): Unit = {
logger.info("Channel {} flushing.", ctx.session)
ctx.flush()
}
override def channelRead(ctx: ChannelHandlerContext, msg: Object): Unit = {
logger.info("Channel {} reading: {}", ctx.session, msg)
ctx.fireChannelRead(msg)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/NeutrinoAuditHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import java.util
import com.ebay.neutrino.NeutrinoRequest
import com.ebay.neutrino.channel.NeutrinoEvent
import com.ebay.neutrino.util.Utilities
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.{ByteBuf, ByteBufHolder}
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.codec.http._
/**
* Hook the Channel session to audit incoming/outgoing channel events for post-
* analysis.
*
*/
@Sharable
class NeutrinoAuditHandler extends ChannelDuplexHandler with StrictLogging
{
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler._
import com.ebay.neutrino.handler.ops.AuditActivity._
@inline def calculateSize(msg: AnyRef) = msg match {
case data: ByteBuf => data.readableBytes()
case data: ByteBufHolder => data.content.readableBytes
case data => 0
}
override def userEventTriggered(ctx: ChannelHandlerContext, event: AnyRef): Unit = {
ctx.channel.audit(UserEvent(event))
ctx.fireUserEventTriggered(event)
}
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
ctx.channel.audit(
msg match {
case data: HttpRequest => Request(data)
case data: HttpResponse => Response(data)
case data: LastHttpContent => Content(data.content.readableBytes, true)
case data: HttpContent => Content(data.content.readableBytes)
case data: ByteBuf => ReadData(data.readableBytes)
})
ctx.fireChannelRead(msg)
}
override def write(ctx: ChannelHandlerContext, msg: Object, promise: ChannelPromise): Unit = {
ctx.channel.audit(
msg match {
case data: HttpRequest => Request(data)
case data: HttpResponse => Response(data)
case data: LastHttpContent => Content(data.content.readableBytes, true)
case data: HttpContent => Content(data.content.readableBytes)
case data: ByteBuf => WriteData(data.readableBytes)
})
ctx.write(msg, promise)
}
override def flush(ctx: ChannelHandlerContext): Unit = {
ctx.channel.audit(Flush())
ctx.flush()
}
override def close(ctx: ChannelHandlerContext, future: ChannelPromise): Unit = {
ctx.channel.audit(Close())
ctx.close(future)
}
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) = {
ctx.channel.audit(Error(cause))
ctx.fireExceptionCaught(cause)
}
}
object NeutrinoAuditHandler extends StrictLogging {
import Utilities._
implicit class NeutrinoAuditRequestSupport(val self: NeutrinoRequest) extends AnyVal {
/**
* Retrieve the activity-state, creating if necessary.
* @return valid state
*/
def state =
self.get(classOf[AuditState]) match {
case None =>
val state = AuditState()
self.set(classOf[AuditState], state)
state
case Some(state) =>
state
}
// Add the activity provided
def audit(channel: Channel, data: => AuditActivity) =
state.add(channel, data)
// Add the activity provided
//def audit(channel: Channel, data: Unit => AuditActivity) =
//state.add(channel, data())
// Add the activity provided
def audit(channel: Channel, datafx: PartialFunction[Channel, AuditActivity]) =
if (datafx.isDefinedAt(channel)) state.add(channel, datafx.apply(channel))
// Clear the audit state from the request provided
def clearstate() = self.clear(classOf[AuditState])
}
implicit class NeutrinoAuditSupport(val channel: Channel) extends AnyVal {
// Retrieve the associated state
def auditstate = AuditState.state(channel)
// Add the activity provided
def audit(data: => AuditActivity) = AuditState.audit(channel, data)
// Clear audit-state, if present
def clear() = AuditState.clear(channel)
}
class NeutrinoAuditLogger(request: NeutrinoRequest) extends ChannelFutureListener {
// Log requests that exceed the request's threshold
override def operationComplete(future: ChannelFuture): Unit = {
// Audit support; if our transaction has taken more than 5 seconds, dump the diagnostics here
val settings = request.session.service.settings.channel
val channel = future.channel
// Generate audit-records and throw debug
request.audit(channel, AuditActivity.Event(NeutrinoEvent.ResponseCompleted(request)))
settings.auditThreshold map { threshold =>
// Grab the request's data
val state = request.clear(classOf[AuditState])
state map { state =>
if (request.elapsed > threshold)
logger.warn("Audit state on long running transaction: {}\n{}", channel.toStringExt, state)
else
logger.debug("Audit state on transaction: {}\n{}", channel.toStringExt, state)
}
}
}
}
}
sealed abstract class AuditActivity {
val time = System.nanoTime
}
object AuditActivity {
// Supported types
case class Request(request: HttpRequest) extends AuditActivity
case class Response(response: HttpResponse) extends AuditActivity
case class Content(size: Int, last: Boolean=false) extends AuditActivity
case class ReadData(size: Int) extends AuditActivity
case class WriteData(size: Int) extends AuditActivity
case class Error(cause: Throwable) extends AuditActivity
case class Downstream() extends AuditActivity
case class DownstreamConnect(success: Boolean) extends AuditActivity
case class ChannelAssigned(channel: Channel) extends AuditActivity
case class ChannelException(channel: Channel, cause: Throwable) extends AuditActivity
case class Event(event: NeutrinoEvent) extends AuditActivity
case class UserEvent(event: AnyRef) extends AuditActivity
case class Detail(value: String) extends AuditActivity
case class Flush() extends AuditActivity
case class Close() extends AuditActivity
}
case class AuditState() {
import com.ebay.neutrino.handler.ops.AuditActivity._
import com.ebay.neutrino.util.AttributeSupport._
import scala.collection.JavaConversions.asScalaSet
private val activity = new util.LinkedList[(Channel, AuditActivity)]
// Add the activity provided
def add(data: (Channel, AuditActivity)) =
this.synchronized { activity.add(data) }
def headerStr(msg: HttpMessage) =
s"headers = [${msg.headers.names.mkString(",")}]"
override def toString = {
val builder = new StringBuilder("AuditState:\n")
val start = if (activity.isEmpty) 0L else activity.peekFirst._2.time
val iter = activity.iterator
// Iterate the activity
while (iter.hasNext) {
val (channel, item) = iter.next()
builder
.append(" ").append(String.format("%9s", ""+(item.time-start)/1000)).append(" micros:\t")
.append(
if (channel.service.isDefined) "Sx"+channel.id
else "0x"+channel.id
)
.append('\t')
builder.append(item match {
case Request (data: FullHttpRequest) => s"FullRequest (${data.uri}), ${headerStr(data)}"
case Request (data) => s"Request (${data.uri}), ${headerStr(data)}"
case Response(data: FullHttpResponse) => s"FullResponse, ${headerStr(data)}"
case Response(data) => s"Response, ${headerStr(data)}"
case Content(size, true) => s"LastContent($size)"
case Content(size, false) => s"Content($size)"
case Error(cause: Throwable) => s"Error($cause)"
case _ => item.toString
})
builder.append('\n')
}
builder.toString
}
}
/**
* Static helper methods.
*
* We define them here to ensure the anonymous lambda classes aren't constructed by
* the value classes.
*/
object AuditState {
import com.ebay.neutrino.handler.ops.NeutrinoAuditHandler._
import com.ebay.neutrino.util.AttributeSupport._
def request(channel: Channel): Option[NeutrinoRequest] =
channel.request orElse (channel.session flatMap (_.channel.request))
def state(channel: Channel): Option[AuditState] =
request(channel) map (_.state)
def audit(channel: Channel, data: => AuditActivity) =
request(channel) map (_.state.add((channel, data)))
// Clear audit-state, if present
def clear(channel: Channel) =
request(channel) map (_.clear(classOf[AuditState]))
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/ops/XForwardedHandler.scala
================================================
package com.ebay.neutrino.handler.ops
import java.net.{InetAddress, InetSocketAddress}
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter}
import io.netty.handler.codec.http.HttpRequest
import scala.util.Try
/**
* Creates or updates the X-Forwarded-For HTTP header.
*
* The general format of the header key: value is:
* X-Forwarded-For: client, proxy1, proxy2
*
* See X-Forwarded-For Wikipedia info.
*
* @author Dan Becker
*/
@Sharable
class XForwardedHandler extends ChannelInboundHandlerAdapter {
// Child classes should override to customize proxy-IP
def localIp(): Option[String] = XForwardedHandler.localHost
/**
* Resolve the client/requestor's IP address.
*
* Our default implementation returns the socket/channel originator's IP address.
* Child classes should override to customize client-IP resolution
*
* @param ctx
* @param request
* @return ip/host of client, or None if unable to resolve.
*/
def clientIp(ctx: ChannelHandlerContext, request: HttpRequest): Option[String] =
ctx.channel.remoteAddress match {
case inet: InetSocketAddress => Option(inet.getAddress.getHostAddress)
case _ => None
}
/**
* Propose the following changes for
* Equivalency with previous
(forwarded, clientIP, localHost) match {
case (Some(fwd), Some(ip), Some(lcl)) if fwd.contains(clientIP) => s"$fwd,$lcl"
case (Some(fwd), Some(ip), Some(lcl)) => s"$ip,$fwd,$lcl"
case (Some(fwd), Some(ip), None) => s"$ip,$fwd"
case (Some(fwd), None, Some(lcl)) => s"$fwd,$lcl"
case (None, Some(ip), Some(lcl)) => s"$ip,$lcl"
case (None, None, Some(lcl)) => s"$lcl"
case (Some(fwd), _, _) => s"$fwd"
case _ => null // ""
}
*/
override def channelRead(ctx: ChannelHandlerContext, msg: AnyRef): Unit = {
msg match {
case request: HttpRequest =>
val client = clientIp(ctx, request)
val forwarded = Option(request.headers.get(XForwardedHandler.X_FORWARDED_FOR))
val headers = Seq(client, forwarded, localIp).flatten.distinct.mkString(",")
// If valid (ie: non-empty), set the header
if (headers.nonEmpty) request.headers.set(XForwardedHandler.X_FORWARDED_FOR, headers)
case _ =>
}
ctx.fireChannelRead(msg)
}
}
object XForwardedHandler {
// TODO externalize 'standard' headers
val X_FORWARDED_FOR = "X-Forwarded-For"
lazy val localHost = Try(InetAddress.getLocalHost) map (_.getHostAddress) toOption
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/package-info.java
================================================
/**
* Created by cbrawn on 1/16/15.
*/
package com.ebay.neutrino.handler;
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/package.scala
================================================
package com.ebay.neutrino
// Inbound Handler
// > take URI, look up an endpoint with it
// > publish EndpointSelection/EndpointId, remove pipeline (replace?)
// Inbound Handler
// > Pipeline...
// Inbound Handler
// > Start connection pair; relay A <=> B
package handler {}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/pipeline/HttpInboundPipeline.java
================================================
package com.ebay.neutrino.handler.pipeline;
import com.ebay.neutrino.NeutrinoRequest;
import io.netty.handler.codec.http.HttpResponse;
/**
* Very very simplified 'generic' pipeline interface for processing HTTP request pipeline
* functionality.
*
* Note that the conditional-check is removed (ie: shouldExecute(request)). Instead, the
* pipeline should simply return continue.
*
* Created by cbrawn on 9/24/14.
*
* TODO - rename something (much) better?
*/
public interface HttpInboundPipeline {
/**
* Response status:
* SKIPPED: pipeline was not executed (did not meet conditions); continue pipeline execution with request
* CONTINUE: pipeline was executed successfully; continue pipeline execution with request
* COMPLETE: pipeline was executed successfully; skip remainder of pipeline and return request
* REJECT: pipeline stage failed; a failure-response is optionally provided.
*/
public enum Status { SKIPPED, CONTINUE, COMPLETE, REJECT }
/**
* Execution result type from the pipeline execution.
* Status will always be populated, as one of Status.enum
* Response is optional and will only be provided on REJECT status
*/
public interface Result {
public Status status();
public HttpResponse response();
}
/**
* Execute the pipeline functionality, and return a conditional response.
*
* @param connection
* @param request
*/
public Result execute(final NeutrinoRequest request);
}
================================================
FILE: src/main/scala/com/ebay/neutrino/handler/pipeline/HttpPipelineUtils.java
================================================
package com.ebay.neutrino.handler.pipeline;
import com.ebay.neutrino.handler.pipeline.HttpInboundPipeline.Result;
import com.ebay.neutrino.handler.pipeline.HttpInboundPipeline.Status;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
/**
* Helper class for interacting with the HttpInboundPipeline interfaces.
*/
public abstract class HttpPipelineUtils {
// Prevent instantiation
private HttpPipelineUtils() {}
/**
* Convert the string-content provided to a byte-buffer
* TODO - should these be hard-coded to prevent heap-allocation issues
*/
public static final ByteBuf toByteBuf(final String content) {
// Allocate some memory for this
// final ByteBuf buffer = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
final ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(content.getBytes(CharsetUtil.UTF_8));
return byteBuf;
}
/**
* Singleton implementations of the common results.
*/
public static final Result RESULT_SKIPPED = new Result() {
public Status status() { return Status.SKIPPED; }
public HttpResponse response() { return null; }
};
public static final Result RESULT_CONTINUE = new Result() {
public Status status() { return Status.CONTINUE; }
public HttpResponse response() { return null; }
};
public static final Result RESULT_COMPLETE = new Result() {
public Status status() { return Status.COMPLETE; }
public HttpResponse response() { return null; }
};
/**
* Create a failed-result with the provided response details.
*
* @param status HTTP status
* @param contentType one of the legal response header CONTENT_TYPE values.
* @param content error message to user. It is important not to divulge stack trace or other internal details.
* @return
*/
public static Result reject(final HttpResponseStatus status, final String contentType, final String content) {
return new Result() {
public Status status() { return Status.REJECT; }
// I think we should rely on the ops pipelines to 'enforce' the correct default outbound headers
public HttpResponse response() {
final HttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, toByteBuf(content));
resp.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
return resp;
}
};
}
/**
* Create a failed-result with the provided response details. Courtesy shortcut method.
*
* @param contentType one of the legal response header CONTENT_TYPE values.
* @param content error message to user. It is important not to divulge stack trace or other internal details.
* @return
*/
public static Result reject(final String contentType, final String content) {
return reject( HttpResponseStatus.OK, contentType, content );
}
/**
* Create a failed-result with the provided response details. Courtesy shortcut method.
*
* @param status HTTP status
* @return
*/
public static Result reject(final HttpResponseStatus status) {
return reject(status, "text/plain; charset=UTF-8", "Reject: " + status.toString() + "\r\n");
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/health/HealthProvider.scala
================================================
package com.ebay.neutrino.health
import com.ebay.neutrino.config.VirtualServer
/**
* Marker interface for processing health information.
*/
trait HealthProvider {
/**
* Retrieve the health of the specific server.
*/
def getHealth(server: VirtualServer)
/**
* Register a health notification callback for the server provided.
*/
def registerListener(server: VirtualServer, listener: HealthListener)
/**
* Remove a health notification callback for the server provided.
*/
def removeListener(server: VirtualServer)
}
/**
* Marker interface for a health listener.
*/
trait HealthListener {
// TODO
}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/HealthMonitor.scala
================================================
package com.ebay.neutrino.metrics
/**
* We support the following dynamic instantiations of the trait implementations:
* newInstance()
* newInstance(c: Config)
*/
trait HealthMonitor {}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/Instrumented.scala
================================================
package com.ebay.neutrino.metrics
import nl.grons.metrics.scala._
import scala.collection.mutable
/**
* This mixin trait can used for creating a class which is instrumented with metrics.
*
* Reimplented our own base-class instead of extending InstrumentedBuilder because
* we wanted to support a subclass implementation of the MetricsBuilder class
* > extends nl.grons.metrics.scala.InstrumentedBuilder
*
*/
trait Instrumented extends BaseBuilder {
/**
* The MetricBuilder that can be used for creating timers, counters, etc.
*/
lazy val metrics =
new MetricBuilder(metricBaseName, Metrics.metricRegistry) with InstrumentedBuilderSupport
}
trait InstrumentedBuilderSupport { self: MetricBuilder =>
/** Helper; build a metric-name out of the components provided */
def metricName(names: String*) = baseName.append(names:_*)
/**
* Registers a new gauge metric.
*/
def gauge[A](metricName: MetricName, scope: String*)(f: => A): Gauge[A] =
new Gauge[A](registry.register(metricName.append(scope:_*).name, new com.codahale.metrics.Gauge[A] {
def getValue: A = f })
)
def gauge[A](clazz: Class[_], name: String*)(f: => A): Gauge[A] =
gauge(MetricName(clazz, name:_*).name)(f)
/**
* Registers a new timer metric.
*/
def timer(metricName: MetricName, scope: String*): Timer =
new Timer(registry.timer(metricName.append(scope:_*).name))
def timer(clazz: Class[_], name: String*): Timer =
timer(MetricName(clazz, name:_*).name)
/**
* Registers a new meter metric.
*/
def meter(metricName: MetricName, scope: String*): Meter =
new Meter(registry.meter(metricName.append(scope:_*).name))
def meter(clazz: Class[_], name: String*): Meter =
meter(MetricName(clazz, name:_*).name)
/**
* Registers a new histogram metric.
*/
def histogram(metricName: MetricName, scope: String*): Histogram =
new Histogram(registry.histogram(metricName.append(scope:_*).name))
def histogram(clazz: Class[_], name: String*): Histogram =
histogram(MetricName(clazz, name:_*).name)
/**
* Registers a new counter metric.
*/
def counter(name: String, scope: String*): Counter =
counter(metricName(name +: scope:_*))
def counter(metricName: MetricName, scope: String*): Counter =
new Counter(registry.counter(metricName.append(scope:_*).name))
def counter(clazz: Class[_], name: String*): Counter =
counter(MetricName(clazz, name:_*).name)
def counterset(name: String, scope: String*): CounterSet =
new CounterSet(metricName(name +: scope:_*))
/**
* Hack - this works around writing a duplicate gauge (ie: with unit testing)
* TODO replace this with a non-static MetricsRegistry
*/
def safegauge[A](metricName: MetricName, scope: String*)(f: => A): Gauge[A] = {
val metricname = metricName.append(scope: _*).name
registry.remove(metricname)
new Gauge[A](registry.register(metricname, new com.codahale.metrics.Gauge[A] { def getValue: A = f }))
}
def safegauge[A](name: String, scope: String = null)(f: => A): Gauge[A] =
safegauge(baseName.append(name), scope)(f)
class CounterSet(metricName: MetricName) {
val set = mutable.Map[String, Counter]()
def apply(name: String): Counter = set.getOrElseUpdate(name, counter(name))
def apply(clazz: Class[_]): Counter = apply(clazz.getSimpleName)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/Metrics.scala
================================================
package com.ebay.neutrino.metrics
import com.ebay.neutrino.channel.NeutrinoSession
import com.ebay.neutrino.handler.NeutrinoRequestHandler
import com.ebay.neutrino.{NeutrinoCore, NeutrinoNode}
import io.netty.channel.Channel
import nl.grons.metrics.scala._
/**
* Yammer metrics implementation based on metrics-scala (https://github.com/erikvanoosten/metrics-scala)
* Static metrics (mapped directly to their key equivalent).
*
* @author hzariv on 8/29/2014.
* @author cbrawn on 2/13/2015; static metrics direct link
*/
object Metrics extends Instrumented {
import com.ebay.neutrino.config.{CompletionStatus => Status}
import com.ebay.neutrino.metrics.{MetricsKey => Key}
/** The application wide metrics registry. */
val metricRegistry = new com.codahale.metrics.MetricRegistry()
// Requests
val RequestsOpen = metrics.counter(Key.Requests, "open")
val RequestsTotal = metrics.meter (Key.Requests, "total")
val RequestsCompleted = metrics.timer (Key.Requests, "duration")
// Store bucketed response detail as well; should we have both total and each? or just each?
val RequestsCompletedType = Map[Status, Timer] (
Status.Incomplete -> metrics.timer(Key.RequestsCompleted, "none"),
Status.Status2xx -> metrics.timer(Key.RequestsCompleted, "2xx"),
Status.Status4xx -> metrics.timer(Key.RequestsCompleted, "4xx"),
Status.Status5xx -> metrics.timer(Key.RequestsCompleted, "5xx"),
Status.Other -> metrics.timer(Key.RequestsCompleted, "other"))
// Sessions
val SessionActive = metrics.counter(Key.Session, "open")
val SessionTotal = metrics.meter (Key.Session, "total")
val SessionDuration = metrics.timer (Key.Session, "duration")
// Channels: Upstream/Browser
val UpstreamOpen = metrics.counter(Key.UpstreamChannel, "open")
val UpstreamTotal = metrics.meter (Key.UpstreamChannel, "total")
val UpstreamCompleted = metrics.timer (Key.UpstreamChannel, "complete")
val UpstreamBytesRead = metrics.counter(Key.UpstreamChannel, "bytes.read")
val UpstreamBytesWrite = metrics.counter(Key.UpstreamChannel, "bytes.write")
val UpstreamPacketsRead = metrics.meter (Key.UpstreamChannel, "packets.read")
val UpstreamPacketsWrite = metrics.meter (Key.UpstreamChannel, "packets.write")
// Channels: Downstream/Server
val DownstreamOpen = metrics.counter(Key.DownstreamChannel, "open")
val DownstreamTotal = metrics.meter (Key.DownstreamChannel, "total")
val DownstreamCompleted = metrics.timer (Key.DownstreamChannel, "complete")
val DownstreamBytesRead = metrics.counter(Key.DownstreamChannel, "bytes.read")
val DownstreamBytesWrite = metrics.counter(Key.DownstreamChannel, "bytes.write")
val DownstreamPacketsRead = metrics.meter (Key.DownstreamChannel, "packets.read")
val DownstreamPacketsWrite = metrics.meter (Key.DownstreamChannel, "packets.write")
// Channels: Connection-pooling
val PooledCreated = metrics.meter (Key.PoolAllocated, "created")
val PooledRecreated = metrics.meter (Key.PoolAllocated, "recreated")
val PooledAssigned = metrics.meter (Key.PoolAllocated, "assigned")
val PooledRetained = metrics.counter(Key.PoolReleased, "retained")
val PooledFailed = metrics.counter(Key.PoolReleased, "failed")
val PooledExpired = metrics.counter(Key.PoolReleased, "expired")
val PooledClosed = metrics.counter(Key.PoolReleased, "closed")
}
/**
* Metric-key constants. These are the common metrics for core-system.
*
* Consumers shouldn't need to refer to these directly; they should be able to use
* the static bindings in Metrics.
*
* @author cbrawn
*/
object MetricsKey {
// Core
val BossThreads = MetricName(classOf[NeutrinoCore], "threads.boss")
val IOThreads = MetricName(classOf[NeutrinoCore], "threads.io")
// Requests
val Requests = MetricName(classOf[NeutrinoRequestHandler], "requests")
val RequestsCompleted = Requests.append("completed")
// Sesssions
val Session = MetricName(classOf[NeutrinoSession])
// Channels
val Channel = MetricName(classOf[Channel])
val UpstreamChannel = Channel.append("upstream")
val DownstreamChannel = Channel.append("downstream")
// Channels: Connection-pooling
val PoolAllocated = MetricName(classOf[NeutrinoNode], "allocate")
val PoolReleased = MetricName(classOf[NeutrinoNode], "release")
}
object HealthCheck {
/** The application wide health check registry. */
val healthChecksRegistry = new com.codahale.metrics.health.HealthCheckRegistry()
}
/**
* This mixin trait can used for creating a class which creates health checks.
*/
trait Checked extends nl.grons.metrics.scala.CheckedBuilder {
val healthCheckRegistry = HealthCheck.healthChecksRegistry
}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/MetricsAnnotationRegistry.scala
================================================
package com.ebay.neutrino.handler
import java.lang.annotation.Annotation
import scala.collection.mutable
import com.codahale.metrics.annotation.{Gauge, Metered, Timed}
import io.netty.channel._
class MetricsAnnotationRegistry {
// Constants
val Inbound = classOf[ChannelInboundHandler]
val Outbound = classOf[ChannelOutboundHandler]
val Methods = Inbound.getMethods ++ Outbound.getMethods
// Extract an interface-supported method matching the method-name, from the class provided.
case class AnnotatedPair[T](clazz: Class[T], methodName: String) {
require(Inbound.isAssignableFrom(clazz) || Outbound.isAssignableFrom(clazz), s"Class $clazz is not a handler interface")
// Check interface for method-signature and find class's matching method
// Extract all available annotations from the class-method provided.
lazy val annotations = Methods find(_.getName == methodName) map (m =>
clazz.getMethod(methodName, m.getParameterTypes:_*)) match {
case Some(method) => method.getAnnotations
case None => throw new IllegalArgumentException(s"Method $methodName not found in $clazz")
}
}
val map = mutable.Map[AnnotatedPair[_], Array[Annotation]]()
def getAnnotations(clazz: Class[_], methodName: String) = {
val pair = AnnotatedPair(clazz, methodName)
map getOrElseUpdate (pair, pair.annotations flatMap {
case timed: Timed => Option(timed)
case meter: Metered => Option(meter)
case gauge: Gauge => Option(gauge)
case _ => None // exception
})
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/MetricsLifecycle.scala
================================================
package com.ebay.neutrino.metrics
import java.net.{InetAddress, InetSocketAddress}
import java.util.concurrent.TimeUnit
import com.codahale.metrics.ConsoleReporter
import com.codahale.metrics.graphite.{Graphite, GraphiteReporter}
import com.ebay.neutrino.config.Configuration
import com.ebay.neutrino.{NeutrinoCore, NeutrinoLifecycle}
import com.typesafe.config.Config
import com.typesafe.scalalogging.slf4j.StrictLogging
/**
* Simple initializer for configuring console-support for metrics.
*
*/
class MetricsLifecycle(settings: Seq[MetricsSettings]) extends NeutrinoLifecycle with StrictLogging {
val registry = Metrics.metricRegistry
// TODO resolve this properly
val hostname = InetAddress.getLocalHost.getHostName
// Our configured reporters
val reporters =
settings flatMap {
case ConsoleMetrics(period) =>
val reporter = ConsoleReporter.forRegistry(registry)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build()
Option(reporter -> period)
case GraphiteMetrics(period, host, port) =>
val graphite = new Graphite(new InetSocketAddress(host, port))
val reporter = GraphiteReporter.forRegistry(registry)
.prefixedWith(hostname)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
//.filter(MetricFilter.ALL)
.build(graphite)
Option(reporter -> period)
case _ => None
}
logger.info("Setting our hostname to {} for metrics reporting.", hostname)
/**
* Overloaded Constructor; create a MetricsLifecycle with a Config object.
*
* This will be used by the plugin reflection (for dynamic class creation) to inject
* a runtime configuration into class creation.
*/
def this(config: Config) = this(MetricsLifecycle.settings(config))
// Start any available reporters
def start() =
reporters foreach {
case (reporter, period) => reporter.start(period.toSeconds, TimeUnit.SECONDS)
}
// Stop all reporters (if appropriate)
def shutdown() =
reporters foreach {
case (reporter, period) => reporter.stop()
}
// Neutrino Lifecycle support
def start(balancer: NeutrinoCore) = start()
def shutdown(balancer: NeutrinoCore) = shutdown()
}
object MetricsLifecycle {
import Configuration._
def settings(config: Config) =
config getOptionalConfigList "metrics" map (MetricsSettings(_))
}
================================================
FILE: src/main/scala/com/ebay/neutrino/metrics/MetricsSettings.scala
================================================
package com.ebay.neutrino.metrics
import java.util
import com.ebay.neutrino.config.HasConfiguration
import com.typesafe.config.Config
import scala.concurrent.duration.FiniteDuration
sealed trait MetricsSettings
case class ConsoleMetrics(publishPeriod: FiniteDuration) extends MetricsSettings
case class GraphiteMetrics(publishPeriod: FiniteDuration, host: String, port: Int) extends MetricsSettings
case class UnknownMetrics(config: Config) extends MetricsSettings with HasConfiguration
object MetricsSettings {
import com.ebay.neutrino.config.Configuration._
import scala.collection.JavaConversions._
// Extract a list of appropriate metric-settings
def apply(list: util.List[_ <: Config]): List[MetricsSettings] =
list.toList map (MetricsSettings(_))
def apply(list: List[_ <: Config]): List[MetricsSettings] =
list.toList map (MetricsSettings(_))
// Extract the appropriate metric-settings based on the type specified in the config
def apply(c: Config): MetricsSettings =
c getString "type" toLowerCase match {
case "console" => console(c)
case "graphite" => graphite(c)
case _ => unknown(c)
}
// Extract settings for publishing console metrics
def console(c: Config): MetricsSettings = ConsoleMetrics(
c getDuration "publish-period"
)
// Extract settings for publishing graphite metrics
def graphite(c: Config): MetricsSettings = GraphiteMetrics(
c getDuration "publish-period",
c getString "host",
c getOptionalInt "port" getOrElse 2003
)
// Extract settings for unknown metrics
def unknown(c: Config): MetricsSettings = UnknownMetrics(c)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/Attributes.scala
================================================
package com.ebay.neutrino.util
import com.ebay.neutrino._
import com.ebay.neutrino.balancer.Balancer
import com.ebay.neutrino.channel.{NeutrinoPipelineChannel, NeutrinoService, NeutrinoSession}
import com.ebay.neutrino.handler.ops.{ChannelStatistics, ChannelTimeout}
import io.netty.channel._
import io.netty.util.{Attribute, AttributeKey, AttributeMap}
/**
* SIP 15 (Static helper) Support for managing known attribute-keys on the
* channel (or indirectly via the context).
*
* This would be easier to do with view-classes, but the compiler is considering them as field annotation:
* implicit class AttributeKeySupport[C <% Channel](val self: C)
*
* @param self
*/
object AttributeSupport {
val BalancerKey = AttributeKey.valueOf[NeutrinoCore]("balancer")
val RequestKey = AttributeKey.valueOf[NeutrinoRequest]("request")
val SessionKey = AttributeKey.valueOf[NeutrinoSession]("session")
val ServiceKey = AttributeKey.valueOf[NeutrinoService]("service")
val TimeoutKey = AttributeKey.valueOf[ChannelTimeout]("timeouts")
val StatisticsKey = AttributeKey.valueOf[ChannelStatistics]("statistics")
/**
* Generalized attribute-map helper functions, for AttributeMap subtypes (Channel, Context)
*
* These must be chained on the underlying AttributeMap class (rather than inherited
* directly in the caller value-class, as that forces an object instantiation.
*
* @param self
*/
implicit class AttributeMapSupport(val self: AttributeMap) extends AnyVal {
// Resolve attribute from the underlying container
def attr[T](key: AttributeKey[T]): Attribute[T] = self.attr(key)
// Generic retrieval method
def get[T](key: AttributeKey[T]): Option[T] =
Option(attr(key).get())
// Generic retrieval method, with default
def getOrElse[T](key: AttributeKey[T])(default: => T): T =
get(key) getOrElse (set(key, Option(default)).get)
// Generic setter/clear method
def set[T](key: AttributeKey[T], value: Option[T]): Option[T] = value match {
case Some(set) => attr(key).set(set); value
case None => attr(key).remove(); value
}
}
/**
* Attribute-based support methods.
*
*/
implicit class AttributeValueSupport(val self: AttributeMap) extends AnyVal {
// We provide a structural definition for this so our static 'statistics' method
// doesn't need to create an anonymous method
private def createStatistics = new ChannelStatistics()
// The balancer-connection (request/response context)
def request = self.get(RequestKey)
def request_=(value: NeutrinoRequest) = self.set(RequestKey, Option(value))
def request_=(value: Option[NeutrinoRequest]) = self.set(RequestKey, value)
// Neutrino-Session (user-pipeline) getter and setter
def session = self.get(SessionKey)
def session_=(value: NeutrinoSession) = self.set(SessionKey, Option(value))
def session_=(value: Option[NeutrinoSession]) = self.set(SessionKey, value)
// Neutrino-Service (wrapper around core) getter and setter
def service = self.get(ServiceKey)
def service_=(value: NeutrinoService) = self.set(ServiceKey, Option(value))
def service_=(value: Option[NeutrinoService]) = self.set(ServiceKey, value)
// Channel timeout support
def timeout = self.get(TimeoutKey)
def timeout_=(value: ChannelTimeout) = self.set(TimeoutKey, Option(value))
def timeout_=(value: Option[ChannelTimeout]) = self.set(TimeoutKey, value)
// IO-Channel Statistics lazy-getter
def statistics = self.get(StatisticsKey) match {
case Some(stats) => stats
case None => self.set(StatisticsKey, Option(new ChannelStatistics())).get
}
}
/**
* Attribute support for NeutrinoRequest (class-key mapped) attributes.
* @param self
*/
implicit class RequestAttributeSupport(val self: NeutrinoRequest) extends AnyVal {
def balancer = self.get(classOf[Balancer])
def balancer_=(value: Balancer) = self.set(classOf[Balancer], value)
def balancer_=(value: Option[Balancer]) = self.set(classOf[Balancer], value getOrElse null)
def node = self.get(classOf[NeutrinoNode])
def node_=(value: NeutrinoNode) = self.set(classOf[NeutrinoNode], value)
def node_=(value: Option[NeutrinoNode]) = self.set(classOf[NeutrinoNode], value getOrElse null)
// Neutrino Pipeline only: downstream context
def downstream = self.get(classOf[Channel])
def downstream_=(value: Channel) = self.set(classOf[Channel], value)
def downstream_=(value: Option[Channel]) = self.set(classOf[Channel], value getOrElse null)
}
}
/**
* Support for dynamic class-based attributes, which can be mixed into other classes for
* 'context' support, allowing dynamic class-specific storage and resolution.
*
* Note that we don't really support null (as a null value gets mapped to None)
*/
class AttributeClassMap {
val attributes = new java.util.HashMap[Class[_], AnyRef]()
// Optionally get the value belonging to
def get[T <: AnyRef](clazz: Class[T]): Option[T] =
Option(attributes.get(clazz).asInstanceOf[T])
// Get the value belonging to , defaulting to the provided
def get[T <: AnyRef](clazz: Class[T], default: => T): T =
attributes.get(clazz) match {
case null => val value = default; attributes.put(clazz, value); value
case value => value.asInstanceOf[T]
}
// Set/Clear the value belonging to
def set[T <: AnyRef](clazz: Class[T], value: T): Unit =
value match {
case null => attributes.remove(clazz)
case value => attributes.put(clazz, value)
}
// Explicitly clear the value belonging to
def clear[T <: AnyRef](clazz: Class[T]): Option[T] =
Option(attributes.remove(clazz)).asInstanceOf[Option[T]]
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/HttpUtils.scala
================================================
package com.ebay.neutrino.util
import java.net.URI
import com.ebay.neutrino.config.Host
import com.typesafe.scalalogging.slf4j.StrictLogging
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.HttpHeaders.{Names, Values}
import io.netty.handler.codec.http._
import io.netty.util.CharsetUtil
/**
* A delegate implementation of the ChannelPipeline to assist with Neutrino pipeline
* creation.
*
*/
object HttpRequestUtils {
import com.ebay.neutrino.util.AttributeSupport._
import scala.collection.JavaConversions._
// Attempt to assign the connection's pool based on the pool-name provided
def setPool(ctx: ChannelHandlerContext, poolname: String): Boolean =
// Try and grab the connection out of the context
ctx.request map (_.pool.set(poolname).isDefined) getOrElse false
// Copy any headers
def copy(request: HttpRequest) = {
val copy = new DefaultHttpRequest(request.protocolVersion(), request.method(), request.uri())
request.headers() foreach { hdr => copy.headers().add(hdr.getKey, hdr.getValue) }
copy
}
/**
* Attempt to construct an absolute URI from the request, if possible, falling
* back on relative URI
*
* Will take:
* 1) HttpRequest URI, if absolute
* 2) HttpRequest URI + Host header, if available
* 3) HttpRequest relative URI
*/
def URI(request: HttpRequest): URI = {
val scheme = "http" // TODO how to extract?
val hosthdr = Option(request.headers.get(HttpHeaderNames.HOST)) map (Host(_))
(java.net.URI.create(request.uri), request.host(false)) match {
case (uri, _) if uri.isAbsolute => uri
case (uri, None) => uri
case (uri, Some(Host(host, 0))) => new URI(scheme, uri.getAuthority, host, uri.getPort, uri.getPath, uri.getQuery, uri.getFragment)
case (uri, Some(Host(host, port))) => new URI(scheme, uri.getAuthority, host, port, uri.getPath, uri.getQuery, uri.getFragment)
}
}
// Pimp for Netty HttpRequest.
implicit class HttpRequestSupport(val self: HttpRequest) extends AnyVal {
/**
* Attempt to construct an absolute URI from the request, if possible, falling
* back on relative URI
*/
def URI(): URI = HttpRequestUtils.URI(self)
def host(useUri: Boolean=true): Option[Host] = useUri match {
case true => host(URI)
case false => Option(self.headers.get(HttpHeaderNames.HOST)) map (Host(_))
}
def host(uri: URI) = {
Option(uri.getHost) map {
case host if uri.getPort <= 0 => Host(host.toLowerCase)
case host => Host(host.toLowerCase, uri.getPort)
}
}
}
}
/**
* A delegate implementation of the ChannelPipeline to assist with Neutrino pipeline
* creation.
*
*/
object HttpResponseUtils {
import io.netty.handler.codec.http.HttpHeaderNames._
import io.netty.handler.codec.http.HttpVersion._
def error(status: HttpResponseStatus) = {
// Allocate some memory for this
val buffer = Unpooled.copiedBuffer(s"Failure: $status\r\n", CharsetUtil.UTF_8)
// Package a response
val response = new DefaultFullHttpResponse(HTTP_1_1, status, buffer)
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8")
response
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/Response.scala
================================================
package com.ebay.neutrino.util
import io.netty.buffer.{ByteBuf, Unpooled}
import io.netty.handler.codec.http._
import scala.xml.Elem
/**
* Convenience methods for generating HTTP response codes.
* TODO - build a simple cache around this...
*/
class ResponseGenerator {
import com.ebay.neutrino.util.ResponseUtil._
import scalatags.Text.all._
// Cached messages
val messages: Map[HttpResponseStatus, String] =
Map(
HttpResponseStatus.BAD_GATEWAY ->
"""Our apologies for the temporary inconvenience. The requested URL generated 503 "Service Unavailable" error.
|This could be due to no available downstream servers, or overloading or maintenance of the downstream server.""".stripMargin,
HttpResponseStatus.GATEWAY_TIMEOUT ->
"""The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.."""
)
val DefaultMessage = messages(HttpResponseStatus.BAD_GATEWAY)
/**
* Generate a message, defaulting to the HttpStatus's stock message.
*/
def generate(status: HttpResponseStatus, detail: String): FullHttpResponse =
generate(status, messages get(status) getOrElse DefaultMessage, detail)
/**
* Generate a message, with the message and detail provided.
*/
def generate(status: HttpResponseStatus, message: String, detail: String): FullHttpResponse =
ResponseUtil.generate(status,
html(
head(title := s"Error ${status.code} ${status.reasonPhrase}"),
body(
h1(s"${status.code} ${status.reasonPhrase}"),
s"The server reported: $message",
h2("Additional Details:"),
detail
)
))
}
/**
* Convenience methods for generating an HTTP response.
*
*/
object ResponseUtil {
import scalatags.Text
// Constants
val TextPlain = "text/plain; charset=UTF-8"
val TextHtml = "text/html; charset=UTF-8"
// Helper methods
private def formatter() = new scala.xml.PrettyPrinter(80, 4)
// Generate a response from the params provided.
def generate(status: HttpResponseStatus, content: Text.TypedTag[String]): FullHttpResponse =
generate(status, scala.xml.XML.loadString(content.toString()))
def generate(status: HttpResponseStatus, content: Elem): FullHttpResponse =
generate(status, formatter().format(content)+"\r\n")
def generate(status: HttpResponseStatus, content: String): FullHttpResponse =
generate(status, Unpooled.wrappedBuffer(content.getBytes), TextHtml)
def generate(status: HttpResponseStatus, buffer: ByteBuf): FullHttpResponse =
generate(status, buffer, TextPlain)
def generate(status: HttpResponseStatus, buffer: ByteBuf, contentType: String): FullHttpResponse =
generate(status, buffer, contentType, HttpVersion.HTTP_1_1)
def generate(status: HttpResponseStatus, buffer: ByteBuf, contentType: String, version: HttpVersion): FullHttpResponse = {
val response = new DefaultFullHttpResponse(version, status, buffer)
response.headers.set(HttpHeaderNames.CONTENT_TYPE, contentType)
response.headers.set(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes())
response
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/ServerContext.scala
================================================
package com.ebay.neutrino.util
import java.net.{UnknownHostException, InetAddress}
import com.typesafe.scalalogging.slf4j.StrictLogging
/**
* Created by blpaul on 12/1/2015.
*/
object ServerContext extends StrictLogging {
val fullHostName = {
var hostName : String = "Not Available"
try {
hostName = InetAddress.getLocalHost.getHostName
}
catch {
case ex : UnknownHostException =>
logger.warn("Unable to get the hostname")
}
hostName
}
val canonicalHostName = {
var hostName : String = "Not Available"
try {
hostName = InetAddress.getLocalHost.getCanonicalHostName
}
catch {
case ex : UnknownHostException =>
logger.warn("Unable to get the hostname")
}
hostName
}
val hostAddress = {
var hostAddress : String = "Not Available"
try {
hostAddress = InetAddress.getLocalHost.getHostAddress
}
catch {
case ex : UnknownHostException =>
logger.warn("Unable to get the hostaddress")
}
hostAddress
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/State.scala
================================================
package com.ebay.neutrino.util
import scala.collection.mutable
// Not exactly algorithmically tuned; just needed a quick hack for splitting sets
case class DifferentialState[T](added: Seq[T], removed: Seq[T], updated: Seq[T])
object DifferentialState {
/**
* Create a new diff-state between the before/after.
* NOTE - does not preserve ordering
*/
def apply[T](pre: Iterable[T], post: Iterable[T]): DifferentialState[T] = {
// Build a differential count accumulator
val occ = new mutable.HashMap[T, Int] { override def default(k: T) = 0 }
for (y <- pre) occ(y) -= 1
for (y <- post) occ(y) += 1
val added = Seq.empty.genericBuilder[T]
val removed = Seq.empty.genericBuilder[T]
val same = Seq.empty.genericBuilder[T]
occ foreach {
case (k, -1) => removed += k
case (k, 0) => same += k
case (k, 1) => added += k
}
DifferentialState(added.result, removed.result, same.result)
}
}
trait DifferentialStateSupport[K,V] {
// Versioning support; is updated on each update() call
private var _version = 0
// Internal state management
protected var state = Seq.empty[V]
def update(values: V*) =
this.synchronized {
// Map the key-values and calculate the diffs
val before = state.foldLeft(Map[K,V]()) { (map, v) => map + (key(v) -> v) }
val after = values.foldLeft(Map[K,V]()) { (map, v) => map + (key(v) -> v) }
val diff = DifferentialState(before.keys, after.keys)
// Extract the associated values
val added = diff.added map (k => after(k))
val removed = diff.removed map (k => before(k))
val updated = diff.updated flatMap (k => if (before(k) != after(k)) Option((before(k), after(k))) else None)
// Update the cached-values
state = values
// If changes have been made, update
if (added.nonEmpty || removed.nonEmpty || updated.nonEmpty) {
removed foreach (v => removeState(v))
updated foreach (v => updateState(v._1, v._2))
added foreach (v => addState(v))
// Update the version
_version += 1
}
}
// Required subclass methods.
// These are protected and should not be called from outside this class.
protected def key(v: V): K
protected def addState(added: V): Unit
protected def removeState(remove: V): Unit
protected def updateState(pre: V, post: V): Unit
// Export version
def version = _version
}
================================================
FILE: src/main/scala/com/ebay/neutrino/util/Utilities.scala
================================================
package com.ebay.neutrino.util
import java.net.URI
import java.util.concurrent.atomic.AtomicLong
import io.netty.buffer.{ByteBuf, Unpooled}
import io.netty.channel.{Channel, _}
import io.netty.handler.codec.http.{DefaultFullHttpResponse, HttpResponseStatus}
import io.netty.util.CharsetUtil
import io.netty.util.concurrent.{Future => NettyFuture, FutureListener}
import scala.concurrent.{Await, CancellationException, Future, Promise}
import scala.language.implicitConversions
object Random {
import scala.concurrent.duration._
import scala.util.{Random => ScalaRandom}
// Return a randomized number of the duration provided, in milliseconds
def nextMillis(duration: Duration) =
(ScalaRandom.nextLong() % duration.toNanos) nanos
// Return a random number uniformly distributed over the range provided
// Note - can only do in millisecond range
def nextUniform(min: Duration, max: Duration): Duration =
min + nextMillis(max - min)
// Scale our result to the correct sigma
def nextGaussian(mean: Duration, sigma: Duration): Duration =
mean + (sigma * ScalaRandom.nextGaussian())
}
object Utilities {
import io.netty.util.concurrent.{Future => NettyFuture}
implicit class ChannelContextSupport(val self: ChannelHandlerContext) extends AnyVal {
import io.netty.handler.codec.http.HttpHeaderNames._
import io.netty.handler.codec.http.HttpVersion._
def sendError(status: HttpResponseStatus): ChannelFuture =
sendError(status, s"Failure: $status")
def sendError(status: HttpResponseStatus, message: String): ChannelFuture = {
val buffer = Unpooled.copiedBuffer(message+"\r\n", CharsetUtil.UTF_8)
val response = new DefaultFullHttpResponse(HTTP_1_1, status, buffer)
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8")
// Close the connection as soon as the error message is sent.
self.writeAndFlush(response)
}
def closeListener = new ChannelFutureListener() {
override def operationComplete(future: ChannelFuture): Unit =
// was able to flush out data, start to read the next chunk
if (future.isSuccess()) {
self.channel().read()
} else {
future.channel().close()
}
}
def writeAndFlush(msg: AnyRef, close: Boolean) =
if (close) self.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE)
else self.writeAndFlush(msg)
def writeAndClose(msg: AnyRef) =
self.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE)
// Closes the specified channel after all queued write requests are flushed.
def closeOnFlush = self.channel.closeOnFlush
}
implicit class ChannelSupport(val self: Channel) extends AnyVal {
// Connection-state listener
def connectListener = new ChannelFutureListener() {
override def operationComplete(future: ChannelFuture): Unit =
// connection complete start to read first data
if (future.isSuccess()) self.read()
// Close the connection if the connection attempt has failed.
else self.close()
}
/**
* Closes the specified channel after all queued write requests are flushed.
*/
def closeOnFlush = if (self.isActive())
self.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE)
/**
* Extract context from the channel (if available)
*/
def context(): Option[ChannelHandlerContext] = Option(self.pipeline().firstContext())
/**
* Readable-string helper.
*/
def toStringExt(): String =
{
import AttributeSupport._
import scala.concurrent.duration._
val stats = self.statistics
val session = s"requests=${stats.requestCount.get}, elapsed=${stats.elapsed.toMillis}ms"
val downstream = self.request flatMap (_.downstream) match {
case Some(channel) => s"allocations=${channel.statistics.allocations.get()}, open=${(System.nanoTime-channel.statistics.startTime).nanos.toMillis}ms"
case None => "N/A"
}
s"${self.toString.dropRight(1)}, $session, $downstream]"
}
}
implicit class ChannelFutureSupport(val self: ChannelFuture) extends AnyVal {
/**
* Add a proxy-promise listener to this ChannelFuture.
* @param promise
* @return
*/
def addListener(promise: ChannelPromise) =
self.addListener(new PromiseListener(promise))
/**
* Add a callback listener function to this ChannelFuture.
* @param f
* @return
*/
def addCloseListener(f: Channel => Unit) =
self.addListener(new ChannelFutureListener {
override def operationComplete(future: ChannelFuture): Unit = f(future.channel)
})
/**
* Register a Scala future on this channel.
* Note - makes no effort to ensure only one future is returned on multiple calls. TODO improve
*/
def future(): Future[Channel] = {
val p = Promise[Channel]()
self.addListener(new ChannelFutureListener {
override def operationComplete(future: ChannelFuture) =
if (future.isSuccess) p success (future.channel)
else if (future.isCancelled) p failure (new CancellationException)
else p failure (future.cause)
})
p.future
}
}
// Note: this needs to be a concrete AnyRef rather than the existential _
// due to Java/Scala support with overriding interfaces w. generic methods.
implicit class FutureSupport[T](val self: NettyFuture[T]) extends AnyVal {
def future(): Future[T] = Netty.toScala(self)
}
class PromiseListener(promise: ChannelPromise) extends ChannelFutureListener {
override def operationComplete(future: ChannelFuture): Unit =
if (future.isSuccess) promise.setSuccess(future.get())
else promise.setFailure(future.cause())
}
implicit class StringSupport(val self: Array[Byte]) extends AnyVal {
def toHexString = self.map("%02X" format _).mkString
}
implicit class ByteBufferSupport(val self: ByteBuf) extends AnyVal {
// Trims leading and trailing whitespace from byte-buffer and returns a slice
// representing the properly
def trim(start: Int=0, length: Int=self.readableBytes) = {
var first = start
var last = start+length-1
// Start at leading, and advance to first non-whitespace
while (first <= last && self.getByte(first).toChar.isWhitespace) { first += 1 }
// Walk back from end to first non-whitespace
while (last >= first && self.getByte(last).toChar.isWhitespace) { last -= 1 }
// Build the final slice
self.slice(first, last-first+1)
}
def skipControlCharacters: Unit =
while (true) {
self.readUnsignedByte.asInstanceOf[Char] match {
case ch if Character.isISOControl(ch) =>
case ch if Character.isWhitespace(ch) =>
case _ =>
// Restore positioning
self.readerIndex(self.readerIndex - 1)
return
}
}
def preserveIndex[T](f: => T): T = {
val startidx = self.readerIndex
try { f }
finally { self.readerIndex(startidx) }
}
}
/**
* URI support methods.
*
* Previously, we relied on Spray's case-object based implementation, but didn't didn't want to bring
* in the Akka transient dependencies.
*/
implicit class URISupport(val self: URI) extends AnyVal {
def isSecure() =
Option(self.getScheme) map (_.startsWith("https")) getOrElse false
def validHost: String = Option(self.getHost) match {
case Some("") | None => require(false, "Endpoint must have a valid host"); ""
case Some(host) => host
}
def validPort(default: Int=80) =
Option(self.getPort) filter (_ != 0) getOrElse default
}
class LazyOption[T](f: => Option[T]) {
private var _value: Option[T] = f
def apply() = _value
def reset() = { val result = _value; _value = None; result }
}
object LazyOption {
import scala.concurrent.duration._
def apply[T](f: => Option[Future[T]], timeout: Duration=1.second): LazyOption[T] = {
// NOTE!! - TRY NOT TO USE> BLOCKING>>>>>
new LazyOption[T]({ f map (Await.result(_, timeout)) })
}
}
// Not sure if this will be instantiated or not...
implicit class WhenSupport[T <: Any](val value: T) extends AnyVal {
def when(check: Boolean): Option[T] = if (check) Option(value) else None
def whenNot(check: Boolean): Option[T] = if (!check) Option(value) else None
}
/**
* Atomic counter DSL support
*/
implicit class AtomicLongSupport(val self: AtomicLong) extends AnyVal {
def +=(delta: Int) = self.addAndGet(delta)
def +=(delta: Long) = self.addAndGet(delta)
}
}
/**
* Netty-specific utility classes.
*/
object Netty {
def toScala[T](future: NettyFuture[T]): Future[T] = {
val p = Promise[T]()
future.addListener(new FutureListener[T] {
override def operationComplete(future: NettyFuture[T]): Unit = {
if (future.isSuccess) p success future.get
else if (future.isCancelled) p failure new CancellationException
else p failure future.cause
}
})
p.future
}
}
object Preconditions {
// TODO move to Utils.Preconditions
def checkDefined[T](f: Option[T]): T = {
require(f.isDefined)
f.get
}
def checkDefined[T](f: Option[T], msg: => Any): T = {
require(f.isDefined, msg)
f.get
}
}
/**
* Lazy-object initialization support.
*
* Use this when we want to defer lazy creation, but also determine if it has
* already been created.
*/
object Lazy {
def lazily[A](f: => A): Lazy[A] = new Lazy(f)
implicit def evalLazy[A](l: Lazy[A]): A = l()
}
class Lazy[A] private(f: => A) {
private var option: Option[A] = None
def apply(): A = option match {
case Some(a) => a
case None => val a = f; option = Some(a); a
}
def isEvaluated: Boolean = option.isDefined
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/Activity.scala
================================================
package com.ebay.neutrino.www
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.www.ui.PageFormatting
import com.ebay.neutrino.www.ui.SideMenu
import scala.concurrent.duration._
object ActivityPage extends SideMenu("Activity") with PageFormatting with Instrumented {
import scalatags.Text.all._
def page(): Frag = {
page(
h1(cls := "page-header", "Activity"),
activity()
)
}
def activity(): Frag =
Seq(
h2(cls :="sub-header", "Recent Activity"),
div(cls :="table-responsive",
table(cls :="table table-striped",
width:="100%", "cellspacing".attr:="0", "cellpadding".attr:="0", style:="width:100%;border:1px solid;padding:4px;",
thead(
td(b("URL")),
td(b("Pool")),
td(b("Request Size")),
td(b("Response Size")),
td(b("Duration")),
td(b("Status"))
),
tbody(
tr(
td("www.ebay.com"),
td("mryjenkins"),
td(count(3500)),
td(count(12050)),
td(1500.milliseconds.toString),
td("200 OK")
),
td("jenkins.ebay.com"),
td("mryjenkins"),
td(count(3500)),
td(count(100050)),
td(300.milliseconds.toString),
td("404 NOT FOUND")
)
)
)
)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/Overview.scala
================================================
package com.ebay.neutrino.www
import com.ebay.neutrino.api.ApiData
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.www.ui.PageFormatting
import com.ebay.neutrino.www.ui.SideMenu
object Overview extends SideMenu("Overview") with OverviewPage with PageFormatting with ApiData with Instrumented {
import ApiData._
import scalatags.Text.all._
//Caller: val status = generateStatus(core)
def generate(status: Status): Frag = {
page(
h1(cls := "page-header", "Overview"),
h2(cls :="sub-header", "Traffic"),
traffic(status),
h2(cls := "sub-header", "Requests"),
requests(status),
h2(cls :="sub-header", "Host Information"),
hostinfo()
)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/OverviewPage.scala
================================================
package com.ebay.neutrino.www
import com.ebay.neutrino.util.ServerContext
import com.ebay.neutrino.www.ui.PageFormatting
import com.ebay.neutrino.www.ui.Page
import com.ebay.neutrino.api.ApiData
import scala.concurrent.duration._
trait OverviewPage extends PageFormatting { self: Page =>
import ApiData._
import scalatags.Text.all._
def traffic(status: Status): Frag = {
div(cls :="table-responsive",
table(cls :="table table-striped",
width:="100%", "cellspacing".attr:="0", "cellpadding".attr:="0", style:="width:100%;border:1px solid;padding:4px;",
thead(
td(i("Last restart ", status.host.lastRestart), style:="width:25%"),
td(b("Bytes"), colspan:=2, textAlign:="center", style:="width:25%"),
td(b("IO Events"), colspan:=2, textAlign:="center", style:="width:25%"),
td(b("Connections"), colspan:=2, textAlign:="center", style:="width:25%")
),
tbody(
tr(
td(b("Type")),
td(b("In")), td(b("Out")),
td(b("In")), td(b("Out")),
td(b("Current")), td(b("Total"))
),
tr(
td("Upstream"),
td(bytes(status.upstreamTraffic.bytesIn)), td(bytes(status.upstreamTraffic.bytesOut)),
td(count(status.upstreamTraffic.packetsIn)), td(count(status.upstreamTraffic.packetsOut)),
td(status.upstreamTraffic.currentConnections), td(status.upstreamTraffic.totalConnections)
),
tr(
td("Downstream"),
td(bytes(status.downstreamTraffic.bytesIn)), td(bytes(status.downstreamTraffic.bytesOut)),
td(count(status.downstreamTraffic.packetsIn)), td(count(status.downstreamTraffic.packetsOut)),
td(status.downstreamTraffic.currentConnections), td(status.downstreamTraffic.totalConnections)
)
)
)
)
}
def requests(status: Status): Frag = {
// Split the response-statuses into timer-groups
val responses = status.responses groupBy (_.responseType) mapValues (_.head.stats)
// Prepare histogram table portions
def timer(title: String, count: Long, data: Option[Timer]): Frag = {
// Render-helper
def valid(f: Timer => String): String = data map (f(_)) getOrElse "N/A"
tr(
td(title),
td(if (count >= 0) count.toString else ""),
td(valid(_.count.toString)),
td(valid(_.min.nanos.toMillis + "ms")),
td(valid(_.mean.nanos.toMillis + "ms")),
td(valid(_.`95th`.nanos.toMillis + "ms")),
td(valid(_.max.nanos.toMillis + "ms")),
td(valid(t => "%.2f/s".format(t.`1min`))),
td(valid(t => "%.2f/s".format(t.`5min`))),
td(valid(t => "%.2f/s".format(t.`15min`)))
)
}
div(cls := "table-responsive",
table(cls := "table table-striped",
width := "100%", "cellspacing".attr := "0", "cellpadding".attr := "0", style := "width:100%;border:1px solid;padding:4px;",
thead(
td("Type"),
td("Active", style := "width:8%"),
td("Total", style := "width:8%"),
td("Min Elapsed", style := "width:9%"),
td("Avs Elapsed", style := "width:9%"),
td("95% Elapsed", style := "width:9%"),
td("Max Elapsed", style := "width:9%"),
td("Last Min Rate", style := "width:9%"),
td("Last 5m Rate", style := "width:9%"),
td("Last 15m Rate", style := "width:9%")
),
tbody(
timer(
"Request Sessions", status.sessions.active, Option(status.sessions.stats) //Metrics.SessionActive.count, Metrics.SessionDuration
),
timer(
"HTTP Requests", status.requests.active, Option(status.requests.stats) //Metrics.RequestsOpen.count, Metrics.RequestsCompleted
),
timer(
"HTTP Requests - 2xx Response", -1, responses get "2xx"
),
timer(
"HTTP Requests - 4xx Response", -1, responses get "4xx"
),
timer(
"HTTP Requests - 5xx Response", -1, responses get "5xx"
),
timer(
"HTTP Requests - Incomplete", -1, responses get "incomplete"
),
timer(
"HTTP Requests - Other", -1, responses get "other")
)
)
)
}
def hostinfo(info: HostInfo): Frag =
div(cls :="table-responsive",
table(cls :="table table-striped",
width:="100%", "cellspacing".attr:="0", "cellpadding".attr:="0", style:="width:100%;border:1px solid;padding:4px;",
thead(
td("Hostname"),
td("Address"),
td("Last Restart Time")
),
tbody(
tr(
td(info.hostname),
td(info.address),
td(info.lastRestart)
)
)
)
)
def hostinfo(): Frag =
div(cls :="table-responsive",
table(cls :="table table-striped",
width:="100%", "cellspacing".attr:="0", "cellpadding".attr:="0", style:="width:100%;border:1px solid;padding:4px;",
thead(
td("Property"),
td("Detail", width:="85%")
),
tbody(
tr(
td("Last Restart Time"),
td(starttime.toString)
),
tr(
td("Hostname"),
td(str(ServerContext.fullHostName))
),
tr(
td("Canonical Hostname"),
td(str(ServerContext.canonicalHostName))
),
tr(
td("IP/Address"),
td(str(ServerContext.hostAddress))
)
/*tr(
td("Local Addresses"),
td(str(NetworkUtils.cachedNames mkString ", "))
),
*/
)
)
)
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/PoolsPages.scala
================================================
package com.ebay.neutrino.www
import com.ebay.neutrino.config.{CanonicalAddress, VirtualPool, WildcardAddress}
import com.ebay.neutrino.metrics.Instrumented
import com.ebay.neutrino.www.ui.{Page, PageFormatting}
import com.ebay.neutrino.{NeutrinoNode, NeutrinoPoolId}
/**
* Result-set support with DataTables.
* @see http://www.datatables.net/examples/data_sources/dom.html
*/
trait PoolsPage extends PageFormatting with Instrumented { self: Page =>
import scalatags.Text.all._
/** Extract canonical-addresses from pool */
@inline def canonicals(pool: VirtualPool): Seq[CanonicalAddress] =
pool.addresses collect {
case canonical: CanonicalAddress => canonical
}
def summary(pools: Seq[VirtualPool]): Frag = {
pageWithLoad(
div(
h5(cls := "pull-right", a(href := s"/refresh", u("Reload Config"))),
h1(cls := "page-header", "Pools")
),
//h2(cls :="sub-header", "Applications Loaded"),
div(cls :="table-responsive",
table(id := "results", cls :="table table-striped",
thead(
tr(
th("Pool Name"),
th("Protocol"),
th("Servers"),
th("CNAME"),
th("Balancer"),
th("Timeouts")
)
),
tbody(
pools map { pool =>
val id = NeutrinoPoolId(pool.id, pool.protocol)
val cname = (canonicals(pool) map (_.host) headOption) getOrElse "N/A"
tr(
td(a(href := s"/pools/$id", pool.id.split(":").head)),
td(pool.protocol.toString.toUpperCase),
td(a(href := s"/pools/$id", pool.servers.size+" Servers")),
td(a(href := s"/pools/$id", cname)),
td(pool.balancer.clazz.getSimpleName),
td("Default")
)
}
)
)
)
)("""
$('#results').DataTable();
""")
}
// Build the detail page (as applicable)
// TODO deal with not-found
def detail(pool: Option[VirtualPool]): Frag =
page(
h1(cls := "page-header", s"Pool Detail"),
pool match {
case None =>
Seq(p("Not found"))
case Some(pool) =>
Seq(
h2(cls :="sub-header", "Details"),
div(cls :="table-responsive",
table(cls :="table table-striped",
thead(
tr(
th("Pool ID"),
th("Type"),
th("Hostname (CNAME/A)"),
th("Source Port"),
th("IP Resolved"),
th("Routing Policy")
)
),
tbody(
pool.addresses collect {
case address: CanonicalAddress =>
tr(
td(pool.id),
td("CNAME"),
td(address.host),
td(address.port),
td(address.socketAddress.toString),
td()
)
case address: WildcardAddress =>
tr(
td(pool.id),
td("L7 Rule"),
td(address.host),
td(address.port),
td(),
td(address.socketAddress.toString),
td(address.path)
)
}
)
)
),
div(cls :="table-responsive",
table(cls :="table table-striped",
thead(
tr(
th("Read Idle Timeout"),
th("Write Idle Timeout"),
th("Write Completion Timeout"),
th("Request Timeout"),
th("Session Timeout")
)
),
tbody(
tr(
td(pool.timeouts.readIdle.toSeconds+"s"),
td(pool.timeouts.writeIdle.toSeconds+"s"),
td(pool.timeouts.writeCompletion.toSeconds+"s"),
td(pool.timeouts.requestCompletion.toSeconds+"s"),
td(pool.timeouts.sessionCompletion.toSeconds+"s")
)
)
)
),
h2(cls :="sub-header", "Servers"),
div(cls :="table-responsive",
table(cls :="table table-striped",
thead(
tr(
th("ServerId"),
th("Host"),
th("Port"),
th("Health State")
)
),
tbody(
pool.servers map { server =>
tr(
td(server.id),
td(server.host),
td(server.port),
td(server.healthState.toString)
)
}
)
)
)
//h2(cls :="sub-header", "Load Balancer")
)
}
)
}
/**
* Result-set support with DataTables.
* @see http://www.datatables.net/examples/data_sources/dom.html
*/
trait ServersPage extends PageFormatting with Instrumented { self: Page =>
import scalatags.Text.all._
def summary(pools: Seq[VirtualPool], nodes: Seq[NeutrinoNode]=Seq()): Frag = {
// If available, grab 'active' server info
val nodemap = nodes.groupBy(_.settings) mapValues (_.head)
pageWithLoad(
h1(cls := "page-header", "Servers"),
//h2(cls :="sub-header", "Applications Loaded"),
div(cls :="table-responsive",
table(id := "results", cls :="table table-striped",
thead(
tr(
th("Host"),
th("Port"),
th("Health State"),
th("Pool Name"),
th("Active Connections"),
th("Pooled Connections"),
th("Last Activity")
)
),
tbody(
pools map { pool =>
pool.servers map { server =>
val node = nodemap get (server)
val id = NeutrinoPoolId(pool.id, pool.protocol)
val allocated = node map (_.allocated.size.toString) getOrElse "-"
val available = node map (_.available.size.toString) getOrElse "-"
tr(
td(server.host),
td(server.port),
td(server.healthState.toString),
td(a(href := s"/pools/$id", pool.id.split(":").head)),
td(allocated),
td(available),
td("-")
)
}
}
)
)
)
)("""
$('#results').DataTable();
""")
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/Web.scala
================================================
package com.ebay.neutrino.www
import java.util.concurrent.TimeUnit
import akka.actor.{ActorRef, Props, ActorSystem, ScalaActorRef}
import akka.pattern.ask
import akka.util.Timeout
import com.ebay.neutrino.{SLB, NeutrinoPoolId}
import com.ebay.neutrino.api.ApiData
import com.ebay.neutrino.cluster.{SLBTopology, SystemConfiguration}
import com.ebay.neutrino.www.ui.SideMenu
import com.ebay.neutrino.www.ui.PageFormatting
import com.ebay.neutrino.cluster.SLBLoader
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.scalalogging.slf4j.StrictLogging
import spray.http.StatusCodes
import scala.concurrent.Await
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
trait WebService extends spray.routing.HttpService with ApiData with PageFormatting with StrictLogging
{
def system: ActorSystem
def topology = SystemConfiguration(system).topology
val poolPage = new SideMenu("Pools") with PoolsPage
val serverPage = new SideMenu("Servers") with ServersPage
def webRoutes =
path ("activity") {
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
ActivityPage.page()
}
} ~
path("pools") {
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
poolPage.summary(topology.toSeq)
}
} ~
path("pools" / Segment) { id =>
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
val pool = topology.getPool(NeutrinoPoolId(id))
poolPage.detail(pool)
}
} ~
path("servers") {
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
val pools = topology.toSeq
val services = topology.asInstanceOf[SLBTopology].core.services
val nodes = services flatMap (_.pools()) flatMap (_.nodes())
serverPage.summary(pools, nodes.toSeq)
}
} ~
path("refresh") {
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
implicit val timeout = Timeout(FiniteDuration(3, TimeUnit.SECONDS))
// Wait for the result, since refresh api has to be synchronous
val reloader = Await.result(system.actorSelection("user/loader").resolveOne(), timeout.duration)
val future = reloader ? "reload"
val result = Await.result(future, timeout.duration)
if (result == "complete") {
logger.warn("Config reloaded, Successfully completed")
} else {
logger.warn("Unable to load the configuration")
}
poolPage.summary(topology.toSeq)
}
} ~
path("config") {
complete {
val sysconfig = SystemConfiguration(system)
sysconfig.config.root.render(ConfigRenderOptions.defaults)
}
} ~
pathEndOrSingleSlash {
complete {
import PageFormatting.ScalaTagsPrettyMarshaller
Overview.generate(generateStatus())
}
} ~
get {
redirect("/", StatusCodes.PermanentRedirect)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/ui/Page.scala
================================================
package com.ebay.neutrino.www.ui
import scalatags.Text.TypedTag
trait Page extends PageFormatting {
import scalatags.Text.all._
//import scala.language.implicitConversions
val CSS = "/css/dashboard.css"
// Additional html components
val nav = "nav".tag[String]
val defer = "defer".attr
// Defaults; should be overridden by children/implementors
def pagename: String = "Neutrino"
def prettyPrint: Boolean = true
//css/jumbotron.css
def header(customStylesheet: Option[String]) = {
head(
title := "Neutrino SLB Dashboard",
meta(charset := "utf-8"),
meta(httpEquiv := "X-UA-Compatible", content := "IE=edge"),
meta(name :="viewport", content:="width=device-width, initial-scale=1"),
//meta(description := "Neutrino SLB"),
//meta(author := "cbrawn"),
link(href := "/favicon.ico", rel :="icon"),
//
link(href := "/css/bootstrap.min.css", rel :="stylesheet"),
// MIT license http://datatables.net/license/
link(href := "http://cdn.datatables.net/1.10.5/css/jquery.dataTables.css", rel :="stylesheet"),
//
if (customStylesheet.isDefined)
link(href := customStylesheet.get, rel := "stylesheet")
else
div()
)
}
def onLoad(frags: Frag*): Frag = {
if (frags.isEmpty)
()
else {
script(
"""$(document).ready(function(){""",
frags,
"""});""")
}
}
// Layout the full page
def page(bodyFragment: Frag*): Frag =
page(bodyFragment, Seq())
def pageWithLoad(bodyFragment: Frag*)(onLoads: Frag*): Frag =
page(bodyFragment, onLoads)
def page(bodyFragment: Seq[Frag], onLoads: Seq[Frag], pretty: Boolean=true): Frag = {
html(lang := "en",
header(Option(CSS)),
body(
navigation(),
div(cls :="container-fluid",
div(cls := "row",
menu(),
div(cls := "col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main",
bodyFragment
)
)
),
//
//
// ?? should we put jquery inline?
script(src := "https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", " "),
// MIT license http://datatables.net/license/
script(src := "http://cdn.datatables.net/1.10.5/js/jquery.dataTables.min.js", " "),
script(src := "/js/bootstrap.min.js", " "),
script(src := "/assets/js/docs.min.js", " "),
//
script(src := "/assets/js/ie10-viewport-bug-workaround.js", " "),
// Kick off any page-wide on-load events
onLoad(onLoads)
)
)
}
def navigation() =
nav(cls := "navbar navbar-inverse navbar-fixed-top",
div(cls := "container-fluid",
div(cls := "navbar-header",
button(cls := "navbar-toggle collapsed",
`type` := "button",
data.toggle := "collapse",
data.target := "#navbar",
aria.expanded := "false",
aria.controls := "navbar",
span(cls := "sr-only", "Toggle navigation"),
span(cls := "icon-bar"),
span(cls := "icon-bar"),
span(cls := "icon-bar")
),
a(cls :="navbar-brand", href := "#", "Neutrino Load Balancer")
)
)
)
def pagelink(link: String, name: String) =
if (name == pagename)
li(cls :="active", a(href :=link, name, span(cls :="sr-only", " (current)")))
else
li(a(href :=link, name))
def menu(): TypedTag[String]
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/ui/PageFormatting.scala
================================================
package com.ebay.neutrino.www.ui
import java.util.Date
import com.typesafe.scalalogging.slf4j.StrictLogging
import nl.grons.metrics.scala.{Counter, Meter}
import spray.http.ContentType
import spray.http.MediaTypes._
import spray.httpx.marshalling.Marshaller
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
import scala.xml.Elem
trait PageFormatting extends StrictLogging {
import com.twitter.conversions.storage._
import scala.language.implicitConversions
val pretty = true
val prettyXml = false // Use XML parsing for prettyprinting
val prettier = new scala.xml.PrettyPrinter(160, 4)
val starttime = new Date()
// Run the HTML format content through the pretty-printer
def prettify(content: String): String = {
if (pretty && prettyXml)
Try(prettier.format(scala.xml.XML.loadString(content))) match {
case Success(content) => content
case Failure(ex) => logger.warn("Unable to pretty-print html", ex); content
}
else if (pretty)
PrettyPrinter.format(content)
else
content
}
def prettyxml(content: String) = {
Try(prettier.format(scala.xml.XML.loadString(content))) match {
case Success(content) => content
case Failure(ex) => logger.warn("Unable to pretty-print html", ex); content
}
}
// Convert current time to uptime
def uptime() = pretty((System.currentTimeMillis()-starttime.getTime).millis)
// Convenience method: pretty print storage size
def bytes(data: Long): String = data.bytes.toHuman
// Convenience method: pretty print count size
def count(data: Long): String =
data match {
case count if count < (2<<10) => s"$count"
case count if count < (2<<18) => "%.1f K".format(count/1000f)
case count if count < (2<<28) => "%.1f M".format(count/1000000f)
case count => "%.1f G".format(count/1000000000f)
}
// Convenience method; pretty print time
def pretty(duration: FiniteDuration): String = {
if (duration.toDays > 0) duration.toDays+" days"
else if (duration.toHours > 0) duration.toHours+" hours"
else if (duration.toMinutes > 0) duration.toMinutes+" minutes"
else duration.toSeconds +" seconds"
}
// Convenience method; ensure non-null string
@inline def str(value: String) = if (value == null) "" else value
}
object PageFormatting extends PageFormatting {
import scalatags.Text.all._
val SupportedOutput: Seq[ContentType] =
Seq(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)
implicit val ScalaTagsMarshaller =
Marshaller.delegate[Frag, String](SupportedOutput:_*) { frag =>
"\n" + frag.toString
}
implicit val ScalaTagsPrettyMarshaller =
Marshaller.delegate[Frag, String](SupportedOutput:_*) { frag =>
"\n" + prettyxml(frag.toString)
}
implicit val XmlMarshaller =
Marshaller.delegate[Elem, String](SupportedOutput:_*) { elem =>
prettify(elem.toString)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/ui/ResourceServices.scala
================================================
package com.ebay.neutrino.www.ui
import akka.actor.ActorSystem
import spray.routing.Route
trait ResourceServices extends spray.routing.HttpService
{
def system: ActorSystem
def getPathFromResourceDirectory(path: String): Route =
pathPrefix (path) {
get { getFromResourceDirectory(path) }
}
def resourceRoutes =
pathPrefix ("assets") {
get {
getFromResourceDirectory("assets")
}
} ~
pathPrefix ("css") {
get {
getFromResourceDirectory("css")
}
} ~
pathPrefix ("js") {
get {
getFromResourceDirectory("js")
}
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/ui/SideMenu.scala
================================================
package com.ebay.neutrino.www.ui
import com.ebay.neutrino.www.ui
class SideMenu(override val pagename: String="Neutrino", override val prettyPrint: Boolean=true)
extends ui.Page
with PageFormatting
{
import scalatags.Text.all._
def menu() = {
def pagelink(link: String, name: String) =
if (name == pagename)
li(cls :="active", a(href :=link, name, span(cls :="sr-only", " (current)")))
else
li(a(href :=link, name))
div(cls := "col-sm-3 col-md-2 sidebar",
ul(cls := "nav nav-sidebar",
pagelink("/", "Overview")
),
ul(cls := "nav nav-sidebar",
pagelink("/pools", "Pools"),
pagelink("/servers", "Servers")
),
ul(cls := "nav nav-sidebar",
pagelink("/config", "Raw Configuration")
)
)
}
}
================================================
FILE: src/main/scala/com/ebay/neutrino/www/ui/Utilities.scala
================================================
package com.ebay.neutrino.www.ui
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.sax.SAXResult
import scala.xml.parsing.NoBindingFactoryAdapter
/**
* There is a long, sordid tale around pretty printing here. It all starts with
* some useful JavaScript code that uses a character or two which are verbotten
* in XML (ie, the && characters).
*
* When outputting this, we can write raw() and these will go to the output, but
* by default when the ScalaTags 'frags' are output the default toString() method
* simply dumps everything rendered (ie: not the raw() blocks) out as one line.
*
* There is PrettyPrinter support, using the default XML library.
* > new PrettyPrinter(80,4).format(content)
*
* This, however, barfs on the parse of the 'invalid' characters. This means we can
* either have:
* - unformatted by 'normal' code (as-is)
* - formatted code without tolerance to JavaScript un-escape
*
* Just escape the JavaScript, you might say. Unfortunately, this makes the browser
* barf, as the JS is not recognized while URLEncoded.
*
*
* The simplest is to do one of two things:
* 1) Fix the parser (can't use default scala XML).
* Lift has one that works but has awful documentation.
* 2) Escape the javascript with a CDATA block.
* Oh yes, this is also not parsed by XML properly.
* 3) Pretty print at source.
* Allows us to print the first block while ignoring the JS completely.
* Just isn't really expected...
*
* So eff-it; let's just dirty up this 'pretty printer' for generating rolled-over
* output. It's not glam but it'll keep our output trim.
*
* @see http://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java
*/
object PrettyPrinter {
val Indent = " "
def format(xml: String): String = {
if (xml == null || xml.trim().length() == 0) return "";
var indent = 0
var inscript = false
val pretty = new StringBuilder()
val rows = xml.trim().replaceAll(">", ">\n").replaceAll("<", "\n<").replaceAll("\n\n", "\n").split("\n")
// Output while adjusting row-indent
rows.iterator foreach {
case "" =>
// skip
case row if inscript =>
pretty.append(row).append('\n')
case row if row.startsWith("
inscript = false
indent -= 1
pretty.append(Indent*indent).append(row.trim).append('\n')
case row if row.startsWith("") =>
indent -= 1
pretty.append(Indent*indent).append(row.trim).append('\n')
//case "