├── .gitignore ├── LICENSE ├── README.md ├── dist ├── apc-1.0.jar ├── config_vars ├── deploy.py ├── instr_prepare.sh └── instr_report.sh ├── policy_exec.png ├── pom.xml ├── sample_summary.png ├── src └── main │ ├── java │ └── com │ │ └── github │ │ └── sriki77 │ │ └── apiproxy │ │ └── instrument │ │ ├── Instrumenter.java │ │ ├── KVMapBasedProxyInstrumenter.java │ │ ├── ProxyInstrumeter.java │ │ ├── io │ │ ├── NodeCleaner.java │ │ ├── PolicyUpdater.java │ │ ├── ProxyDirectoryHandler.java │ │ ├── ProxyFileHandler.java │ │ ├── ProxyZipFileHandler.java │ │ └── Util.java │ │ ├── model │ │ ├── DOMStep.java │ │ ├── Endpoint.java │ │ ├── FaultRule.java │ │ ├── FaultRules.java │ │ ├── Flow.java │ │ ├── FlowSteps.java │ │ ├── Flows.java │ │ ├── LocationProvider.java │ │ ├── NodeHolder.java │ │ ├── PolicyUpdate.java │ │ ├── PostFlow.java │ │ ├── PreFlow.java │ │ ├── ProxyEndpoint.java │ │ ├── RequestFlow.java │ │ ├── ResponseFlow.java │ │ ├── Step.java │ │ └── TargetEndpoint.java │ │ └── report │ │ ├── EndpointStat.java │ │ ├── FlowStat.java │ │ ├── InstrumentReportGenerator.java │ │ ├── InstrumentResultMap.java │ │ ├── KVMapInstrumentReportGenerator.java │ │ ├── ProxyStat.java │ │ └── Stats.java │ └── resources │ ├── kv_instr_template.xml │ ├── non-flow-tags-remover.xsl │ └── report │ ├── bootstrap.min.css │ ├── index.html │ ├── proxy.xsl │ ├── summary.xsl │ └── table_snippet.html └── test-policy.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.next 5 | release.properties 6 | .idea 7 | *.iml 8 | out 9 | profile_test* 10 | cov_report 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Srikanth Seshadri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Apigee Proxy Coverage 2 | ================== 3 | 4 | This is a tool to determine test case coverage for [Apigee](www.apigee.com) proxies. 5 | 6 | Given a fully functional [API proxy](http://apigee.com/docs/), the tool produces an instrumented proxy file as output. The instrumented proxy file should be deployed to Apigee Edge against which the test cases should be run. Upon completion, the execution data is retrieved by the tool from Apigee Edge to produce the coverage report. 7 | 8 | The tool determines coverage of single API proxy under test. 9 | 10 | Coverage in case of Apigee proxies is the percentage of policies executed with respect to the total number of policies. Unlike, programming languages where the practical acceptable coverage is around 80%, in case of Apigee proxies the coverage must be 100% - there are no policies that need not be tested or have a remote possibility of execution. 11 | 12 | Usage 13 | -------- 14 | 15 | **Pre-requisties** 16 | 17 | The [distribution](https://github.com/apigee/apigee-proxy-coverage/releases) provides shell scripts, a executable `Java 8` jar file and `python` scripts - which makes` bash, python, Java SE 8` as pre-requisites. 18 | 19 | --- 20 | 21 | **Steps:** *(Short Version)* 22 | 23 | 1. Configure environment settings in `config_vars` 24 | 2. Run `instr_prepare.sh` 25 | 3. Run your test cases against the instrumented proxy deployed by previous script 26 | 4. Run `instr_report.sh` to obtain the coverage report. 27 | 28 | --- 29 | 30 | **Steps:** *(A bit more detail)* 31 | 32 | 1. Enter your environment details in the file `config_vars`. The details of Apigee `org`, `env`, `api` proxy for coverage and the `revision` of the proxy to be used for instrumentation. Credentials to access the `org` is essential. 33 | 2. Run `instr_prepare.sh`. Following are steps performed by this script. 34 | + Download specified revision of proxy bundle from the `org`. 35 | + Instrument the downloaded bundle. 36 | + Deploy the instrumented bundle into the `org` and activate it. 37 | + Delete any instrumented data recorded against the proxy in the org. 38 | 3. Run the all tests against the deployed instrumented bundle. The instrument bundle has the same base path as the original bundle 39 | 4. Run `instr_report.sh` to obtain the coverage results. Following are the steps performed by this script. 40 | + Download the instrumented data recorded in the [Key Value map](http://apigee.com/docs/api-services/content/persist-data-using-keyvaluemap) in Apigee edge. 41 | + Generate the coverage report. The report has XML format - a `summary.xml` and one XML corresponding to every proxy and target endpoints are generated. 42 | + Custom stylesheets can be used convert the XMLs into desired report format. 43 | + A HTML version of the report is also generated by applying the default stylesheet. 44 | + Open the `summary.html` in the default browser. 45 | 46 | Sample Report Screenshots 47 | -------------------------------------- 48 | 49 | **Summary** 50 | ![summary](https://raw.githubusercontent.com/apigee/apigee-proxy-coverage/master/sample_summary.png) 51 | 52 | **Policy Execution Drilldown** 53 | 54 | ![policy execution](https://raw.githubusercontent.com/apigee/apigee-proxy-coverage/master/policy_exec.png) 55 | 56 | Coverage Numbers 57 | ---------------- 58 | 59 | Coverage is percentage of policies executed against the total number of policies that exists in the API proxy. With this definition it's possible to have overall coverage at 100%, while the flow level coverage be less than 100%. This is because all the policies would have been executed - but not on all paths. 60 | 61 | It's best to achieve 100% flow level coverage in effect attain 100% overall coverage, than vice versa. 62 | 63 | Limitations 64 | -------------- 65 | All proxy endpoints are expected to have **only one** 66 | 67 | + `PreFlow` tag 68 | + `PostFlow` tag 69 | + `FaultRules` tag 70 | 71 | Apigee Edge allows multiple of the above tags, though the behaviour in certain cases is un-documented and not supported for instrumentation. 72 | 73 | 74 | Internals 75 | ------------ 76 | 77 | The tool has to determine the policies that executed in each flow to arrive at the final coverage number. 78 | 79 | The tool instruments a given proxy using the following approach. 80 | 81 | 1. For each policy a new key-value map policy is added to record the execution. 82 | 2. The policy is added prior to the actual policy with the same condition as the one that exists on the actual. 83 | 3. The rationale is that if the key-value map policy executes so will the actual policy since the same condition is satisfied. 84 | 4. Further, its added prior to the actual policy to handle the scenario of fault policies in which case the policy following the fault will not be executed. 85 | 2. The generated policies have random unique names to avoid conflict. They log the information into a Key Value Map named instrument at the api proxy level. 86 | 3. After test execution, the tool uses the content of the key value map to reconstruct the execution information and calculate coverage. 87 | 88 | About 89 | -------- 90 | The initial version is implemented by Srikanth Seshadri. [Interesting feedback](https://github.com/apigee/apigee-proxy-coverage/issues?state=open) has been provided by the Apigee CS architects for enhancement. Please feel free to contribute. 91 | 92 | 93 | -------------------------------------------------------------------------------- /dist/apc-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apigee/apigee-proxy-coverage/5fc1e23fba0238fc61e35290607d320aae310391/dist/apc-1.0.jar -------------------------------------------------------------------------------- /dist/config_vars: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #=========================================== 4 | # Edit this section only 5 | 6 | # Proxy Env Configuration 7 | export org= #Org name 8 | export api= #API proxy name 9 | export env= # Env name 10 | export rev= # API Proxy revision for instrumentation 11 | 12 | #Credentials 13 | export username= #Username 14 | export password= #password 15 | 16 | #Report directory 17 | export report_dir=coverage_report # report directory 18 | #=========================================== -------------------------------------------------------------------------------- /dist/deploy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | import getopt 5 | import httplib 6 | import json 7 | import re 8 | import os 9 | import sys 10 | import StringIO 11 | import urlparse 12 | import xml.dom.minidom 13 | import zipfile 14 | 15 | debug=False 16 | 17 | def httpCall(verb, uri, headers, body): 18 | if httpScheme == 'https': 19 | conn = httplib.HTTPSConnection(httpHost) 20 | else: 21 | conn = httplib.HTTPConnection(httpHost) 22 | 23 | if headers == None: 24 | hdrs = dict() 25 | else: 26 | hdrs = headers 27 | 28 | hdrs['Authorization'] = 'Basic %s' % base64.b64encode(UserPW) 29 | conn.request(verb, uri, body, hdrs) 30 | 31 | return conn.getresponse() 32 | 33 | def getElementText(n): 34 | c = n.firstChild 35 | str = StringIO.StringIO() 36 | 37 | while c != None: 38 | if c.nodeType == xml.dom.Node.TEXT_NODE: 39 | str.write(c.data) 40 | c = c.nextSibling 41 | 42 | return str.getvalue().strip() 43 | 44 | def getElementVal(n, name): 45 | c = n.firstChild 46 | 47 | while c != None: 48 | if c.nodeName == name: 49 | return getElementText(c) 50 | c = c.nextSibling 51 | 52 | return None 53 | 54 | # Return TRUE if any component of the file path contains a directory name that 55 | # starts with a "." like '.svn', but not '.' or '..' 56 | def pathContainsDot(p): 57 | c = re.compile('\.\w+') 58 | 59 | for pc in p.split('/'): 60 | if c.match(pc) != None: 61 | return True 62 | 63 | return False 64 | 65 | def getDeployments(): 66 | # Print info on deployments 67 | hdrs = {'Accept': 'application/xml'} 68 | resp = httpCall('GET', 69 | '/v1/organizations/%s/apis/%s/deployments' \ 70 | % (Organization, Name), 71 | hdrs, None) 72 | 73 | if resp.status != 200: 74 | return None 75 | 76 | ret = list() 77 | deployments = xml.dom.minidom.parse(resp) 78 | environments = deployments.getElementsByTagName('Environment') 79 | 80 | for env in environments: 81 | envName = env.getAttribute('name') 82 | revisions = env.getElementsByTagName('Revision') 83 | for rev in revisions: 84 | revNum = int(rev.getAttribute('name')) 85 | error = None 86 | state = getElementVal(rev, 'State') 87 | basePaths = rev.getElementsByTagName('BasePath') 88 | 89 | if len(basePaths) > 0: 90 | basePath = getElementText(basePaths[0]) 91 | else: 92 | basePath = 'unknown' 93 | 94 | # svrs = rev.getElementsByTagName('Server') 95 | status = {'environment': envName, 96 | 'revision': revNum, 97 | 'basePath': basePath, 98 | 'state': state} 99 | 100 | if error != None: 101 | status['error'] = error 102 | 103 | ret.append(status) 104 | 105 | return ret 106 | 107 | def printDeployments(dep): 108 | for d in dep: 109 | print 'Environment: %s' % d['environment'] 110 | print ' Revision: %i BasePath = %s' % (d['revision'], d['basePath']) 111 | print ' State: %s' % d['state'] 112 | if 'error' in d: 113 | print ' Error: %s' % d['error'] 114 | 115 | ApigeeHost = 'https://api.enterprise.apigee.com' 116 | UserPW = None 117 | Directory = None 118 | Organization = None 119 | Environment = None 120 | Name = None 121 | BasePath = '/' 122 | ShouldDeploy = True 123 | seamless = True 124 | 125 | Options = 'h:u:d:e:n:p:o:i:z:' 126 | 127 | opts = getopt.getopt(sys.argv[1:], Options)[0] 128 | 129 | for o in opts: 130 | if o[0] == '-n': 131 | Name = o[1] 132 | elif o[0] == '-o': 133 | Organization = o[1] 134 | elif o[0] == '-h': 135 | ApigeeHost = o[1] 136 | elif o[0] == '-d': 137 | Directory = o[1] 138 | elif o[0] == '-e': 139 | Environment = o[1] 140 | elif o[0] == '-p': 141 | BasePath = o[1] 142 | elif o[0] == '-u': 143 | UserPW = o[1] 144 | elif o[0] == '-i': 145 | ShouldDeploy = False 146 | elif o[0] == '-v': 147 | seamless = False 148 | elif o[0] == '-z': 149 | ZipFile = o[1] 150 | 151 | if UserPW == None or \ 152 | (Directory == None and ZipFile == None) or \ 153 | Environment == None or \ 154 | Name == None or \ 155 | Organization == None: 156 | print """Usage: deploy -n [name] (-d [directory name] | -z [zipfile]) 157 | -e [environment] -u [username:password] -o [organization] 158 | [-p [base path] -h [apigee API url] -i -v] 159 | base path defaults to "/" 160 | Apigee URL defaults to "https://api.enterprise.apigee.com" 161 | -i denotes to import only and not actually deploy 162 | -v denotes non-seamless deployments 163 | """ 164 | sys.exit(1) 165 | 166 | url = urlparse.urlparse(ApigeeHost) 167 | httpScheme = url[0] 168 | httpHost = url[1] 169 | 170 | body = None 171 | 172 | if Directory != None: 173 | # Construct a ZIPped copy of the bundle in memory 174 | tf = StringIO.StringIO() 175 | zipout = zipfile.ZipFile(tf, 'w') 176 | 177 | dirList = os.walk(Directory) 178 | for dirEntry in dirList: 179 | if not pathContainsDot(dirEntry[0]): 180 | for fileEntry in dirEntry[2]: 181 | if not fileEntry.endswith('~'): 182 | fn = os.path.join(dirEntry[0], fileEntry) 183 | en = os.path.join( 184 | os.path.relpath(dirEntry[0], Directory), 185 | fileEntry) 186 | if (debug): print 'Writing %s to %s' % (fn, en) 187 | zipout.write(fn, en) 188 | 189 | zipout.close() 190 | body = tf.getvalue() 191 | elif ZipFile != None: 192 | f = open(ZipFile, 'r') 193 | body = f.read() 194 | f.close() 195 | 196 | # Upload the bundle to the API 197 | hdrs = {'Content-Type': 'application/octet-stream', 198 | 'Accept': 'application/json'} 199 | uri = '/v1/organizations/%s/apis?action=import&name=%s' % \ 200 | (Organization, Name) 201 | resp = httpCall('POST', uri, hdrs, body) 202 | 203 | if resp.status != 200 and resp.status != 201: 204 | print 'Import failed to %s with status %i:\n%s' % \ 205 | (uri, resp.status, resp.read()) 206 | sys.exit(2) 207 | 208 | deployment = json.load(resp) 209 | revision = int(deployment['revision']) 210 | 211 | print 'Imported new proxy version %i' % revision 212 | 213 | if ShouldDeploy: 214 | # Undeploy duplicates 215 | def nonSeamLessDeployments(): 216 | deps = getDeployments() 217 | for d in deps: 218 | if d['environment'] == Environment and \ 219 | d['basePath'] == BasePath and \ 220 | d['revision'] != revision: 221 | print 'Undeploying revision %i in same environment and path:' % \ 222 | d['revision'] 223 | conn = httplib.HTTPSConnection(httpHost) 224 | resp = httpCall('POST', 225 | ('/v1/organizations/%s/apis/%s/deployments' + 226 | '?action=undeploy' + 227 | '&env=%s' + 228 | '&revision=%i') % \ 229 | (Organization, Name, Environment, d['revision']), 230 | None, None) 231 | if resp.status != 200 and resp.status != 204: 232 | print 'Error %i on undeployment:\n%s' % \ 233 | (resp.status, resp.read()) 234 | 235 | # Deploy the bundle 236 | print 'Deploying proxy version %i' % revision 237 | hdrs = {'Accept': 'application/json'} 238 | return httpCall('POST', 239 | ('/v1/organizations/%s/apis/%s/deployments' + 240 | '?action=deploy' + 241 | '&env=%s' + 242 | '&revision=%i' + 243 | '&basepath=%s') % \ 244 | (Organization, Name, Environment, revision, BasePath), 245 | hdrs, None) 246 | 247 | def seamLessDeployments(): 248 | # Deploy the bundle 249 | print 'Deploying proxy version %i' % revision 250 | hdrs = { 251 | 'Accept': 'application/json', 252 | 'Content-type': 'application/x-www-form-urlencoded' 253 | } 254 | return httpCall('POST', 255 | ('/v1/o/%s/environments/%s/apis/%s/revisions/%s/deployments' + 256 | '?override=true') % \ 257 | (Organization, Environment, Name, revision), 258 | hdrs, None) 259 | 260 | deployment_type = { 261 | True: seamLessDeployments, 262 | False: nonSeamLessDeployments 263 | } 264 | 265 | resp = deployment_type[seamless]() 266 | if resp.status != 200 and resp.status != 201: 267 | print 'Deploy failed with status %i:\n%s' % (resp.status, resp.read()) 268 | sys.exit(2) 269 | 270 | deps = getDeployments() 271 | printDeployments(deps) -------------------------------------------------------------------------------- /dist/instr_prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./config_vars 4 | 5 | echo "Downloading API Bundle $api revision $rev" 6 | curl -u $username:$password -o "$api-$rev.zip" "https://api.enterprise.apigee.com/v1/o/$org/apis/$api/revisions/$rev/?format=bundle" 7 | 8 | echo "Instrumenting $api bundle" 9 | java -jar apc-1.0.jar -z "$api-$rev.zip" 10 | 11 | echo "Deploying Instrumented $api bundle" 12 | ./deploy.py -n $api -u $username:$password -o $org -e $env -z $api-${rev}_instr.zip 13 | 14 | echo "Deleting Instrumented KV Data" 15 | curl -X DELETE -u $username:$password "https://api.enterprise.apigee.com/v1/o/$org/apis/$api/keyvaluemaps/instrument" 16 | echo "Note: You can ignore the last error - its trying to delete a key value map named 'instrument' if it exists." 17 | -------------------------------------------------------------------------------- /dist/instr_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./config_vars 4 | 5 | inst_txt=kvm_instrument.txt 6 | 7 | rm -fr $inst_txt 8 | 9 | echo "Downloading Recorded Instrumented Data" 10 | curl -u $username:$password -o $inst_txt "https://api.enterprise.apigee.com/v1/o/$org/apis/$api/keyvaluemaps/instrument" 11 | 12 | echo "Generating Coverage report" 13 | java -jar apc-1.0.jar -z "$api-$rev.zip" -kv $inst_txt -o $report_dir 14 | 15 | open $report_dir/summary.html& 16 | -------------------------------------------------------------------------------- /policy_exec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apigee/apigee-proxy-coverage/5fc1e23fba0238fc61e35290607d320aae310391/policy_exec.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.sriki77 8 | apc 9 | 1.0 10 | 11 | 12 | 13 | junit 14 | junit 15 | 4.11 16 | test 17 | 18 | 19 | org.mockito 20 | mockito-all 21 | 1.9.5 22 | test 23 | 24 | 25 | org.zeroturnaround 26 | zt-zip 27 | 1.13 28 | jar 29 | 30 | 31 | com.thoughtworks.xstream 32 | xstream 33 | 1.4.7 34 | 35 | 36 | commons-io 37 | commons-io 38 | 2.1 39 | 40 | 41 | xmlunit 42 | xmlunit 43 | 1.5 44 | 45 | 46 | commons-cli 47 | commons-cli 48 | 1.2 49 | 50 | 51 | com.jayway.jsonpath 52 | json-path 53 | 0.9.1 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-compiler-plugin 61 | 2.5.1 62 | 63 | 1.8 64 | 1.8 65 | 66 | 67 | 68 | maven-assembly-plugin 69 | 70 | 71 | 72 | com.github.sriki77.apiproxy.instrument.Instrumenter 73 | 74 | 75 | 76 | jar-with-dependencies 77 | 78 | false 79 | 80 | 81 | 82 | make-assembly 83 | 84 | package 85 | 86 | 87 | single 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /sample_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apigee/apigee-proxy-coverage/5fc1e23fba0238fc61e35290607d320aae310391/sample_summary.png -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/Instrumenter.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument; 2 | 3 | 4 | import com.github.sriki77.apiproxy.instrument.io.ProxyFileHandler; 5 | import com.github.sriki77.apiproxy.instrument.io.ProxyZipFileHandler; 6 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 7 | import com.github.sriki77.apiproxy.instrument.model.FlowSteps; 8 | import com.github.sriki77.apiproxy.instrument.model.Step; 9 | import com.github.sriki77.apiproxy.instrument.report.KVMapInstrumentReportGenerator; 10 | import org.apache.commons.cli.*; 11 | import org.apache.commons.io.FileUtils; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | public class Instrumenter { 21 | 22 | public static final String OPT_HELP = "help"; 23 | public static final String OPT_ZIP = "z"; 24 | public static final String OPT_DIR = "d"; 25 | public static final String OPT_KV = "kv"; 26 | public static final String OPT_REPORT_DIR = "o"; 27 | public static final File DEFAULT_REPORT_DIRECTORY = new File("report"); 28 | private ProxyFileHandler proxyFileHandler; 29 | private File kvInstrumentFile; 30 | private boolean generateReport; 31 | private File reportDirectory; 32 | 33 | public static void main(String... args) throws Exception { 34 | final Instrumenter instrumenter = new Instrumenter(); 35 | processCommandLineArgs(instrumenter, args); 36 | instrumenter.doWork(); 37 | } 38 | 39 | private void doWork() throws IOException { 40 | if (generateReport) { 41 | writeReport(); 42 | } else { 43 | instrumetProxy(); 44 | } 45 | } 46 | 47 | private void instrumetProxy() throws IOException { 48 | try (ProxyFileHandler proxyFileHandler = this.proxyFileHandler) { 49 | final List endpoints = proxyFileHandler.getEndpoints(); 50 | final ProxyInstrumeter proxyInstrumenter = getProxyInstrumenter(endpoints); 51 | final List instrumentedEndPoints = proxyInstrumenter.instrument(); 52 | instrumentedEndPoints.forEach(proxyFileHandler::updateEndpoint); 53 | } 54 | } 55 | 56 | private void writeReport() throws IOException { 57 | final List endpoints = proxyFileHandler.getEndpoints(); 58 | try (final KVMapInstrumentReportGenerator reportGenerator = new KVMapInstrumentReportGenerator(proxyFileHandler.proxyName(),kvInstrumentFile, reportDirectory)) { 59 | endpoints.forEach(reportGenerator::generateReport); 60 | } 61 | 62 | System.err.println("Generated report in directory: " + reportDirectory.getAbsolutePath()); 63 | } 64 | 65 | private int policiesCount(List endpoints) { 66 | Set policies = new HashSet<>(); 67 | endpoints.forEach(e -> { 68 | e.getFaultRules().getFaultRules().forEach(f -> policies.addAll(policies(f))); 69 | policies.addAll(policies(e.getPreflow().getRequestFlow())); 70 | policies.addAll(policies(e.getPreflow().getResponseFlow())); 71 | policies.addAll(policies(e.getPostflow().getRequestFlow())); 72 | policies.addAll(policies(e.getPostflow().getResponseFlow())); 73 | e.getFlows().getFlows().forEach(f -> { 74 | policies.addAll(policies(f.getRequestFlow())); 75 | policies.addAll(policies(f.getResponseFlow())); 76 | }); 77 | }); 78 | return policies.size(); 79 | } 80 | 81 | private Set policies(FlowSteps f) { 82 | return f.getSteps().stream().map(Step::getName).collect(Collectors.toSet()); 83 | } 84 | 85 | 86 | private static void processCommandLineArgs(Instrumenter instrumenter, String[] args) throws Exception { 87 | CommandLineParser parser = new BasicParser(); 88 | Options options = buildOptions(); 89 | processOptions(instrumenter, getCommandLine(args, parser, options), options); 90 | 91 | } 92 | 93 | private static CommandLine getCommandLine(String[] args, CommandLineParser parser, Options options) throws ParseException { 94 | try { 95 | return parser.parse(options, args); 96 | } catch (AlreadySelectedException ase) { 97 | final OptionGroup optionGroup = ase.getOptionGroup(); 98 | System.out.print("Error: Only one of "); 99 | String separator = ""; 100 | for (Object option : optionGroup.getOptions()) { 101 | Option opt = (Option) option; 102 | System.out.print(separator + "-" + opt.getOpt()); 103 | separator = ","; 104 | } 105 | System.out.println(" is allowed"); 106 | printHelpAndExit(options); 107 | } 108 | return null; 109 | } 110 | 111 | private static Options buildOptions() { 112 | Options options = new Options(); 113 | options.addOption(new Option(OPT_HELP, "Print this message")); 114 | final Option zipFileOption = OptionBuilder.withArgName("file") 115 | .hasArg().withDescription("Proxy Zip File Name") 116 | .create(OPT_ZIP); 117 | final Option dirOption = OptionBuilder.withArgName("directory") 118 | .hasArg().withDescription("Proxy Directory Name") 119 | .create(OPT_DIR); 120 | final OptionGroup optionGroup = new OptionGroup(); 121 | optionGroup.addOption(zipFileOption); 122 | optionGroup.addOption(dirOption); 123 | final Option kv = OptionBuilder.withArgName("kvm.json").hasArg() 124 | .withDescription("Instrument Key Value Map JSON file") 125 | .create(OPT_KV); 126 | final Option optReportDir = OptionBuilder.withArgName("report directory").hasArg() 127 | .withDescription("Directory for report files") 128 | .create(OPT_REPORT_DIR); 129 | options.addOptionGroup(optionGroup); 130 | options.addOption(kv); 131 | options.addOption(optReportDir); 132 | return options; 133 | } 134 | 135 | private static void processOptions(Instrumenter instrumenter, CommandLine cli, Options options) throws Exception { 136 | processReportDirectory(instrumenter, cli); 137 | if (cli.hasOption(OPT_KV)) { 138 | processKVFile(instrumenter, cli); 139 | } 140 | if (cli.hasOption(OPT_ZIP)) { 141 | processZipFile(instrumenter, cli); 142 | return; 143 | } 144 | if (cli.hasOption(OPT_DIR)) { 145 | processDir(instrumenter, cli); 146 | return; 147 | } 148 | System.err.println("Proxy zip or directory must be specified"); 149 | printHelpAndExit(options); 150 | } 151 | 152 | private static void processReportDirectory(Instrumenter instrumenter, CommandLine cli) { 153 | File reportDirectory = DEFAULT_REPORT_DIRECTORY; 154 | if (cli.hasOption(OPT_REPORT_DIR)) { 155 | final String value = cli.getOptionValue(OPT_REPORT_DIR); 156 | reportDirectory = new File(value); 157 | } 158 | FileUtils.deleteQuietly(reportDirectory); 159 | reportDirectory.mkdirs(); 160 | instrumenter.setReportDirectory(reportDirectory); 161 | } 162 | 163 | private static void processDir(Instrumenter instrumenter, CommandLine cli) throws Exception { 164 | final String value = cli.getOptionValue(OPT_DIR); 165 | File file = new File(value); 166 | if (!file.exists()) { 167 | System.err.println("Specified proxy directory not found: " + file); 168 | System.exit(-1); 169 | } 170 | if (!file.isDirectory()) { 171 | System.err.println("Specified value is not directory: " + file); 172 | System.exit(-1); 173 | } 174 | if (!file.canRead() || !file.canWrite()) { 175 | System.err.println("Specified proxy directory should be readable and writeable: " + file); 176 | System.exit(-1); 177 | } 178 | instrumenter.setProxyDirectory(file); 179 | } 180 | 181 | private void setProxyDirectory(File file) throws Exception { 182 | proxyFileHandler = new ProxyZipFileHandler(file); 183 | } 184 | 185 | private static void processKVFile(Instrumenter instrumenter, CommandLine cli) throws Exception { 186 | final String value = cli.getOptionValue(OPT_KV); 187 | File file = new File(value); 188 | if (!file.exists()) { 189 | System.err.println("Specified Key Value Map file not found: " + file); 190 | System.exit(-1); 191 | } 192 | if (file.isDirectory()) { 193 | System.err.println("Specified file is not a plain JSON file: " + file); 194 | System.exit(-1); 195 | } 196 | if (!file.canRead()) { 197 | System.err.println("Specified Key Value map file should be readable: " + file); 198 | System.exit(-1); 199 | } 200 | instrumenter.setKVFile(file); 201 | } 202 | 203 | private void setKVFile(File kvfile) { 204 | this.kvInstrumentFile = kvfile; 205 | this.generateReport = true; 206 | } 207 | 208 | private static void processZipFile(Instrumenter instrumenter, CommandLine cli) throws Exception { 209 | final String value = cli.getOptionValue(OPT_ZIP); 210 | File file = new File(value); 211 | if (!file.exists()) { 212 | System.err.println("Specified proxy file not found: " + file); 213 | System.exit(-1); 214 | } 215 | if (file.isDirectory()) { 216 | System.err.println("Specified file is not a zip file: " + file); 217 | System.exit(-1); 218 | } 219 | if (!file.canRead() || !file.canWrite()) { 220 | System.err.println("Specified proxy file should be readable and writeable: " + file); 221 | System.exit(-1); 222 | } 223 | instrumenter.setProxyFile(file); 224 | } 225 | 226 | private void setProxyFile(File file) throws Exception { 227 | proxyFileHandler = new ProxyZipFileHandler(file); 228 | } 229 | 230 | private static void printHelpAndExit(Options options) { 231 | System.out.println(); 232 | final HelpFormatter formatter = new HelpFormatter(); 233 | formatter.printHelp("java " + Instrumenter.class.getName(), options); 234 | System.exit(-1); 235 | } 236 | 237 | public ProxyInstrumeter getProxyInstrumenter(List endpoints) { 238 | return new KVMapBasedProxyInstrumenter(endpoints); 239 | } 240 | 241 | public void setReportDirectory(File reportDirectory) { 242 | this.reportDirectory = reportDirectory; 243 | } 244 | 245 | } 246 | 247 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/KVMapBasedProxyInstrumenter.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 4 | import com.github.sriki77.apiproxy.instrument.model.FlowSteps; 5 | import com.github.sriki77.apiproxy.instrument.model.PolicyUpdate; 6 | import com.github.sriki77.apiproxy.instrument.model.Step; 7 | import org.apache.commons.io.IOUtils; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class KVMapBasedProxyInstrumenter implements ProxyInstrumeter { 14 | 15 | private final List endpoints; 16 | 17 | public KVMapBasedProxyInstrumenter(List endpoints) { 18 | this.endpoints = endpoints; 19 | } 20 | 21 | @Override 22 | public List instrument() { 23 | endpoints.forEach(this::instrument); 24 | return endpoints; 25 | } 26 | 27 | private void instrument(Endpoint e) { 28 | e.getFaultRules().getFaultRules().forEach(f -> instrument(f, e)); 29 | instrument(e.getPreflow().getRequestFlow(), e); 30 | instrument(e.getPreflow().getResponseFlow(), e); 31 | instrument(e.getPostflow().getRequestFlow(), e); 32 | instrument(e.getPostflow().getResponseFlow(), e); 33 | e.getFlows().getFlows().forEach(f -> { 34 | instrument(f.getRequestFlow(), e); 35 | instrument(f.getResponseFlow(), e); 36 | }); 37 | } 38 | 39 | private void instrument(FlowSteps f, Endpoint e) { 40 | new ArrayList<>(f.getSteps()).forEach(s -> instrument(f, s, e)); 41 | } 42 | 43 | private void instrument(FlowSteps f, Step s, Endpoint e) { 44 | final Step step = f.cloneStep(s); 45 | try { 46 | String template = getStepTemplate(); 47 | final String policyData = step.initUsingTemplate(template,step.getName()); 48 | e.addUpdate(new PolicyUpdate(step.getName(), policyData)); 49 | } catch (IOException ioe) { 50 | throw new RuntimeException(ioe); 51 | } 52 | } 53 | 54 | private String getStepTemplate() throws IOException { 55 | return IOUtils.toString(this.getClass().getResourceAsStream("/kv_instr_template.xml")); 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/ProxyInstrumeter.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 4 | 5 | public interface ProxyInstrumeter { 6 | java.util.List instrument(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/NodeCleaner.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | import org.w3c.dom.Node; 4 | import org.w3c.dom.NodeList; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | public enum NodeCleaner { 11 | 12 | PROXY_ENDPOINT_NODES("ProxyEndpoint", "FaultRules", "PreFlow", "PostFlow", "Flows"), 13 | TARGET_ENDPOINT_NODES("TargetEndpoint", "FaultRules", "PreFlow", "PostFlow", "Flows"), 14 | FLOWS_NODES("Flows", "Flow"), 15 | FLOW_NODES("Flow", "Request", "Response", "Condition"), 16 | REQ_NODES("Request", "Step"), 17 | RES_NODES("Response", "Step"), 18 | STEP_NODES("Step", "Name", "Condition"), 19 | FAULT_NODES("FaultRules", "FaultRule"), 20 | FAULT_RULE_NODES("FaultRule", "Step"); 21 | 22 | private final List knownNodes; 23 | private final String name; 24 | 25 | private NodeCleaner(String name, String... knownNodes) { 26 | this.name = name; 27 | this.knownNodes = Arrays.asList(knownNodes); 28 | } 29 | 30 | public void cleanNode(Node node) { 31 | final NodeList topChildren = node.getChildNodes(); 32 | List nodesToBeRemoved = new ArrayList<>(); 33 | for (int i = 0; i < topChildren.getLength(); i++) { 34 | final Node n = topChildren.item(i); 35 | if (!knownNodes.contains(n.getNodeName())) { 36 | nodesToBeRemoved.add(n); 37 | } 38 | } 39 | nodesToBeRemoved.stream().forEach(node::removeChild); 40 | } 41 | 42 | private static NodeCleaner toCleaner(Node node) { 43 | final NodeCleaner[] nodeCleaners = values(); 44 | for (int i = 0; i < nodeCleaners.length; i++) { 45 | NodeCleaner nodeCleaner = nodeCleaners[i]; 46 | if (nodeCleaner.name.equals(node.getNodeName())) { 47 | return nodeCleaner; 48 | } 49 | } 50 | return null; 51 | } 52 | 53 | public static void clean(Node n) { 54 | final NodeCleaner nodeCleaner = toCleaner(n); 55 | if (nodeCleaner != null) { 56 | nodeCleaner.cleanNode(n); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/PolicyUpdater.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | @FunctionalInterface 4 | public interface PolicyUpdater { 5 | void updatePolicy(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/ProxyDirectoryHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 4 | import com.github.sriki77.apiproxy.instrument.model.PolicyUpdate; 5 | import com.thoughtworks.xstream.XStream; 6 | import org.apache.commons.io.FileUtils; 7 | import org.w3c.dom.Document; 8 | import org.w3c.dom.Node; 9 | import org.w3c.dom.NodeList; 10 | import org.xml.sax.SAXException; 11 | 12 | import javax.xml.parsers.DocumentBuilder; 13 | import javax.xml.parsers.DocumentBuilderFactory; 14 | import javax.xml.parsers.ParserConfigurationException; 15 | import javax.xml.transform.Transformer; 16 | import javax.xml.transform.TransformerConfigurationException; 17 | import javax.xml.transform.TransformerException; 18 | import javax.xml.transform.TransformerFactory; 19 | import javax.xml.transform.dom.DOMSource; 20 | import javax.xml.transform.stream.StreamResult; 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.io.StringWriter; 24 | import java.util.Arrays; 25 | import java.util.List; 26 | import java.util.stream.Collectors; 27 | import java.util.stream.Stream; 28 | 29 | import static com.github.sriki77.apiproxy.instrument.io.Util.xStreamInit; 30 | 31 | public class ProxyDirectoryHandler implements ProxyFileHandler { 32 | private File apiProxyDir; 33 | private File proxyFilesDir; 34 | private File targetFilesDir; 35 | private XStream xStream; 36 | private DocumentBuilder builder; 37 | private Transformer transformer; 38 | protected File proxyDir; 39 | private File policyDir; 40 | private String proxyName; 41 | 42 | public ProxyDirectoryHandler(File proxyDir) throws IOException, ParserConfigurationException, TransformerConfigurationException { 43 | initProxyRelatedDirectories(proxyDir); 44 | initXMLInfra(); 45 | determineProxyName(); 46 | } 47 | 48 | private void determineProxyName() { 49 | final File[] files = apiProxyDir.listFiles((dir, name) -> name.endsWith(".xml")); 50 | if (files.length == 0) { 51 | return; 52 | } 53 | try { 54 | final Document document = builder.parse(files[0]); 55 | final Node apiProxy = document.getElementsByTagName("APIProxy").item(0); 56 | proxyName = apiProxy.getAttributes().getNamedItem("name").getNodeValue(); 57 | } catch (Exception e) { 58 | throw new RuntimeException("Failed to parse file: " + files[0].getAbsolutePath(), e); 59 | } 60 | } 61 | 62 | private void initXMLInfra() throws ParserConfigurationException, TransformerConfigurationException { 63 | xStream = xStreamInit(); 64 | builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 65 | transformer = TransformerFactory.newInstance().newTransformer(); 66 | } 67 | 68 | private void initProxyRelatedDirectories(File proxyDir) { 69 | this.proxyDir = proxyDir; 70 | apiProxyDir = apiProxyDir(proxyDir); 71 | proxyFilesDir = getDirNamed("proxies"); 72 | targetFilesDir = getDirNamed("targets"); 73 | policyDir = getDirNamed("policies"); 74 | } 75 | 76 | 77 | private File getDirNamed(String dirName) { 78 | if (apiProxyDir == null) { 79 | return null; 80 | } 81 | final File proxies = new File(apiProxyDir, dirName); 82 | if (!proxies.exists()) { 83 | return null; 84 | } 85 | return proxies; 86 | } 87 | 88 | private File apiProxyDir(File proxyDir) { 89 | final File apiproxy = new File(proxyDir, "apiproxy"); 90 | if (!apiproxy.exists()) { 91 | return null; 92 | } 93 | return apiproxy; 94 | } 95 | 96 | @Override 97 | public List getEndpoints() { 98 | final List endpoints = getProxyFiles().map(this::toEndpoint).collect(Collectors.toList()); 99 | if (endpoints.isEmpty()) { 100 | System.err.println("Warning!! No endpoints found."); 101 | } 102 | return endpoints; 103 | } 104 | 105 | private Endpoint toEndpoint(File file) { 106 | try { 107 | final Document cleanedDocument = cleanupProxyFile(file); 108 | final Endpoint endpoint = (Endpoint) xStream.fromXML(toString(cleanedDocument)); 109 | endpoint.init(file, builder.parse(file)); 110 | return endpoint; 111 | } catch (Exception e) { 112 | System.err.println("Failed Processing File: " + file); 113 | throw new RuntimeException(e); 114 | } 115 | } 116 | 117 | private String toString(Document cleanedDocument) throws TransformerException { 118 | final StringWriter cleanedXml = new StringWriter(); 119 | transformer.transform(new DOMSource(cleanedDocument), new StreamResult(cleanedXml)); 120 | return cleanedXml.toString(); 121 | } 122 | 123 | private Document cleanupProxyFile(File file) throws IOException, SAXException, TransformerException { 124 | final Document document = builder.parse(file); 125 | cleanupNode(document); 126 | return document; 127 | } 128 | 129 | private void cleanupNode(Node node) { 130 | final NodeList topChildren = node.getChildNodes(); 131 | for (int i = 0; i < topChildren.getLength(); i++) { 132 | final Node n = topChildren.item(i); 133 | NodeCleaner.clean(n); 134 | cleanupNode(n); 135 | } 136 | } 137 | 138 | Stream getProxyFiles() { 139 | return Stream.concat(getFilesFrom(proxyFilesDir), getFilesFrom(targetFilesDir)); 140 | } 141 | 142 | private Stream getFilesFrom(File filesDir) { 143 | if (filesDir == null) { 144 | return Stream.empty(); 145 | } 146 | return Arrays.stream(filesDir.listFiles((dir, name) -> name.endsWith(".xml"))); 147 | } 148 | 149 | @Override 150 | public void updateEndpoint(Endpoint endpoint) { 151 | try { 152 | transformer.transform(new DOMSource(endpoint.getNode()), new StreamResult(endpoint.getXmlFile())); 153 | } catch (TransformerException e) { 154 | throw new RuntimeException(e); 155 | } 156 | final List updates = endpoint.updates(); 157 | updates.forEach(this::createFile); 158 | } 159 | 160 | @Override 161 | public String proxyName() { 162 | return proxyName; 163 | } 164 | 165 | private void createFile(PolicyUpdate u) { 166 | try { 167 | final File policyName = new File(policyDir, u.name + ".xml"); 168 | FileUtils.writeStringToFile(policyName, u.policyData); 169 | } catch (IOException e) { 170 | throw new RuntimeException(e); 171 | } 172 | } 173 | 174 | @Override 175 | public void close() throws IOException { 176 | 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/ProxyFileHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 4 | 5 | import java.io.Closeable; 6 | 7 | public interface ProxyFileHandler extends Closeable { 8 | 9 | java.util.List getEndpoints(); 10 | 11 | void updateEndpoint(Endpoint endpoint); 12 | 13 | String proxyName(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/ProxyZipFileHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.apache.commons.io.FilenameUtils; 5 | import org.zeroturnaround.zip.ZipUtil; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | 12 | public class ProxyZipFileHandler extends ProxyDirectoryHandler { 13 | private File zipFile; 14 | 15 | public ProxyZipFileHandler(File zipFile) throws Exception { 16 | super(expandZipFile(zipFile)); 17 | this.zipFile = zipFile; 18 | } 19 | 20 | static File expandZipFile(File zipFile) throws IOException { 21 | final Path tempDirectory = Files.createTempDirectory(zipFile.getName()); 22 | ZipUtil.unpack(zipFile, tempDirectory.toFile()); 23 | return tempDirectory.toFile(); 24 | } 25 | 26 | public File buildInstrumentedZipFile() { 27 | final File parentDir = zipFile.getParentFile(); 28 | final File targetZipFile = targetZipFile(parentDir); 29 | FileUtils.deleteQuietly(targetZipFile); 30 | ZipUtil.pack(proxyDir, targetZipFile); 31 | return targetZipFile; 32 | } 33 | 34 | private File targetZipFile(File parentDir) { 35 | final String origName = zipFile.getName(); 36 | final String newName = FilenameUtils.getBaseName(origName) + "_instr." + 37 | FilenameUtils.getExtension(origName); 38 | return new File(parentDir, newName); 39 | } 40 | 41 | @Override 42 | public void close() throws IOException { 43 | super.close(); 44 | File instrZipFile = buildInstrumentedZipFile(); 45 | System.err.println("Instrument File generated: " + instrZipFile); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/io/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.io; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.*; 4 | import com.thoughtworks.xstream.XStream; 5 | import com.thoughtworks.xstream.io.xml.StaxDriver; 6 | 7 | public final class Util { 8 | private Util() { 9 | } 10 | 11 | public static XStream xStreamInit() { 12 | final XStream stream = new XStream(new StaxDriver()); 13 | stream.processAnnotations(new Class[]{ProxyEndpoint.class, 14 | Endpoint.class, FaultRule.class, 15 | FaultRules.class, Flow.class, FlowSteps.class, 16 | RequestFlow.class, ResponseFlow.class, Step.class, 17 | TargetEndpoint.class}); 18 | return stream; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/DOMStep.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import org.w3c.dom.Node; 4 | 5 | import java.util.Optional; 6 | 7 | public class DOMStep extends Step { 8 | 9 | private Step inner; 10 | private Optional nameNode; 11 | private Optional condNode; 12 | 13 | public DOMStep(Step inner, Optional nameNode, Optional condNode) { 14 | super(inner.name,inner.condition,inner.parent); 15 | this.inner = inner; 16 | this.nameNode = nameNode; 17 | this.condNode = condNode; 18 | this.baseName= inner.baseName; 19 | setName(inner.name); 20 | setCondition(inner.condition); 21 | } 22 | 23 | 24 | @Override 25 | public void setName(String name) { 26 | if(name==null){ 27 | return; 28 | } 29 | validateNodePresence(nameNode); 30 | super.setName(name); 31 | inner.setName(name); 32 | updateNode(nameNode, name); 33 | 34 | } 35 | 36 | private void validateNodePresence(Optional node) { 37 | if (!node.isPresent()) { 38 | throw new RuntimeException("Name/Condition node not present for original step. You cannot add a new one to the clone."); 39 | } 40 | } 41 | 42 | private void updateNode(Optional node, String value) { 43 | if (node.isPresent()) { 44 | node.get().setTextContent(value); 45 | } 46 | } 47 | 48 | @Override 49 | public void setCondition(String condition) { 50 | if(condition==null){ 51 | return; 52 | } 53 | validateNodePresence(condNode); 54 | super.setCondition(condition); 55 | inner.setCondition(condition); 56 | updateNode(condNode, condition); 57 | } 58 | 59 | @Override 60 | public Step duplicate() { 61 | throw new RuntimeException("Duplication of DOM Step not supported"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/Endpoint.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamAsAttribute; 5 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 6 | import org.w3c.dom.Document; 7 | import org.w3c.dom.Node; 8 | 9 | import java.io.File; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public abstract class Endpoint implements NodeHolder, LocationProvider { 14 | 15 | @XStreamAlias("FaultRules") 16 | protected FaultRules faultRules; 17 | 18 | @XStreamAlias("PreFlow") 19 | protected PreFlow preflow; 20 | 21 | @XStreamAlias("Flows") 22 | protected Flows flows; 23 | 24 | @XStreamAlias("PostFlow") 25 | protected PostFlow postflow; 26 | 27 | @XStreamAlias("name") 28 | @XStreamAsAttribute 29 | private String name; 30 | 31 | 32 | @XStreamOmitField 33 | private File xmlFile; 34 | 35 | @XStreamOmitField 36 | private Node node; 37 | 38 | @XStreamOmitField 39 | private List updates; 40 | 41 | public File getXmlFile() { 42 | return xmlFile; 43 | } 44 | 45 | @Override 46 | public void holdNode(Node node) { 47 | this.node = node; 48 | NodeHolder.holdNode(faultRules, NodeHolder.findMyselfUsingXpath(node, "//FaultRules")); 49 | NodeHolder.holdNode(preflow, NodeHolder.findMyselfUsingXpath(node, "//PreFlow")); 50 | NodeHolder.holdNode(postflow, NodeHolder.findMyselfUsingXpath(node, "//PostFlow")); 51 | NodeHolder.holdNode(flows, NodeHolder.findMyselfUsingXpath(node, "//Flows")); 52 | } 53 | 54 | public FaultRules getFaultRules() { 55 | return faultRules == null ? new FaultRules() : faultRules; 56 | } 57 | 58 | public PreFlow getPreflow() { 59 | return preflow == null ? new PreFlow() : preflow; 60 | } 61 | 62 | public Flows getFlows() { 63 | return flows == null ? new Flows() : flows; 64 | } 65 | 66 | public PostFlow getPostflow() { 67 | return postflow == null ? new PostFlow() : postflow; 68 | } 69 | 70 | public Node getNode() { 71 | return node; 72 | } 73 | 74 | @Override 75 | public String location() { 76 | return endpointType() + ":" + name + ",File:" + xmlFile.getName(); 77 | } 78 | 79 | 80 | public abstract String endpointType(); 81 | 82 | public void init(File xmlFile, Document node) { 83 | this.xmlFile = xmlFile; 84 | holdNode(node); 85 | setParent(this); 86 | } 87 | 88 | @Override 89 | public void setParent(LocationProvider parent) { 90 | LocationProvider.setParent(faultRules, parent); 91 | LocationProvider.setParent(preflow, parent); 92 | LocationProvider.setParent(postflow, parent); 93 | LocationProvider.setParent(flows, parent); 94 | } 95 | 96 | public void addUpdate(PolicyUpdate update) { 97 | if (updates == null) { 98 | this.updates = new ArrayList<>(); 99 | } 100 | this.updates.add(update); 101 | } 102 | 103 | public List updates() { 104 | return updates == null ? new ArrayList<>() : updates; 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "Endpoint{" + 110 | "name='" + name + '\'' + 111 | '}'; 112 | } 113 | 114 | public String getName() { 115 | return name; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/FaultRule.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamAsAttribute; 5 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 6 | import org.w3c.dom.Node; 7 | 8 | @XStreamAlias("FaultRule") 9 | public class FaultRule extends FlowSteps { 10 | @XStreamAlias("name") 11 | @XStreamAsAttribute 12 | private String name; 13 | 14 | @XStreamOmitField 15 | private Node node; 16 | 17 | @Override 18 | public void holdNode(Node node) { 19 | this.node = NodeHolder.findMyselfUsingXpath(node, String.format("//FaultRule[@name='%s']", name)); 20 | } 21 | 22 | 23 | @Override 24 | protected Node getDOMNode() { 25 | return node; 26 | } 27 | 28 | @Override 29 | public String location() { 30 | return LocationProvider.append(parent, "FaultRule: " + name); 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/FaultRules.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | 4 | import com.thoughtworks.xstream.annotations.XStreamAlias; 5 | import com.thoughtworks.xstream.annotations.XStreamImplicit; 6 | import org.w3c.dom.Node; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | @XStreamAlias("FaultRules") 12 | public class FaultRules implements NodeHolder, LocationProvider { 13 | 14 | @XStreamImplicit(itemFieldName = "FaultRule") 15 | private List faultRules; 16 | 17 | @Override 18 | public void holdNode(Node node) { 19 | NodeHolder.holdNodes(faultRules, node); 20 | } 21 | 22 | public List getFaultRules() { 23 | return faultRules == null ? Collections.emptyList() : Collections.unmodifiableList(faultRules); 24 | } 25 | 26 | 27 | @Override 28 | public void setParent(LocationProvider parent) { 29 | LocationProvider.setParent(faultRules, parent); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/Flow.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamAsAttribute; 5 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 6 | import org.w3c.dom.Node; 7 | 8 | @XStreamAlias("Flow") 9 | public class Flow implements NodeHolder, LocationProvider { 10 | 11 | @XStreamAlias("Request") 12 | protected RequestFlow requestFlow; 13 | 14 | @XStreamAlias("Response") 15 | protected ResponseFlow responseFlow; 16 | 17 | @XStreamAlias("Condition") 18 | protected String condition; 19 | 20 | @XStreamAlias("name") 21 | @XStreamAsAttribute 22 | private String name; 23 | 24 | @XStreamOmitField 25 | protected LocationProvider parent; 26 | 27 | @Override 28 | public void holdNode(Node node) { 29 | NodeHolder.holdNode(requestFlow, NodeHolder.findMyselfUsingXpath(node, getReqNodeXPath())); 30 | NodeHolder.holdNode(responseFlow, NodeHolder.findMyselfUsingXpath(node, getResNodeXPath())); 31 | } 32 | 33 | protected String getReqNodeXPath() { 34 | return String.format("//Flow[@name='%s']/Request", name); 35 | } 36 | 37 | protected String getResNodeXPath() { 38 | return String.format("//Flow[@name='%s']/Response", name); 39 | } 40 | 41 | public RequestFlow getRequestFlow() { 42 | return requestFlow == null ? new RequestFlow() : requestFlow; 43 | } 44 | 45 | public ResponseFlow getResponseFlow() { 46 | return responseFlow == null ? new ResponseFlow() : responseFlow; 47 | } 48 | 49 | @Override 50 | public void setParent(LocationProvider parent) { 51 | this.parent = parent; 52 | LocationProvider.setParent(requestFlow, this); 53 | LocationProvider.setParent(responseFlow, this); 54 | } 55 | 56 | @Override 57 | public String location() { 58 | return LocationProvider.append(parent, name); 59 | } 60 | 61 | public String getName() { 62 | return name; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/FlowSteps.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamImplicit; 4 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 5 | import org.w3c.dom.Node; 6 | import org.w3c.dom.NodeList; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public abstract class FlowSteps implements NodeHolder, LocationProvider { 14 | 15 | @XStreamImplicit(itemFieldName = "Step") 16 | protected List steps; 17 | 18 | @XStreamOmitField 19 | protected LocationProvider parent; 20 | 21 | @Override 22 | public String toString() { 23 | return "FlowSteps{" + 24 | "steps=" + steps + 25 | '}'; 26 | } 27 | 28 | public List getSteps() { 29 | return steps == null ? Collections.emptyList() : Collections.unmodifiableList(steps); 30 | } 31 | 32 | public Step cloneStep(Step step) { 33 | final int i = steps.indexOf(step); 34 | if (i == -1) { 35 | throw new RuntimeException("Step not found: " + step); 36 | } 37 | Step copy = step.duplicate(); 38 | steps.add(i, copy); 39 | final List nodes = updateNode(i); 40 | return new DOMStep(copy, getNameNode(nodes), getCondNode(nodes)); 41 | } 42 | 43 | protected List updateNode(int index) { 44 | final Node domNode = getDOMNode(); 45 | NodeList childNodes = domNode.getChildNodes(); 46 | final Node origNode = getNthStep(childNodes, index); 47 | final Node cloneNode = origNode.cloneNode(true); 48 | final List nodes = usefulNodes(cloneNode); 49 | if (nodes.size() < 1 || nodes.size() > 2) { 50 | throw new RuntimeException("Step nodes with only Name and Condition elements are supported"); 51 | } 52 | domNode.insertBefore(cloneNode, origNode); 53 | return nodes; 54 | } 55 | 56 | private List usefulNodes(Node cloneNode) { 57 | List usefulNodes = new ArrayList<>(); 58 | final NodeList childNodes = cloneNode.getChildNodes(); 59 | for (int i = 0; i < childNodes.getLength(); i++) { 60 | final Node item = childNodes.item(i); 61 | if (item.getNodeName().equalsIgnoreCase("Name") 62 | || item.getNodeName().equalsIgnoreCase("Condition")) { 63 | usefulNodes.add(item); 64 | } 65 | } 66 | return usefulNodes; 67 | } 68 | 69 | private Optional getNameNode(List nodes) { 70 | return nodes.stream().filter(n -> n.getNodeName().equalsIgnoreCase("Name")).findFirst(); 71 | } 72 | 73 | private Optional getCondNode(List nodes) { 74 | return nodes.stream().filter(n -> n.getNodeName().equalsIgnoreCase("Condition")).findFirst(); 75 | } 76 | 77 | 78 | private Node getNthStep(NodeList childNodes, int index) { 79 | for (int i = 0, j = 0; i < childNodes.getLength(); i++) { 80 | final Node item = childNodes.item(i); 81 | if (item.getNodeName().equalsIgnoreCase("Step")) { 82 | if (index == j) { 83 | return item; 84 | } 85 | ++j; 86 | } 87 | } 88 | throw new RuntimeException(index + "th step node not found"); 89 | } 90 | 91 | 92 | @Override 93 | public void setParent(LocationProvider parent) { 94 | this.parent = parent; 95 | LocationProvider.setParent(steps, this); 96 | } 97 | 98 | protected abstract Node getDOMNode(); 99 | 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/Flows.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamImplicit; 5 | import org.w3c.dom.Node; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | @XStreamAlias("Flows") 11 | public class Flows implements NodeHolder, LocationProvider { 12 | 13 | @XStreamImplicit(itemFieldName = "Flow") 14 | protected List flows ; 15 | 16 | @Override 17 | public void holdNode(Node node) { 18 | NodeHolder.holdNodes(flows, node); 19 | } 20 | 21 | 22 | @Override 23 | public void setParent(LocationProvider parent) { 24 | LocationProvider.setParent(flows, parent); 25 | } 26 | 27 | public List getFlows() { 28 | return flows == null ? Collections.emptyList() : Collections.unmodifiableList(flows); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/LocationProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import java.util.List; 4 | 5 | import static com.github.sriki77.apiproxy.instrument.model.LocationProvider.LOC_PARTS.*; 6 | 7 | 8 | public interface LocationProvider { 9 | 10 | default String location(){ 11 | return ""; 12 | } 13 | 14 | void setParent(LocationProvider parent); 15 | 16 | enum LOC_PARTS {ENDPOINT, PROXY, FLOW, POLICY} 17 | 18 | static String proxyFileName(LocationProvider provider) { 19 | final String[] splits = provider.location().split(","); 20 | return splits[PROXY.ordinal()]; 21 | } 22 | 23 | static String endpointName(LocationProvider provider) { 24 | final String[] splits = provider.location().split(","); 25 | return splits[ENDPOINT.ordinal()]; 26 | 27 | } 28 | 29 | static String flowName(LocationProvider provider) { 30 | final String[] splits = provider.location().split(","); 31 | return splits[FLOW.ordinal()]; 32 | 33 | } 34 | 35 | static String policyName(LocationProvider provider) { 36 | final String[] splits = provider.location().split(","); 37 | return splits[POLICY.ordinal()]; 38 | 39 | } 40 | 41 | static String append(LocationProvider parent, String loc) { 42 | return parent.location() + "," + loc; 43 | } 44 | 45 | static void setParent(List providers, LocationProvider parent) { 46 | if (providers != null) { 47 | providers.forEach(p -> setParent(p, parent)); 48 | } 49 | } 50 | 51 | static void setParent(LocationProvider provider, LocationProvider parent) { 52 | if (provider != null) { 53 | provider.setParent(parent); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/NodeHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import org.w3c.dom.Node; 4 | 5 | import javax.xml.xpath.XPath; 6 | import javax.xml.xpath.XPathConstants; 7 | import javax.xml.xpath.XPathExpressionException; 8 | import javax.xml.xpath.XPathFactory; 9 | import java.util.List; 10 | 11 | @FunctionalInterface 12 | public interface NodeHolder { 13 | void holdNode(Node node); 14 | 15 | static void holdNodes(List nodeHolders, Node node) { 16 | if (nodeHolders != null) { 17 | nodeHolders.forEach(d -> holdNode(d, node)); 18 | } 19 | } 20 | 21 | static void holdNode(NodeHolder holder, Node node) { 22 | if (holder != null) { 23 | holder.holdNode(node); 24 | } 25 | } 26 | 27 | static Node findMyselfUsingXpath(Node node, String path) { 28 | final XPath xPath = XPathFactory.newInstance().newXPath(); 29 | try { 30 | return (Node) xPath.evaluate(path, node, XPathConstants.NODE); 31 | } catch (XPathExpressionException e) { 32 | throw new RuntimeException(e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/PolicyUpdate.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | public class PolicyUpdate { 4 | public final String name; 5 | public final String policyData; 6 | 7 | public PolicyUpdate(String name, String policyData) { 8 | this.name = name; 9 | this.policyData = policyData; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/PostFlow.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | @XStreamAlias("PostFlow") 6 | public class PostFlow extends Flow { 7 | @Override 8 | protected String getReqNodeXPath() { 9 | return "//PostFlow/Request"; 10 | } 11 | 12 | @Override 13 | protected String getResNodeXPath() { 14 | return "//PostFlow/Response"; 15 | } 16 | 17 | @Override 18 | public String location() { 19 | return LocationProvider.append(parent, "PostFlow"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/PreFlow.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | @XStreamAlias("PreFlow") 6 | public class PreFlow extends Flow { 7 | 8 | @Override 9 | protected String getReqNodeXPath() { 10 | return "//PreFlow/Request"; 11 | } 12 | 13 | @Override 14 | protected String getResNodeXPath() { 15 | return "//PreFlow/Response"; 16 | } 17 | 18 | @Override 19 | public String location() { 20 | return LocationProvider.append(parent, "PreFlow"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/ProxyEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | @XStreamAlias("ProxyEndpoint") 6 | public class ProxyEndpoint extends Endpoint { 7 | 8 | @Override 9 | public String endpointType() { 10 | return "Proxy"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/RequestFlow.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 5 | import org.w3c.dom.Node; 6 | 7 | @XStreamAlias("Request") 8 | public class RequestFlow extends FlowSteps { 9 | 10 | @XStreamOmitField 11 | private Node node; 12 | 13 | @Override 14 | public void holdNode(Node node) { 15 | this.node = node; 16 | } 17 | 18 | @Override 19 | protected Node getDOMNode() { 20 | return node; 21 | } 22 | 23 | @Override 24 | public String location() { 25 | return parent.location()+":RequestFlow"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/ResponseFlow.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 5 | import org.w3c.dom.Node; 6 | 7 | @XStreamAlias("Response") 8 | public class ResponseFlow extends FlowSteps { 9 | 10 | @XStreamOmitField 11 | private Node node; 12 | 13 | @Override 14 | public void holdNode(Node node) { 15 | this.node = node; 16 | } 17 | 18 | @Override 19 | protected Node getDOMNode() { 20 | return node; 21 | } 22 | 23 | @Override 24 | public String location() { 25 | return parent.location()+":ResponseFlow"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/Step.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | import com.thoughtworks.xstream.annotations.XStreamAsAttribute; 5 | import com.thoughtworks.xstream.annotations.XStreamOmitField; 6 | 7 | @XStreamAlias("Step") 8 | public class Step implements LocationProvider { 9 | 10 | @XStreamAlias("Name") 11 | protected String name; 12 | 13 | @XStreamAlias("Condition") 14 | protected String condition; 15 | 16 | @XStreamAlias("executed") 17 | @XStreamAsAttribute 18 | protected boolean executed = true; 19 | 20 | @XStreamOmitField 21 | protected String baseName; 22 | 23 | @XStreamOmitField 24 | protected LocationProvider parent; 25 | 26 | protected Step(String name, String condition, LocationProvider parent) { 27 | this.name = name; 28 | this.condition = escapeCondition(condition); 29 | this.parent = parent; 30 | } 31 | 32 | private String escapeCondition(String condition) { 33 | return condition==null? null:condition.replace("<","<").replace(">",">"); 34 | } 35 | 36 | public void setName(String name) { 37 | this.name = name; 38 | } 39 | 40 | public void setCondition(String condition) { 41 | this.condition = condition; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "Step{" + 47 | "name='" + name + '\'' + 48 | ", condition='" + condition + '\'' + 49 | '}'; 50 | } 51 | 52 | 53 | public Step duplicate() { 54 | final Step copy = new Step(name + System.currentTimeMillis(), condition, parent); 55 | copy.baseName = name; 56 | return copy; 57 | } 58 | 59 | @Override 60 | public void setParent(LocationProvider parent) { 61 | this.parent = parent; 62 | } 63 | 64 | @Override 65 | public String location() { 66 | String loc = "Policy: " + name; 67 | if (condition != null) { 68 | loc += " Condition: " + condition; 69 | } 70 | return LocationProvider.append(parent, loc); 71 | } 72 | 73 | 74 | public String policyNameAndCondition() { 75 | String loc = "Policy: " + baseName; 76 | if (condition != null) { 77 | loc += " Condition: " + condition; 78 | } 79 | return loc; 80 | } 81 | 82 | public String initUsingTemplate(String template, String name) { 83 | return String.format(template, name, LocationProvider.endpointName(this), 84 | LocationProvider.proxyFileName(this), LocationProvider.flowName(this), policyNameAndCondition()); 85 | } 86 | 87 | public String getName() { 88 | return name; 89 | } 90 | 91 | public void setExecuted(boolean executed) { 92 | this.executed = executed; 93 | } 94 | 95 | public boolean isExecuted() { 96 | return executed; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/model/TargetEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.model; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | @XStreamAlias("TargetEndpoint") 6 | public class TargetEndpoint extends Endpoint { 7 | 8 | @Override 9 | public String endpointType() { 10 | return "Target"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/EndpointStat.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @XStreamAlias("EndpointStat") 9 | public class EndpointStat extends Stats { 10 | protected List stats = new ArrayList<>(); 11 | public String endpointType; 12 | 13 | 14 | @Override 15 | protected void calcCoverage() { 16 | stats.forEach(f -> f.calcCoverage()); 17 | super.calcCoverage(); 18 | } 19 | 20 | 21 | public void add(FlowStat flowStat) { 22 | this.stats.add(flowStat); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/FlowStat.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Step; 4 | import com.thoughtworks.xstream.annotations.XStreamAlias; 5 | 6 | import java.util.List; 7 | 8 | @XStreamAlias("FlowStat") 9 | public class FlowStat extends Stats { 10 | public String flowType; 11 | 12 | protected FlowStat(String flowType,String name, List steps) { 13 | this.flowType = flowType; 14 | this.name = name; 15 | updateStats(steps); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/InstrumentReportGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 4 | 5 | public interface InstrumentReportGenerator { 6 | void generateReport(Endpoint endpoint); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/InstrumentResultMap.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import java.util.List; 4 | 5 | public class InstrumentResultMap { 6 | private List values; 7 | public InstrumentResultMap(List values) { 8 | this.values = values; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/KVMapInstrumentReportGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.github.sriki77.apiproxy.instrument.io.Util; 4 | import com.github.sriki77.apiproxy.instrument.model.Endpoint; 5 | import com.github.sriki77.apiproxy.instrument.model.FaultRule; 6 | import com.github.sriki77.apiproxy.instrument.model.Flow; 7 | import com.github.sriki77.apiproxy.instrument.model.Step; 8 | import com.jayway.jsonpath.JsonPath; 9 | import com.thoughtworks.xstream.XStream; 10 | import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; 11 | import org.apache.commons.io.FilenameUtils; 12 | import org.apache.commons.io.IOUtils; 13 | 14 | import javax.xml.transform.Transformer; 15 | import javax.xml.transform.TransformerFactory; 16 | import javax.xml.transform.stream.StreamResult; 17 | import javax.xml.transform.stream.StreamSource; 18 | import java.io.*; 19 | import java.util.ArrayList; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Set; 23 | import java.util.stream.Collectors; 24 | 25 | public class KVMapInstrumentReportGenerator implements InstrumentReportGenerator, Closeable { 26 | 27 | private static List overallSteps = new ArrayList<>(); 28 | private static ProxyStat proxyStat = new ProxyStat(); 29 | 30 | private String proxyName; 31 | private final File kvInstrumentFile; 32 | private final File reportDirectory; 33 | private List instrumentEntries; 34 | private File summaryXMLFile; 35 | ; 36 | 37 | public KVMapInstrumentReportGenerator(String proxyName, File kvInstrumentFile, File reportDirectory) throws IOException { 38 | this.proxyName = proxyName; 39 | this.kvInstrumentFile = kvInstrumentFile; 40 | this.reportDirectory = reportDirectory; 41 | summaryXMLFile = new File(reportDirectory, "summary.xml"); 42 | instrumentEntries = cleanUpEntries(JsonPath.read(this.kvInstrumentFile, "$.entry[*].value")); 43 | } 44 | 45 | private List cleanUpEntries(List entries) { 46 | return entries.stream().map(e -> e.replace(" ", "")).collect(Collectors.toList()); 47 | } 48 | 49 | @Override 50 | public void generateReport(Endpoint e) { 51 | final List allSteps = getAllSteps(e); 52 | allSteps.forEach(this::updateStep); 53 | updateStats(e, allSteps); 54 | writeToDisk(e); 55 | } 56 | 57 | private void updateStats(Endpoint e, List allSteps) { 58 | overallSteps.addAll(allSteps); 59 | EndpointStat endpointStat = new EndpointStat(); 60 | endpointStat.name = e.getName(); 61 | endpointStat.endpointType = e.endpointType(); 62 | 63 | updateStats(allSteps, endpointStat); 64 | proxyStat.add(endpointStat); 65 | updateFlowStats(e, endpointStat); 66 | } 67 | 68 | 69 | private void writeToDisk(Endpoint e) { 70 | final XStream xStream = Util.xStreamInit(); 71 | try { 72 | final File outFile = new File(reportDirectory, e.endpointType() + "_" + e.getXmlFile().getName()); 73 | final PrettyPrintWriter writer = new PrettyPrintWriter(new FileWriter(outFile)); 74 | xStream.marshal(e, writer); 75 | writer.close(); 76 | } catch (IOException ioe) { 77 | throw new RuntimeException(ioe); 78 | } 79 | } 80 | 81 | private void updateStats(List steps, Stats stats) { 82 | Set uniquePolicies = new HashSet<>(); 83 | Set executedPolicies = new HashSet<>(); 84 | steps.forEach(s -> { 85 | uniquePolicies.add(s.getName()); 86 | if (s.isExecuted()) { 87 | executedPolicies.add(s.getName()); 88 | } 89 | }); 90 | stats.totalPolicies = uniquePolicies.size(); 91 | stats.executedPolicies = executedPolicies.size(); 92 | } 93 | 94 | private void updateStep(Step step) { 95 | final String location = step.location(); 96 | step.setExecuted(instrumentEntries.contains(location.replace(" ", ""))); 97 | } 98 | 99 | private List getAllSteps(Endpoint e) { 100 | List steps = new ArrayList<>(); 101 | e.getFaultRules().getFaultRules().forEach(f -> steps.addAll(f.getSteps())); 102 | steps.addAll(e.getPreflow().getRequestFlow().getSteps()); 103 | steps.addAll(e.getPreflow().getResponseFlow().getSteps()); 104 | steps.addAll(e.getPostflow().getRequestFlow().getSteps()); 105 | steps.addAll(e.getPostflow().getResponseFlow().getSteps()); 106 | e.getFlows().getFlows().forEach(f -> { 107 | steps.addAll(f.getRequestFlow().getSteps()); 108 | steps.addAll(f.getResponseFlow().getSteps()); 109 | }); 110 | return steps; 111 | } 112 | 113 | private void updateFlowStats(Endpoint e, EndpointStat stats) { 114 | updateFaultFlowStats(e, stats); 115 | updatePreFlowStats(e, stats); 116 | updatePostFlowStats(e, stats); 117 | for (Flow flow : e.getFlows().getFlows()) { 118 | List steps = new ArrayList<>(); 119 | steps.addAll(flow.getRequestFlow().getSteps()); 120 | steps.addAll(flow.getResponseFlow().getSteps()); 121 | stats.add(new FlowStat("Flow", flow.getName(), steps)); 122 | } 123 | } 124 | 125 | private void updatePostFlowStats(Endpoint e, EndpointStat stats) { 126 | List steps = new ArrayList<>(); 127 | steps.addAll(e.getPostflow().getRequestFlow().getSteps()); 128 | steps.addAll(e.getPostflow().getResponseFlow().getSteps()); 129 | stats.add(new FlowStat("Post Flow", "", steps)); 130 | } 131 | 132 | private void updatePreFlowStats(Endpoint e, EndpointStat stats) { 133 | List steps = new ArrayList<>(); 134 | steps.addAll(e.getPreflow().getRequestFlow().getSteps()); 135 | steps.addAll(e.getPreflow().getResponseFlow().getSteps()); 136 | stats.add(new FlowStat("Pre Flow", "", steps)); 137 | } 138 | 139 | private void updateFaultFlowStats(Endpoint e, EndpointStat stats) { 140 | for (FaultRule faultRule : e.getFaultRules().getFaultRules()) { 141 | stats.add(new FlowStat("Fault Rule", faultRule.getName(), faultRule.getSteps())); 142 | } 143 | 144 | } 145 | 146 | @Override 147 | public void close() throws IOException { 148 | proxyStat.name = proxyName; 149 | updateStats(overallSteps, proxyStat); 150 | proxyStat.calcCoverage(); 151 | final XStream xStream = new XStream(); 152 | xStream.processAnnotations(new Class[]{EndpointStat.class, 153 | FlowStat.class, ProxyStat.class}); 154 | final PrettyPrintWriter writer = new PrettyPrintWriter(new FileWriter(summaryXMLFile)); 155 | xStream.marshal(proxyStat, writer); 156 | writer.close(); 157 | generateHTMLReport(); 158 | } 159 | 160 | private void generateHTMLReport() { 161 | generateSummary(); 162 | generateProxyHtml(); 163 | copyBootStrapCSS(); 164 | } 165 | 166 | private void copyBootStrapCSS() { 167 | try { 168 | final FileWriter destFile = new FileWriter(new File(reportDirectory, "bootstrap.min.css")); 169 | IOUtils.copy(this.getClass().getResourceAsStream("/report/bootstrap.min.css"), destFile); 170 | destFile.close(); 171 | 172 | } catch (Exception e) { 173 | throw new RuntimeException(e); 174 | } 175 | } 176 | 177 | private void generateProxyHtml() { 178 | try { 179 | final Transformer transformer = TransformerFactory.newInstance() 180 | .newTransformer(new StreamSource(this.getClass() 181 | .getResourceAsStream("/report/proxy.xsl"))); 182 | final File[] proxyFiles = reportDirectory.listFiles((dir, name) -> name.startsWith("Proxy_") || name.startsWith("Target_")); 183 | for (File proxyFile : proxyFiles) { 184 | transformer.transform(new StreamSource(new FileReader(proxyFile)), 185 | new StreamResult(new FileWriter(new File(reportDirectory, 186 | FilenameUtils.getBaseName(proxyFile.getName()) + ".html")))); 187 | } 188 | 189 | } catch (Exception e) { 190 | throw new RuntimeException(e); 191 | } 192 | 193 | } 194 | 195 | private void generateSummary() { 196 | try { 197 | final Transformer transformer = TransformerFactory.newInstance() 198 | .newTransformer(new StreamSource(this.getClass() 199 | .getResourceAsStream("/report/summary.xsl"))); 200 | transformer.transform(new StreamSource(new FileReader(summaryXMLFile)), 201 | new StreamResult(new FileWriter(new File(reportDirectory, "summary.html")))); 202 | } catch (Exception e) { 203 | throw new RuntimeException(e); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/ProxyStat.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.thoughtworks.xstream.annotations.XStreamAlias; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @XStreamAlias("ProxyStat") 9 | public class ProxyStat extends Stats { 10 | 11 | protected List stats = new ArrayList<>(); 12 | 13 | @Override 14 | protected void calcCoverage() { 15 | stats.forEach(e -> e.calcCoverage()); 16 | super.calcCoverage(); 17 | } 18 | 19 | public void add(EndpointStat endpointStat) { 20 | this.stats.add(endpointStat); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/sriki77/apiproxy/instrument/report/Stats.java: -------------------------------------------------------------------------------- 1 | package com.github.sriki77.apiproxy.instrument.report; 2 | 3 | import com.github.sriki77.apiproxy.instrument.model.Step; 4 | 5 | import java.util.List; 6 | 7 | public class Stats { 8 | protected String name; 9 | 10 | protected long coverage; 11 | 12 | protected int totalPolicies; 13 | 14 | protected int executedPolicies; 15 | 16 | protected void calcCoverage() { 17 | if (totalPolicies == 0) { 18 | return; 19 | } 20 | coverage = Math.round(executedPolicies * 100.0 / totalPolicies); 21 | 22 | } 23 | 24 | protected void updateStats(List steps) { 25 | totalPolicies = steps.size(); 26 | steps.forEach(s -> { 27 | if (s.isExecuted()) ++executedPolicies; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/kv_instr_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | -1 5 | 6 | 7 | 8 | %1$s 9 | 10 | 11 | %2$s 12 | 13 | 14 | %3$s 15 | 16 | 17 | %4$s 18 | 19 | 20 | %5$s 21 | 22 | 23 | apiproxy 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/non-flow-tags-remover.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/report/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @import url("//fonts.googleapis.com/css?family=Raleway:400,700");/*! 2 | * Bootswatch v3.1.1+1 3 | * Homepage: http://bootswatch.com 4 | * Copyright 2012-2014 Thomas Park 5 | * Licensed under MIT 6 | * Based on Bootstrap 7 | *//*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,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:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !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^="javascript:"]:after,a[href^="#"]: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}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-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:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:Georgia,"Times New Roman",Times,serif;font-size:16px;line-height:1.42857143;color:#333333;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#4582ec;text-decoration:none}a:hover,a:focus{color:#134fb8;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{padding:4px;line-height:1.42857143;background-color:#ffffff;border:1px solid #dddddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:22px;margin-bottom:22px;border:0;border-top:1px solid #eeeeee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:bold;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:#b3b3b3}h1,.h1,h2,.h2,h3,.h3{margin-top:22px;margin-bottom:11px}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:11px;margin-bottom:11px}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:41px}h2,.h2{font-size:34px}h3,.h3{font-size:28px}h4,.h4{font-size:20px}h5,.h5{font-size:16px}h6,.h6{font-size:14px}p{margin:0 0 11px}.lead{margin-bottom:22px;font-size:18px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:24px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#b3b3b3}.text-primary{color:#4582ec}a.text-primary:hover{color:#1863e6}.text-success{color:#3fad46}a.text-success:hover{color:#318837}.text-info{color:#5bc0de}a.text-info:hover{color:#31b0d5}.text-warning{color:#f0ad4e}a.text-warning:hover{color:#ec971f}.text-danger{color:#d9534f}a.text-danger:hover{color:#c9302c}.bg-primary{color:#fff;background-color:#4582ec}a.bg-primary:hover{background-color:#1863e6}.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:10px;margin:44px 0 22px;border-bottom:1px solid #eeeeee}ul,ol{margin-top:0;margin-bottom:11px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:22px}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;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #b3b3b3}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:11px 22px;margin:0 0 22px;font-size:20px;border-left:5px solid #4582ec}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:#333333}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #4582ec;border-left:0;text-align:right}.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'}blockquote:before,blockquote:after{content:""}address{margin-bottom:22px;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;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:10.5px;margin:0 0 11px;font-size:15px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333333;background-color:#f5f5f5;border:1px solid #cccccc;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{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-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-left:15px;padding-right: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:0%}.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:0%}.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:0%}.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:0%}.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:0%}.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:0%}.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:0%}.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:0%}.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{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:22px}.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 #dddddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #dddddd}.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 #dddddd}.table .table{background-color:#ffffff}.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 #dddddd}.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 #dddddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.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.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.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.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.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.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:16.5px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #dddddd;-webkit-overflow-scrolling:touch}.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{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:22px;font-size:24px;line-height:inherit;color:#333333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;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:9px;font-size:16px;line-height:1.42857143;color:#333333}.form-control{display:block;width:100%;height:40px;padding:8px 12px;font-size:16px;line-height:1.42857143;color:#333333;background-color:#ffffff;background-image:none;border:1px solid #dddddd;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-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,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#b3b3b3;opacity:1}.form-control:-ms-input-placeholder{color:#b3b3b3}.form-control::-webkit-input-placeholder{color:#b3b3b3}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eeeeee;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:40px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:22px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:33px;padding:5px 10px;font-size:14px;line-height:1.5;border-radius:3px}select.input-sm{height:33px;line-height:33px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:57px;padding:14px 16px;font-size:20px;line-height:1.33;border-radius:6px}select.input-lg{height:57px;line-height:57px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:50px}.has-feedback .form-control-feedback{position:absolute;top:27px;right:0;display:block;width:40px;height:40px;line-height:40px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3fad46}.has-success .form-control{border-color:#3fad46;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#318837;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #81d186;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #81d186}.has-success .input-group-addon{color:#3fad46;border-color:#3fad46;background-color:#dff0d8}.has-success .form-control-feedback{color:#3fad46}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#ec971f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f8d9ac;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f8d9ac}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#f0ad4e}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#d9534f}.has-error .form-control{border-color:#d9534f;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#c9302c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #eba5a3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #eba5a3}.has-error .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#f2dede}.has-error .form-control-feedback{color:#d9534f}.form-control-static{margin-bottom: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 .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;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:9px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:31px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:9px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:8px 12px;font-size:16px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.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{color:#333333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333333;background-color:#ffffff;border-color:#dddddd}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333333;background-color:#ebebeb;border-color:#bebebe}.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: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:#ffffff;border-color:#dddddd}.btn-default .badge{color:#ffffff;background-color:#333333}.btn-primary{color:#ffffff;background-color:#4582ec;border-color:#4582ec}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#ffffff;background-color:#2069e8;border-color:#175fdd}.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: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:#4582ec;border-color:#4582ec}.btn-primary .badge{color:#4582ec;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#3fad46;border-color:#3fad46}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#ffffff;background-color:#348f3a;border-color:#2f8034}.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: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:#3fad46;border-color:#3fad46}.btn-success .badge{color:#3fad46;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#ffffff;background-color:#39b3d7;border-color:#2aabd2}.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: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:#5bc0de}.btn-info .badge{color:#5bc0de;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#ffffff;background-color:#ed9c28;border-color:#eb9316}.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: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:#f0ad4e}.btn-warning .badge{color:#f0ad4e;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#ffffff;background-color:#d2322d;border-color:#c12e2a}.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: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:#d9534f}.btn-danger .badge{color:#d9534f;background-color:#ffffff}.btn-link{color:#4582ec;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.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:#134fb8;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:#b3b3b3;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:14px 16px;font-size:20px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:14px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:14px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.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 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@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.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{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"}.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}.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;list-style:none;font-size:16px;background-color:#ffffff;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:10px 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:#333333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#ffffff;background-color:#4582ec}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#4582ec}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#b3b3b3}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:14px;line-height:1.42857143;color:#b3b3b3}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.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:focus,.btn-group-vertical>.btn:focus{outline:none}.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-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-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>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,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-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.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-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right: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:57px;padding:14px 16px;font-size:20px;line-height:1.33;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:57px;line-height:57px}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:33px;padding:5px 10px;font-size:14px;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:33px;line-height:33px}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:8px 12px;font-size:16px;font-weight:normal;line-height:1;color:#333333;text-align:center;background-color:#eeeeee;border:1px solid #dddddd;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:14px;border-radius:3px}.input-group-addon.input-lg{padding:14px 16px;font-size:20px;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-bottom-right-radius:0;border-top-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-bottom-left-radius:0;border-top-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{margin-bottom:0;padding-left: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:#eeeeee}.nav>li.disabled>a{color:#b3b3b3}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#b3b3b3;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eeeeee;border-color:#4582ec}.nav .nav-divider{height:1px;margin:10px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #dddddd}.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:#eeeeee #eeeeee #dddddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555555;background-color:#ffffff;border:1px solid #dddddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.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 #dddddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #dddddd;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:#ffffff}}.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:#ffffff;background-color:#4582ec}.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{text-align:center;margin-bottom:5px}.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 #dddddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #dddddd;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:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:22px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.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;padding:14px 15px;font-size:20px;line-height:22px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:none}.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:7px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:22px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;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:22px}.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:14px;padding-bottom:14px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:5px;margin-bottom:5px}@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 .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;padding-left:0;vertical-align:middle}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:5px;margin-bottom:5px}.navbar-btn.btn-sm{margin-top:8.5px;margin-bottom:8.5px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:14px;margin-bottom:14px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#ffffff;border-color:transparent}.navbar-default .navbar-brand{color:#4582ec}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#4582ec;background-color:transparent}.navbar-default .navbar-text{color:#333333}.navbar-default .navbar-nav>li>a{color:#4582ec}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#4582ec;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#4582ec;background-color:transparent}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-toggle{border-color:#dddddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#dddddd}.navbar-default .navbar-toggle .icon-bar{background-color:#cccccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:transparent;color:#4582ec}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#4582ec}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#4582ec;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:#4582ec;background-color:transparent}.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:#333333;background-color:transparent}}.navbar-default .navbar-link{color:#4582ec}.navbar-default .navbar-link:hover{color:#4582ec}.navbar-inverse{background-color:#ffffff;border-color:transparent}.navbar-inverse .navbar-brand{color:#333333}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-text{color:#333333}.navbar-inverse .navbar-nav>li>a{color:#333333}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#dddddd}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#dddddd}.navbar-inverse .navbar-toggle .icon-bar{background-color:#cccccc}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#ededed}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:transparent;color:#333333}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#333333}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#333333;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:#333333;background-color:transparent}.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:#cccccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#333333}.navbar-inverse .navbar-link:hover{color:#333333}.breadcrumb{padding:8px 15px;margin-bottom:22px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#cccccc}.breadcrumb>.active{color:#b3b3b3}.pagination{display:inline-block;padding-left:0;margin:22px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:8px 12px;line-height:1.42857143;text-decoration:none;color:#333333;background-color:#ffffff;border:1px solid #dddddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ffffff;background-color:#4582ec;border-color:#4582ec}.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:#ffffff;background-color:#4582ec;border-color:#4582ec;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#b3b3b3;background-color:#ffffff;border-color:#dddddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:14px 16px;font-size:20px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:14px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:22px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#ffffff;border:1px solid #dddddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#4582ec}.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:#b3b3b3;background-color:#ffffff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#ffffff}.label-default[href]:hover,.label-default[href]:focus{background-color:#e6e6e6}.label-primary{background-color:#4582ec}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#1863e6}.label-success{background-color:#3fad46}.label-success[href]:hover,.label-success[href]:focus{background-color:#318837}.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:14px;font-weight:bold;color:#ffffff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#4582ec;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:#ffffff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#4582ec;background-color:#ffffff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#f7f7f7}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:24px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:72px}}.thumbnail{display:block;padding:4px;margin-bottom:22px;line-height:1.42857143;background-color:#ffffff;border:1px solid #dddddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#4582ec}.thumbnail .caption{padding:9px;color:#333333}.alert{padding:15px;margin-bottom:22px;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{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#3fad46;border-color:#3fad46;color:#ffffff}.alert-success hr{border-top-color:#389a3e}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#5bc0de;border-color:#5bc0de;color:#ffffff}.alert-info hr{border-top-color:#46b8da}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#f0ad4e;border-color:#f0ad4e;color:#ffffff}.alert-warning hr{border-top-color:#eea236}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#d9534f;border-color:#d9534f;color:#ffffff}.alert-danger hr{border-top-color:#d43f3a}.alert-danger .alert-link{color:#e6e6e6}@-webkit-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{overflow:hidden;height:22px;margin-bottom:22px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:14px;line-height:22px;color:#ffffff;text-align:center;background-color:#4582ec;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#3fad46}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.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,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.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,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.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,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #dddddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555555}a.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#4582ec;border-color:#4582ec}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#fefeff}.list-group-item-success{color:#3fad46;background-color:#dff0d8}a.list-group-item-success{color:#3fad46}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3fad46;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:#3fad46;border-color:#3fad46}.list-group-item-info{color:#5bc0de;background-color:#d9edf7}a.list-group-item-info{color:#5bc0de}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#5bc0de;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:#5bc0de;border-color:#5bc0de}.list-group-item-warning{color:#f0ad4e;background-color:#fcf8e3}a.list-group-item-warning{color:#f0ad4e}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#f0ad4e;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:#f0ad4e;border-color:#f0ad4e}.list-group-item-danger{color:#d9534f;background-color:#f2dede}a.list-group-item-danger{color:#d9534f}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#d9534f;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:#d9534f;border-color:#d9534f}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:22px;background-color:#ffffff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:18px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#ffffff;border-top:1px solid #dddddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.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}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-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 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{border-top:1px solid #dddddd}.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{border:0;margin-bottom:0}.panel-group{margin-bottom:22px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #dddddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #dddddd}.panel-default{border-color:#dddddd}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:#dddddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#dddddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#dddddd}.panel-primary{border-color:transparent}.panel-primary>.panel-heading{color:#ffffff;background-color:#4582ec;border-color:transparent}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-success{border-color:transparent}.panel-success>.panel-heading{color:#3fad46;background-color:#3fad46;border-color:transparent}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-info{border-color:transparent}.panel-info>.panel-heading{color:#5bc0de;background-color:#5bc0de;border-color:transparent}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-warning{border-color:transparent}.panel-warning>.panel-heading{color:#f0ad4e;background-color:#f0ad4e;border-color:transparent}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-danger{border-color:transparent}.panel-danger>.panel-heading{color:#d9534f;background-color:#d9534f;border-color:transparent}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f7f7f7;border:1px solid #e5e5e5;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:24px;font-weight:bold;line-height:1;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000000;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:14px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:rgba(0,0,0,0.9);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:rgba(0,0,0,0.9)}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:rgba(0,0,0,0.9)}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:rgba(0,0,0,0.9)}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:rgba(0,0,0,0.9)}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:rgba(0,0,0,0.9)}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:rgba(0,0,0,0.9)}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:rgba(0,0,0,0.9)}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:rgba(0,0,0,0.9)}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#ffffff;background-clip:padding-box;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:16px;font-weight:normal;line-height:18px;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{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#ffffff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999999;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#ffffff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#ffffff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#ffffff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.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;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left, color-stop(rgba(0,0,0,0.5) 0), color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, color-stop(rgba(0,0,0,0.0001) 0), color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:none;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.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%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.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%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,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;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix: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{content:" ";display:table}.clearfix: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-left:auto;margin-right: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}@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 (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: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: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 (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}}@media print{.hidden-print{display:none !important}}.navbar{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}.navbar-nav>li>a{padding:8px 12px;margin:12px 6px;border:1px solid transparent;border-radius:4px}.navbar-nav>li>a:hover{border:1px solid #ddd}.navbar-nav>.active>a,.navbar-nav>.active>a:hover{border:1px solid #ddd}.navbar-default .navbar-nav>.active>a:hover{color:#4582ec}.navbar-inverse .navbar-nav>.active>a:hover{color:#333333}.navbar-brand{margin-top:6px}.navbar-form{margin-top:12px}.btn{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}legend{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}.input-group-addon{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border:1px solid #ddd}.pagination{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}.pagination-lg>li>a,.pagination-lg>li>span{padding:14px 24px}.pager{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif}.pager a{color:#333333}.pager a:hover{border-color:transparent;color:#fff}.pager .disabled a{border-color:#dddddd}.alert a,.alert .alert-link{color:#ffffff;text-decoration:underline}.alert .close{color:#fff;text-decoration:none;opacity:0.4}.alert .close:hover,.alert .close:focus{color:#fff;opacity:1}.label{font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:normal}.label-default{border:1px solid #ddd;color:#333333}.badge{padding:1px 7px 5px;vertical-align:2px;font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:normal}.panel{-webkit-box-shadow:none;box-shadow:none}.panel-default .panel-heading,.panel-default .panel-footer{background-color:#fff;font-family:"Raleway","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:bold}.panel-primary .panel-heading,.panel-success .panel-heading,.panel-warning .panel-heading,.panel-danger .panel-heading,.panel-info .panel-heading{color:#fff}.panel-primary .panel-body,.panel-success .panel-body,.panel-warning .panel-body,.panel-danger .panel-body,.panel-info .panel-body{border:1px solid #ddd;border-top-width:0;border-radius:0 0 4px 4px} -------------------------------------------------------------------------------- /src/main/resources/report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Apigee Proxy Coverage Report 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Total Unique Policies: %3$sTotal Unique Policies Executed: %4$sTotal Policies Instances: %5$sTotal Policies Instances Executed: %6$s
42 |
43 |
44 |
45 | 46 |
47 | 48 | %7$s 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 | 60 | %8$s 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/resources/report/proxy.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Apigee Proxy Coverage Report 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |

18 | Endpoint: 19 | 20 | 21 |
22 | (Policy Execution Stats) 23 |

24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 | 32 |

33 | 34 |

35 | 36 | 37 |

38 | 39 | 40 | 41 |

42 | 43 |

44 |
45 | 46 | 73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 |

84 |
85 |

Flow:

86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 111 | 112 |
Policy NameCondition
98 | Request Flow 99 |
106 | Response Flow 107 |
113 |
114 |

115 |
116 |
117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 |

125 |
126 | 127 |

Fault Rule:

128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
Policy NameCondition
142 |
143 |

144 |
145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | - 193 | 194 | 195 | 196 | 197 | 198 |
-------------------------------------------------------------------------------- /src/main/resources/report/summary.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Apigee Proxy Coverage Report 10 | 11 | 12 | 13 |
14 |
15 |
16 |

17 |

API Proxy:

18 |
19 | Coverage: 20 | , of covered 21 |
22 |

23 |
24 |
25 |
26 |
27 |
28 |
29 |

Proxy Endpoints

30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 |

Target Endpoints

41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | 76 | 77 | 78 |
79 | 80 |

81 |
82 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 127 | 130 | 131 | 132 | 133 |
Flow NameCoverage
103 | 104 | 105 | _.html# 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Flow: 121 | 122 | 123 | 124 | 125 | 126 | 128 | , of 129 |
134 |
135 |

136 |
137 | 138 | 139 | 140 | % 141 | 142 | 143 | 144 | 145 | % 146 | 147 | 148 | 149 | 150 | % 151 | 152 | 153 | 154 |
-------------------------------------------------------------------------------- /src/main/resources/report/table_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %s 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Endpoint: ____
Flow NamePolicy CountCoverage
-------------------------------------------------------------------------------- /test-policy.xml: -------------------------------------------------------------------------------- 1 | Hello test policy!! --------------------------------------------------------------------------------