From 9a5dcecf6d6f1fb89c40fba53701a1ae5a101cf6 Mon Sep 17 00:00:00 2001 From: rickgao Date: Mon, 18 Apr 2022 21:46:47 +0800 Subject: [PATCH] init --- .gitignore | 23 + aggregator/build.gradle | 63 + aggregator/c-tests/build.gradle | 46 + .../java/book/aggr/CommentsContractTest.java | 164 + .../c-tests/src/test/resources/arquillian.xml | 16 + aggregator/cp-tests/build.gradle | 42 + .../java/book/aggr/CommentsGatewayTest.java | 55 + .../test/resources/hoverfly/simulation.json | 43 + .../gradle/wrapper/gradle-wrapper.properties | 6 + aggregator/gradlew | 160 + aggregator/gradlew.bat | 90 + aggregator/i-tests/build.gradle | 58 + .../java/book/aggr/CommentsGatewayTest.java | 72 + .../java/book/aggr/CommentsNegativeTest.java | 83 + aggregator/settings.gradle | 5 + .../main/java/book/aggr/CommentsGateway.java | 72 + .../src/main/java/book/aggr/Futures.java | 20 + .../book/aggr/GameAggregatorApplication.java | 29 + .../main/java/book/aggr/GamersResource.java | 63 + .../src/main/java/book/aggr/GamesGateway.java | 43 + .../java/book/aggr/CommentsGatewayTest.java | 20 + comments/build.gradle | 63 + comments/c-tests/build.gradle | 61 + .../boundary/CommentsProviderTest.java | 128 + .../src/test/resources/test-resources.xml | 11 + .../gradle/wrapper/gradle-wrapper.properties | 6 + comments/gradlew | 160 + comments/gradlew.bat | 90 + comments/i-tests/build.gradle | 63 + .../test/java/book/comments/CommentsTest.java | 72 + .../resources/expected-insert-comments.json | 9 + .../src/test/resources/resources-test.xml | 11 + comments/settings.gradle | 4 + .../book/comments/MongoClientProvider.java | 26 + .../java/book/comments/boundary/Comments.java | 57 + .../boundary/CommentsInternalResource.java | 40 + .../comments/boundary/CommentsResource.java | 59 + .../boundary/DocumentToJsonObject.java | 54 + .../comments/boundary/JaxRsApplication.java | 20 + .../src/main/webapp/WEB-INF/resources.xml | 11 + .../boundary/CommentsResourceTest.java | 54 + .../book/comments/boundary/CommentsTest.java | 26 + .../comments/boundary/CommentsWarpTest.java | 114 + .../src/test/resources/test-resources.xml | 11 + .../gradle/wrapper/gradle-wrapper.properties | 6 + game-aggregator-service/gradlew | 172 + game-aggregator-service/gradlew.bat | 84 + game/pom.xml | 198 + game/src/main/java/book/games/Main.java | 65 + .../boundary/ExecutorServiceProducer.java | 17 + .../main/java/book/games/boundary/Games.java | 40 + .../book/games/boundary/GamesResource.java | 111 + .../java/book/games/boundary/IgdbGateway.java | 94 + .../java/book/games/control/GamesService.java | 81 + .../src/main/java/book/games/entity/Game.java | 226 + .../entity/LocalDatePersistenceConverter.java | 23 + .../java/book/games/entity/ReleaseDate.java | 36 + .../java/book/games/entity/SearchResult.java | 59 + .../main/resources/META-INF/gamesCreate.ddl | 0 .../src/main/resources/META-INF/gamesDrop.ddl | 0 .../main/resources/META-INF/persistence.xml | 14 + game/src/main/webapp/WEB-INF/beans.xml | 8 + game/src/main/webapp/WEB-INF/web.xml | 55 + .../arquillian/ArquillianAbstractTest.java | 53 + .../games/arquillian/ArquillianBasicTest.java | 67 + .../arquillian/ArquillianRemoteTest.java | 53 + .../arquillian/ArquillianResourceTest.java | 59 + .../games/boundary/GamesResourceTest.java | 128 + .../java/book/games/boundary/GamesTest.java | 81 + .../book/games/boundary/IgdbGatewayTest.java | 49 + .../book/games/control/GamesServiceTest.java | 121 + .../test/java/integrationtests/GamesTest.java | 54 + game/src/test/resources/arquillian.xml | 25 + game/src/test/resources/gamesCreate.ddl | 0 game/src/test/resources/gamesDrop.ddl | 0 game/src/test/resources/test-persistence.xml | 14 + video/Dockerfile | 7 + video/README.adoc | 20 + video/build.gradle | 46 + video/c-tests/build.gradle | 33 + .../book/video/VideoServiceContainerTest.java | 26 + .../c-tests/src/test/resources/arquillian.xml | 26 + .../gradle/wrapper/gradle-wrapper.properties | 6 + video/gradlew | 160 + video/gradlew.bat | 90 + video/i-tests/build.gradle | 39 + .../video/YoutubeVideosArquillianTest.java | 62 + .../java/book/video/YoutubeVideosTest.java | 56 + .../resources/book/video/expected-videos.json | 14 + video/settings.gradle | 3 + video/shutdown.bat | 1 + video/shutdown.sh | 3 + .../book/video/ControllerConfiguration.java | 24 + video/src/main/java/book/video/Futures.java | 19 + video/src/main/java/book/video/Main.java | 30 + .../video/ThreadExecutorConfiguration.java | 19 + .../book/video/boundary/VideosResource.java | 31 + .../book/video/boundary/YouTubeGateway.java | 81 + .../book/video/boundary/YouTubeVideos.java | 40 + .../controller/VideoServiceController.java | 46 + .../controller/YouTubeVideoLinkCreator.java | 21 + .../java/book/video/entity/YoutubeLink.java | 35 + .../java/book/video/entity/YoutubeLinks.java | 33 + video/src/test/java/YouTubeSearchTest.java | 62 + .../YouTubeVideoLinkCreatorTest.java | 25 + .../book/video/entity/YouTubeLinkTest.java | 25 + web/LICENSE | 201 + web/README.md | 22 + web/pom.xml | 217 + web/src/main/java/book/web/Phone.java | 93 + web/src/main/java/book/web/PhoneService.java | 82 + .../main/java/book/web/ResourceEndpoints.java | 35 + .../main/java/book/web/RestApplication.java | 28 + web/src/main/tomee/conf/tomee.xml | 18 + web/src/main/webapp/WEB-INF/web.xml | 42 + web/src/main/webapp/assets/css/app.css | 6 + web/src/main/webapp/assets/css/bootstrap.css | 5784 +++++++++++++++++ web/src/main/webapp/assets/js/angular.js | 333 + web/src/main/webapp/assets/js/controllers.js | 67 + web/src/main/webapp/assets/js/route.js | 17 + web/src/main/webapp/detail.html | 10 + web/src/main/webapp/favicon.ico | Bin 0 -> 32988 bytes web/src/main/webapp/index.html | 37 + web/src/test/java/book/web/EndToEndTest.java | 231 + .../java/book/web/MongoClientProvider.java | 26 + web/src/test/java/book/web/page/Detail.java | 17 + web/src/test/java/book/web/page/Index.java | 45 + web/src/test/java/book/web/page/List.java | 50 + .../rule/DefaultJavaResolutionStrategy.java | 32 + .../java/book/web/rule/MicroserviceRule.java | 154 + .../test/java/book/web/rule/MongodRule.java | 51 + .../test/java/book/web/rule/RedisRule.java | 32 + .../book/web/rule/ResolutionStrategy.java | 8 + web/src/test/resources/arquillian.xml | 60 + web/src/test/resources/test-persistence.xml | 14 + web/src/test/resources/test-resources.xml | 11 + web/src/test/resources/test-web.xml | 43 + 137 files changed, 13165 insertions(+) create mode 100644 .gitignore create mode 100644 aggregator/build.gradle create mode 100644 aggregator/c-tests/build.gradle create mode 100644 aggregator/c-tests/src/test/java/book/aggr/CommentsContractTest.java create mode 100644 aggregator/c-tests/src/test/resources/arquillian.xml create mode 100644 aggregator/cp-tests/build.gradle create mode 100644 aggregator/cp-tests/src/test/java/book/aggr/CommentsGatewayTest.java create mode 100644 aggregator/cp-tests/src/test/resources/hoverfly/simulation.json create mode 100644 aggregator/gradle/wrapper/gradle-wrapper.properties create mode 100644 aggregator/gradlew create mode 100644 aggregator/gradlew.bat create mode 100644 aggregator/i-tests/build.gradle create mode 100644 aggregator/i-tests/src/test/java/book/aggr/CommentsGatewayTest.java create mode 100644 aggregator/i-tests/src/test/java/book/aggr/CommentsNegativeTest.java create mode 100644 aggregator/settings.gradle create mode 100644 aggregator/src/main/java/book/aggr/CommentsGateway.java create mode 100644 aggregator/src/main/java/book/aggr/Futures.java create mode 100644 aggregator/src/main/java/book/aggr/GameAggregatorApplication.java create mode 100644 aggregator/src/main/java/book/aggr/GamersResource.java create mode 100644 aggregator/src/main/java/book/aggr/GamesGateway.java create mode 100644 aggregator/src/test/java/book/aggr/CommentsGatewayTest.java create mode 100644 comments/build.gradle create mode 100644 comments/c-tests/build.gradle create mode 100644 comments/c-tests/src/test/java/book/comments/boundary/CommentsProviderTest.java create mode 100644 comments/c-tests/src/test/resources/test-resources.xml create mode 100644 comments/gradle/wrapper/gradle-wrapper.properties create mode 100644 comments/gradlew create mode 100644 comments/gradlew.bat create mode 100644 comments/i-tests/build.gradle create mode 100644 comments/i-tests/src/test/java/book/comments/CommentsTest.java create mode 100644 comments/i-tests/src/test/resources/expected-insert-comments.json create mode 100644 comments/i-tests/src/test/resources/resources-test.xml create mode 100644 comments/settings.gradle create mode 100644 comments/src/main/java/book/comments/MongoClientProvider.java create mode 100644 comments/src/main/java/book/comments/boundary/Comments.java create mode 100644 comments/src/main/java/book/comments/boundary/CommentsInternalResource.java create mode 100644 comments/src/main/java/book/comments/boundary/CommentsResource.java create mode 100644 comments/src/main/java/book/comments/boundary/DocumentToJsonObject.java create mode 100644 comments/src/main/java/book/comments/boundary/JaxRsApplication.java create mode 100644 comments/src/main/webapp/WEB-INF/resources.xml create mode 100644 comments/src/test/java/book/comments/boundary/CommentsResourceTest.java create mode 100644 comments/src/test/java/book/comments/boundary/CommentsTest.java create mode 100644 comments/src/test/java/book/comments/boundary/CommentsWarpTest.java create mode 100644 comments/src/test/resources/test-resources.xml create mode 100644 game-aggregator-service/gradle/wrapper/gradle-wrapper.properties create mode 100644 game-aggregator-service/gradlew create mode 100644 game-aggregator-service/gradlew.bat create mode 100644 game/pom.xml create mode 100644 game/src/main/java/book/games/Main.java create mode 100644 game/src/main/java/book/games/boundary/ExecutorServiceProducer.java create mode 100644 game/src/main/java/book/games/boundary/Games.java create mode 100644 game/src/main/java/book/games/boundary/GamesResource.java create mode 100644 game/src/main/java/book/games/boundary/IgdbGateway.java create mode 100644 game/src/main/java/book/games/control/GamesService.java create mode 100644 game/src/main/java/book/games/entity/Game.java create mode 100644 game/src/main/java/book/games/entity/LocalDatePersistenceConverter.java create mode 100644 game/src/main/java/book/games/entity/ReleaseDate.java create mode 100644 game/src/main/java/book/games/entity/SearchResult.java create mode 100644 game/src/main/resources/META-INF/gamesCreate.ddl create mode 100644 game/src/main/resources/META-INF/gamesDrop.ddl create mode 100644 game/src/main/resources/META-INF/persistence.xml create mode 100644 game/src/main/webapp/WEB-INF/beans.xml create mode 100644 game/src/main/webapp/WEB-INF/web.xml create mode 100644 game/src/test/java/book/games/arquillian/ArquillianAbstractTest.java create mode 100644 game/src/test/java/book/games/arquillian/ArquillianBasicTest.java create mode 100644 game/src/test/java/book/games/arquillian/ArquillianRemoteTest.java create mode 100644 game/src/test/java/book/games/arquillian/ArquillianResourceTest.java create mode 100644 game/src/test/java/book/games/boundary/GamesResourceTest.java create mode 100644 game/src/test/java/book/games/boundary/GamesTest.java create mode 100644 game/src/test/java/book/games/boundary/IgdbGatewayTest.java create mode 100644 game/src/test/java/book/games/control/GamesServiceTest.java create mode 100644 game/src/test/java/integrationtests/GamesTest.java create mode 100644 game/src/test/resources/arquillian.xml create mode 100644 game/src/test/resources/gamesCreate.ddl create mode 100644 game/src/test/resources/gamesDrop.ddl create mode 100644 game/src/test/resources/test-persistence.xml create mode 100644 video/Dockerfile create mode 100644 video/README.adoc create mode 100644 video/build.gradle create mode 100644 video/c-tests/build.gradle create mode 100644 video/c-tests/src/test/java/book/video/VideoServiceContainerTest.java create mode 100644 video/c-tests/src/test/resources/arquillian.xml create mode 100644 video/gradle/wrapper/gradle-wrapper.properties create mode 100644 video/gradlew create mode 100644 video/gradlew.bat create mode 100644 video/i-tests/build.gradle create mode 100644 video/i-tests/src/test/java/book/video/YoutubeVideosArquillianTest.java create mode 100644 video/i-tests/src/test/java/book/video/YoutubeVideosTest.java create mode 100644 video/i-tests/src/test/resources/book/video/expected-videos.json create mode 100644 video/settings.gradle create mode 100644 video/shutdown.bat create mode 100644 video/shutdown.sh create mode 100644 video/src/main/java/book/video/ControllerConfiguration.java create mode 100644 video/src/main/java/book/video/Futures.java create mode 100644 video/src/main/java/book/video/Main.java create mode 100644 video/src/main/java/book/video/ThreadExecutorConfiguration.java create mode 100644 video/src/main/java/book/video/boundary/VideosResource.java create mode 100644 video/src/main/java/book/video/boundary/YouTubeGateway.java create mode 100644 video/src/main/java/book/video/boundary/YouTubeVideos.java create mode 100644 video/src/main/java/book/video/controller/VideoServiceController.java create mode 100644 video/src/main/java/book/video/controller/YouTubeVideoLinkCreator.java create mode 100644 video/src/main/java/book/video/entity/YoutubeLink.java create mode 100644 video/src/main/java/book/video/entity/YoutubeLinks.java create mode 100644 video/src/test/java/YouTubeSearchTest.java create mode 100644 video/src/test/java/book/video/controller/YouTubeVideoLinkCreatorTest.java create mode 100644 video/src/test/java/book/video/entity/YouTubeLinkTest.java create mode 100644 web/LICENSE create mode 100644 web/README.md create mode 100644 web/pom.xml create mode 100644 web/src/main/java/book/web/Phone.java create mode 100644 web/src/main/java/book/web/PhoneService.java create mode 100644 web/src/main/java/book/web/ResourceEndpoints.java create mode 100644 web/src/main/java/book/web/RestApplication.java create mode 100644 web/src/main/tomee/conf/tomee.xml create mode 100644 web/src/main/webapp/WEB-INF/web.xml create mode 100644 web/src/main/webapp/assets/css/app.css create mode 100644 web/src/main/webapp/assets/css/bootstrap.css create mode 100644 web/src/main/webapp/assets/js/angular.js create mode 100644 web/src/main/webapp/assets/js/controllers.js create mode 100644 web/src/main/webapp/assets/js/route.js create mode 100644 web/src/main/webapp/detail.html create mode 100644 web/src/main/webapp/favicon.ico create mode 100644 web/src/main/webapp/index.html create mode 100644 web/src/test/java/book/web/EndToEndTest.java create mode 100644 web/src/test/java/book/web/MongoClientProvider.java create mode 100644 web/src/test/java/book/web/page/Detail.java create mode 100644 web/src/test/java/book/web/page/Index.java create mode 100644 web/src/test/java/book/web/page/List.java create mode 100644 web/src/test/java/book/web/rule/DefaultJavaResolutionStrategy.java create mode 100644 web/src/test/java/book/web/rule/MicroserviceRule.java create mode 100644 web/src/test/java/book/web/rule/MongodRule.java create mode 100644 web/src/test/java/book/web/rule/RedisRule.java create mode 100644 web/src/test/java/book/web/rule/ResolutionStrategy.java create mode 100644 web/src/test/resources/arquillian.xml create mode 100644 web/src/test/resources/test-persistence.xml create mode 100644 web/src/test/resources/test-resources.xml create mode 100644 web/src/test/resources/test-web.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc7bdeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* \ No newline at end of file diff --git a/aggregator/build.gradle b/aggregator/build.gradle new file mode 100644 index 0000000..db3f7d2 --- /dev/null +++ b/aggregator/build.gradle @@ -0,0 +1,63 @@ +// tag::build[] +//<1> +plugins { + id "io.spring.dependency-management" version "0.6.1.RELEASE" +} + +//<2> +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' + mavenBom 'org.jboss.shrinkwrap:shrinkwrap-bom:1.2.6' + mavenBom 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-bom:2.2.4' + } +} + +apply plugin: 'war' + +group = 'org.gamer' +version = '1.0-SNAPSHOT' +war.archiveName = "gameaggregatorservice.war" + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +//<3> +dependencies { + + testCompile 'junit:junit:4.12' + + //<4> + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-container' + + //<5> + testCompile 'org.jboss.arquillian.container:arquillian-tomcat-embedded-8:1.0.0.CR7' + + //<6> + testCompile 'org.jboss.shrinkwrap:shrinkwrap-api' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-spi' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-impl-base' + + //<7> + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven-archive' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven-archive' + + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.22.2' + compile 'org.glassfish.jersey.core:jersey-client:2.22.2' + compile 'org.glassfish:javax.json:1.0.4' + compile 'org.glassfish:jsonp-jaxrs:1.0' +} +// end::build[] + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +repositories { + mavenCentral() +} diff --git a/aggregator/c-tests/build.gradle b/aggregator/c-tests/build.gradle new file mode 100644 index 0000000..ab59551 --- /dev/null +++ b/aggregator/c-tests/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'java' + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath "io.spring.gradle:dependency-management-plugin:1.0.0.RELEASE" + } +} + +apply plugin: "io.spring.dependency-management" + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' + } +} + +repositories { + mavenCentral() + jcenter() +} + +test { + testLogging { + showStandardStreams = true + } + +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.4.1' + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-standalone' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven-archive' + testCompile 'org.arquillian.algeron:arquillian-algeron-pact-consumer-core:1.0.1' + testCompile 'org.arquillian.algeron:arquillian-algeron-consumer-git-publisher:1.0.1' + testCompile 'au.com.dius:pact-jvm-consumer_2.11:3.5.0' + +} diff --git a/aggregator/c-tests/src/test/java/book/aggr/CommentsContractTest.java b/aggregator/c-tests/src/test/java/book/aggr/CommentsContractTest.java new file mode 100644 index 0000000..bd8db50 --- /dev/null +++ b/aggregator/c-tests/src/test/java/book/aggr/CommentsContractTest.java @@ -0,0 +1,164 @@ +package book.aggr; + +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.model.PactFragment; +import java.net.URL; +import org.arquillian.algeron.consumer.StubServer; +import org.arquillian.algeron.pact.consumer.spi.Pact; +import org.arquillian.algeron.pact.consumer.spi.PactVerification; +import org.jboss.arquillian.junit.Arquillian; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.ws.rs.core.Response; +import java.io.StringReader; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::test[] +@RunWith(Arquillian.class) // <1> +@Pact(provider = "comments_service", consumer = + "games_aggregator_service") +// <2> +public class CommentsContractTest { + + + private static final String commentObject = "{" + " 'comment' " + + ": 'This Game is Awesome'," + " 'rate' : 5," + " " + + "'gameId': 1234" + "}"; + + private static final String commentResult = "{" + " 'rate': " + + "5.0," + " 'total': 1," + " 'comments': ['This Game" + + " is Awesome']" + "}"; + + public PactFragment putCommentFragment(PactDslWithProvider + builder) { // <3> + final Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + + return builder.uponReceiving("User creates a new comment") + .path("/comments").method("POST").headers(headers) + .body(toJson(commentObject)) // <4> + .willRespondWith().status(201).matchHeader + ("Location", ".*/[0-9a-f]+", + "/comments/1234").toFragment(); + } + // end::test[] + + //TODO beta3 makes it better. header thing + + // tag::test2[] + + @StubServer + URL url; + + public PactFragment getCommentsFragment(PactDslWithProvider + builder) { + + final Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + + return builder.given("A game with id 12 with rate 5 and " + + "message This Game is Awesome") // <1> + .uponReceiving("User gets comments for given Game") + .matchPath("/comments/12").method("GET") + .willRespondWith().status(200).headers(headers) + .body(toJson(commentResult)).toFragment(); + + } + + @Test + @PactVerification(fragment = "getCommentsFragment") + public void shouldGetComentsFromCommentsService() throws + ExecutionException, InterruptedException { + final CommentsGateway commentsGateway = new CommentsGateway(); + commentsGateway.initRestClient(url.toString()); + + final Future comments = commentsGateway + .getCommentsFromCommentsService(12); + final JsonObject commentsResponse = comments.get(); + + assertThat(commentsResponse.getJsonNumber("rate") + .doubleValue()).isEqualTo(5); // <2> + assertThat(commentsResponse.getInt("total")).isEqualTo(1); + assertThat(commentsResponse.getJsonArray("comments")) + .hasSize(1); + + } + + // end::test2[] + + // tag::test[] + @Test + @PactVerification(fragment = "putCommentFragment") // <5> + public void shouldInsertCommentsInCommentsService() throws + ExecutionException, InterruptedException { + + final CommentsGateway commentsGateway = new CommentsGateway(); + commentsGateway.initRestClient(url.toString()); // + // <6> + + JsonReader jsonReader = Json.createReader(new StringReader + (toJson(commentObject))); + JsonObject commentObject = jsonReader.readObject(); + jsonReader.close(); + + final Future comment = commentsGateway + .createComment(commentObject); + + final Response response = comment.get(); + final URI location = response.getLocation(); + + assertThat(location).isNotNull(); + final String id = extractId(location); + + assertThat(id).matches("[0-9a-f]+"); + assertThat(response.getStatus()).isEqualTo(201); + + } + + // end::test[] + + private String extractId(final URI location) { + final String commentLocation = location.getPath(); + return commentLocation.substring(commentLocation + .lastIndexOf('/') + 1); + } + + // TODO in beta3 of pact consumer a method is providing for this + public static String toJson(String jsonString) { + StringBuilder builder = new StringBuilder(); + boolean single_context = false; + for (int i = 0; i < jsonString.length(); i++) { + char ch = jsonString.charAt(i); + if (ch == '\\') { + i = i + 1; + if (i < jsonString.length()) { + ch = jsonString.charAt(i); + if (!(single_context && ch == '\'')) { + // unescape ' inside single quotes + builder.append('\\'); + } + } + } else if (ch == '\'') { + // Turn ' into ", for proper JSON string + ch = '"'; + single_context = !single_context; + } + builder.append(ch); + } + + return builder.toString(); + } + + // tag::test[] +} +// end::test[] \ No newline at end of file diff --git a/aggregator/c-tests/src/test/resources/arquillian.xml b/aggregator/c-tests/src/test/resources/arquillian.xml new file mode 100644 index 0000000..cecceee --- /dev/null +++ b/aggregator/c-tests/src/test/resources/arquillian.xml @@ -0,0 +1,16 @@ + + + + + + provider: folder + outputFolder: /tmp/mypacts + contractsFolder: target/pacts + + ${env.publishcontracts:true} + + + \ No newline at end of file diff --git a/aggregator/cp-tests/build.gradle b/aggregator/cp-tests/build.gradle new file mode 100644 index 0000000..64d7e05 --- /dev/null +++ b/aggregator/cp-tests/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'java' + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath "io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE" + classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.13.0' + } +} + +apply plugin: "io.spring.dependency-management" + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' + } +} + +repositories { + mavenCentral() + jcenter() +} + +test { + testLogging { + showStandardStreams = true + } +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.4.1' + + testCompile "io.specto:hoverfly-java:0.6.1" + + +} diff --git a/aggregator/cp-tests/src/test/java/book/aggr/CommentsGatewayTest.java b/aggregator/cp-tests/src/test/java/book/aggr/CommentsGatewayTest.java new file mode 100644 index 0000000..c284341 --- /dev/null +++ b/aggregator/cp-tests/src/test/java/book/aggr/CommentsGatewayTest.java @@ -0,0 +1,55 @@ +package book.aggr; + +import io.specto.hoverfly.junit.rule.HoverflyRule; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::test[] +public class CommentsGatewayTest { + + @ClassRule + public static HoverflyRule hoverfly = HoverflyRule + .inCaptureOrSimulationMode("simulation.json"); // <1> + + @Test + public void shouldInsertComments() + throws ExecutionException, InterruptedException { + + final JsonObject commentObject = Json.createObjectBuilder() + .add("comment", "This Game is Awesome").add("rate", + 5).add("gameId", 1234).build(); + + final CommentsGateway commentsGateway = new CommentsGateway + (); // <4> + commentsGateway.initRestClient("http://comments.gamers.com") + ; // <2> + + final Future comment = commentsGateway + .createComment(commentObject); + + final Response response = comment.get(); + final URI location = response.getLocation(); + + assertThat(location).isNotNull(); + final String id = extractId(location); + assertThat(id).matches("[0-9a-f]+"); // <3> + } + + // end::test[] + private String extractId(final URI location) { + final String commentLocation = location.getPath(); + return commentLocation.substring(commentLocation + .lastIndexOf('/') + 1); + } + // tag::test[] + +} +// end::test[] \ No newline at end of file diff --git a/aggregator/cp-tests/src/test/resources/hoverfly/simulation.json b/aggregator/cp-tests/src/test/resources/hoverfly/simulation.json new file mode 100644 index 0000000..b80f436 --- /dev/null +++ b/aggregator/cp-tests/src/test/resources/hoverfly/simulation.json @@ -0,0 +1,43 @@ +{ + "data" : { + "pairs" : [ { + "request" : { + "path" : { + "exactMatch" : "/comments" + }, + "method" : { + "exactMatch" : "POST" + }, + "destination" : { + "exactMatch" : "comments.gamers.com" + }, + "scheme" : { + "exactMatch" : "http" + }, + "query" : { + "exactMatch" : "" + }, + "body" : { + "jsonMatch" : "{\"comment\":\"This Game is Awesome\",\"rate\":5,\"gameId\":1234}" + } + }, + "response" : { + "status" : 201, + "encodedBody" : false, + "headers" : { + "Content-Length" : [ "0" ], + "Date" : [ "Thu, 15 Jun 2017 17:51:17 GMT" ], + "Hoverfly" : [ "Was-Here" ], + "Location" : [ "http://192.168.99.100:8080/commentsservice/5942c915c9e77c0001454df1" ], + "Server" : [ "Apache TomEE" ] + } + } + } ], + "globalActions" : { + "delays" : [ ] + } + }, + "meta" : { + "schemaVersion" : "v2" + } +} \ No newline at end of file diff --git a/aggregator/gradle/wrapper/gradle-wrapper.properties b/aggregator/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37b269c --- /dev/null +++ b/aggregator/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jul 28 09:17:27 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip diff --git a/aggregator/gradlew b/aggregator/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/aggregator/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/aggregator/gradlew.bat b/aggregator/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/aggregator/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/aggregator/i-tests/build.gradle b/aggregator/i-tests/build.gradle new file mode 100644 index 0000000..66e3c45 --- /dev/null +++ b/aggregator/i-tests/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'java' + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath "io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE" + classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.13.0' + } +} + +apply plugin: "io.spring.dependency-management" +apply plugin: 'mongo' + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' + } +} + +repositories { + mavenCentral() + jcenter() +} + +mongo { + port 27017 + logging 'console' +} + +test { + testLogging { + showStandardStreams = true + } + + runWithMongoDb = true +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.4.1' + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-container' + testCompile 'org.apache.tomee:arquillian-tomee-remote:7.0.3' + + testCompile 'org.jboss.shrinkwrap:shrinkwrap-api:1.2.6' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-spi:1.2.6' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6' + + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-gradle-depchain:2.2.2' + + testCompile 'com.github.tomakehurst:wiremock:2.1.9' + + +} diff --git a/aggregator/i-tests/src/test/java/book/aggr/CommentsGatewayTest.java b/aggregator/i-tests/src/test/java/book/aggr/CommentsGatewayTest.java new file mode 100644 index 0000000..cf6056e --- /dev/null +++ b/aggregator/i-tests/src/test/java/book/aggr/CommentsGatewayTest.java @@ -0,0 +1,72 @@ +package book.aggr; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.gradle.archive.importer.embedded + .EmbeddedGradleImporter; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.net.URL; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.fest.assertions.Assertions.assertThat; + +// tag::test[] +@RunWith(Arquillian.class) +public class CommentsGatewayTest { + + @Deployment(testable = false) // <1> + public static WebArchive createCommentsDeployment() { + return ShrinkWrap.create(EmbeddedGradleImporter.class, + CommentsGatewayTest.class.getName() + ".war") // <2> + .forProjectDirectory("../../comments") + .importBuildOutput().as(WebArchive.class); + } + + @ArquillianResource // <3> + private URL url; + + @Test + public void shouldInsertCommentsInCommentsService() throws + ExecutionException, InterruptedException { + + final JsonObject commentObject = Json.createObjectBuilder() + .add("comment", "This Game is Awesome").add("rate", + 5).add("gameId", 1234).build(); + + final CommentsGateway commentsGateway = new CommentsGateway + (); // <4> + commentsGateway.initRestClient(url.toString()); + + final Future comment = commentsGateway + .createComment(commentObject); + + final Response response = comment.get(); + final URI location = response.getLocation(); + + assertThat(location).isNotNull(); + final String id = extractId(location); + + assertThat(id).matches("[0-9a-f]+"); // <5> + + + } + + // end::test[] + private String extractId(final URI location) { + final String commentLocation = location.getPath(); + return commentLocation.substring(commentLocation + .lastIndexOf('/') + 1); + } + // tag::test[] +} +// end::test[] diff --git a/aggregator/i-tests/src/test/java/book/aggr/CommentsNegativeTest.java b/aggregator/i-tests/src/test/java/book/aggr/CommentsNegativeTest.java new file mode 100644 index 0000000..99cf382 --- /dev/null +++ b/aggregator/i-tests/src/test/java/book/aggr/CommentsNegativeTest.java @@ -0,0 +1,83 @@ +package book.aggr; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Rule; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Response; +import java.net.SocketTimeoutException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.fest.assertions.Assertions.assertThat; + +// tag::test[] +public class CommentsNegativeTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(8089); // <1> + + @Test + public void + shouldReturnAServerErrorInCaseOfStatus500WhenCreatingAComment() + throws ExecutionException, InterruptedException { + stubFor( // <2> + post(urlEqualTo("/comments")).willReturn(aResponse + ().withStatus(500).withBody("Exception " + + "during creation of comment"))); + + CommentsGateway commentsGateway = new CommentsGateway(); + commentsGateway.initRestClient("http://localhost:8089"); // + // <3> + + final JsonObject commentObject = Json.createObjectBuilder() + .add("comment", "This Game is Awesome").add("rate", + 5).add("gameId", 1234).build(); + + final Future comment = commentsGateway + .createComment(commentObject); + final Response response = comment.get(); + + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.getStatusInfo().getReasonPhrase()) + .isEqualTo("Server Error"); // <4> + } + + @Test + public void shouldThrowAnExceptionInCaseOfTimeout() throws + ExecutionException, InterruptedException { + stubFor(post(urlEqualTo("/comments")).willReturn(aResponse + ().withStatus(201).withHeader("Location", + "http://localhost:8089/comments/12345") + .withFixedDelay(1000) // <5> + )); + + CommentsGateway commentsGateway = new CommentsGateway(); + commentsGateway.initRestClient("http://localhost:8089"); + + final JsonObject commentObject = Json.createObjectBuilder() + .add("comment", "This Game is Awesome").add("rate", + 5).add("gameId", 1234).build(); + + final Future comment = commentsGateway + .createComment(commentObject); + + try { + comment.get(); + } catch (Exception e) { + assertThat(e).isInstanceOf(ExecutionException.class); + // <6> + final Throwable processingException = e.getCause(); + assertThat(processingException).isInstanceOf + (ProcessingException.class); + assertThat(processingException.getCause()).isInstanceOf + (SocketTimeoutException.class); + } + + } +} +// end::test[] diff --git a/aggregator/settings.gradle b/aggregator/settings.gradle new file mode 100644 index 0000000..17e432c --- /dev/null +++ b/aggregator/settings.gradle @@ -0,0 +1,5 @@ +include 'i-tests' +include 'c-tests' +include 'cp-tests' + +rootProject.name = 'aggr' diff --git a/aggregator/src/main/java/book/aggr/CommentsGateway.java b/aggregator/src/main/java/book/aggr/CommentsGateway.java new file mode 100644 index 0000000..320246f --- /dev/null +++ b/aggregator/src/main/java/book/aggr/CommentsGateway.java @@ -0,0 +1,72 @@ +package book.aggr; + +import org.glassfish.json.jaxrs.JsonStructureBodyReader; +import org.glassfish.json.jaxrs.JsonStructureBodyWriter; +import org.jvnet.hk2.annotations.Service; + +import javax.annotation.PreDestroy; +import javax.json.JsonObject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Optional; +import java.util.concurrent.Future; + +// tag::test[] +@Service // <1> +public class CommentsGateway { + + public static final String COMMENTS_SERVICE_URL = + "COMMENTS_SERVICE_URL"; + private Client client; + private WebTarget comments; + + public CommentsGateway() { // <2> + + // tag::env[] + String commentsHost = Optional.ofNullable(System.getenv + (COMMENTS_SERVICE_URL)) // <1> + .orElse(Optional.ofNullable(System.getProperty + ("COMMENTS_SERVICE_URL")).orElse + ("http://localhost:8282/comments-service/")); + // end::env[] + initRestClient(commentsHost); + } + + void initRestClient(final String host) { + this.client = ClientBuilder.newClient().property("jersey" + + ".config.client.connectTimeout", 2000).property + ("jersey.config.client.readTimeout", 2000); + this.comments = this.client.target(host); + } + + public Future createComment(final JsonObject comment) + { // <3> + return this.comments.path("comments").register + (JsonStructureBodyWriter.class) // <4> + .request(MediaType.APPLICATION_JSON).async() // <5> + .post(Entity.entity(comment, MediaType + .APPLICATION_JSON_TYPE)); // <6> + } + + public Future getCommentsFromCommentsService(final + long gameId) { + return this.comments.path("comments/{gameId}") + .resolveTemplate("gameId", gameId).register + (JsonStructureBodyReader.class).request + (MediaType.APPLICATION_JSON).async().get + (JsonObject.class); + } + + @PreDestroy + public void preDestroy() { + if (null != client) { + client.close(); + } + } +} +// end:test[] + diff --git a/aggregator/src/main/java/book/aggr/Futures.java b/aggregator/src/main/java/book/aggr/Futures.java new file mode 100644 index 0000000..9f012da --- /dev/null +++ b/aggregator/src/main/java/book/aggr/Futures.java @@ -0,0 +1,20 @@ +package book.aggr; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +public class Futures { + public static CompletableFuture toCompletable(final + Future + future, final Executor executor) { + return CompletableFuture.supplyAsync(() -> { + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }, executor); + } +} diff --git a/aggregator/src/main/java/book/aggr/GameAggregatorApplication.java b/aggregator/src/main/java/book/aggr/GameAggregatorApplication.java new file mode 100644 index 0000000..4ad18e0 --- /dev/null +++ b/aggregator/src/main/java/book/aggr/GameAggregatorApplication.java @@ -0,0 +1,29 @@ +package book.aggr; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.json.jaxrs.JsonStructureBodyReader; +import org.glassfish.json.jaxrs.JsonStructureBodyWriter; + +import javax.ws.rs.ApplicationPath; + +@ApplicationPath("/") +public class GameAggregatorApplication extends ResourceConfig { + + public GameAggregatorApplication() { + packages("book.aggr"); + register(JsonStructureBodyWriter.class); + register(JsonStructureBodyReader.class); + register(new AbstractBinder() { + @Override + protected void configure() { + bind(GamesGateway.class).to(GamesGateway.class); + bind(CommentsGateway.class).to(CommentsGateway.class); + } + }); + + } + + + +} diff --git a/aggregator/src/main/java/book/aggr/GamersResource.java b/aggregator/src/main/java/book/aggr/GamersResource.java new file mode 100644 index 0000000..facfc3a --- /dev/null +++ b/aggregator/src/main/java/book/aggr/GamersResource.java @@ -0,0 +1,63 @@ +package book.aggr; + +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Path("/") +public class GamersResource { + + // tag::aggr[] + + @Inject + private GamesGateway gamesGateway; // <1> + + @Inject + private CommentsGateway commentsGateway; + + private final Executor executor = Executors.newFixedThreadPool(8); + + @GET + @Path("{gameId}") + @Produces(MediaType.APPLICATION_JSON) + public void getGameInfo(@Suspended final AsyncResponse + asyncResponse, @PathParam + ("gameId") final long gameId) { + + asyncResponse.setTimeoutHandler(ar -> ar.resume(Response + .status(Response.Status.SERVICE_UNAVAILABLE).entity + ("TIME OUT !").build())); + asyncResponse.setTimeout(15, TimeUnit.SECONDS); + + final CompletableFuture gamesGatewayFuture = + Futures.toCompletable(gamesGateway + .getGameFromGamesService(gameId), executor); + final CompletableFuture commentsGatewayFuture = + Futures.toCompletable(commentsGateway + .getCommentsFromCommentsService(gameId), + executor); + + gamesGatewayFuture.thenCombine(commentsGatewayFuture, // <2> + (g, c) -> Json.createObjectBuilder() // <3> + .add("game", g).add("comments", c).build()) + .thenApply(info -> asyncResponse.resume(Response.ok + (info).build()) // <4> + ).exceptionally(asyncResponse::resume); + + } + + // end::aggr[] + +} diff --git a/aggregator/src/main/java/book/aggr/GamesGateway.java b/aggregator/src/main/java/book/aggr/GamesGateway.java new file mode 100644 index 0000000..d8a51b2 --- /dev/null +++ b/aggregator/src/main/java/book/aggr/GamesGateway.java @@ -0,0 +1,43 @@ +package book.aggr; + +import org.glassfish.json.jaxrs.JsonStructureBodyReader; +import org.jvnet.hk2.annotations.Service; + +import javax.json.JsonObject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.util.Optional; +import java.util.concurrent.Future; + +@Service +// tag::gateway[] +public class GamesGateway { + + private final Client client; + private final WebTarget games; + private final String gamesHost; + + public GamesGateway() { + this.client = ClientBuilder.newClient(); + + this.gamesHost = Optional.ofNullable(System.getenv + ("GAMES_SERVICE_URL")).orElse(Optional.ofNullable + (System.getProperty("GAMES_SERVICE_URL")).orElse + ("http://localhost:8181/")); + + this.games = this.client.target(gamesHost); // <1> + } + + public Future getGameFromGamesService(final long + gameId) { + return this.games.path("{gameId}").resolveTemplate + ("gameId", gameId) // <2> + .register(JsonStructureBodyReader.class) // <3> + .request(MediaType.APPLICATION_JSON).async() // <4> + .get(JsonObject.class); + } + +} +// end::gateway[] \ No newline at end of file diff --git a/aggregator/src/test/java/book/aggr/CommentsGatewayTest.java b/aggregator/src/test/java/book/aggr/CommentsGatewayTest.java new file mode 100644 index 0000000..33eb770 --- /dev/null +++ b/aggregator/src/test/java/book/aggr/CommentsGatewayTest.java @@ -0,0 +1,20 @@ +package book.aggr; + +import javax.inject.Inject; + +public class CommentsGatewayTest { + + @Inject + private CommentsGateway gateway; + + @org.junit.Test + public void createComment() throws Exception { + + } + + @org.junit.Test + public void getCommentsFromCommentsService() throws Exception { + + } + +} \ No newline at end of file diff --git a/comments/build.gradle b/comments/build.gradle new file mode 100644 index 0000000..8097c70 --- /dev/null +++ b/comments/build.gradle @@ -0,0 +1,63 @@ + +plugins { + id "io.spring.dependency-management" version "0.6.1.RELEASE" +} + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.13.Final' + mavenBom 'org.jboss.shrinkwrap:shrinkwrap-bom:1.2.6' + mavenBom 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-bom:2.2.6' + } +} + +apply plugin: 'war' + +repositories { + mavenCentral() +} + +group = 'org.gamer' +version = '1.0-SNAPSHOT' +war.archiveName = "commentsservice.war" + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + providedCompile 'javax:javaee-api:7.0' + + compile 'org.mongodb:mongodb-driver:3.2.2' + + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.4.1' + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-container' + testCompile 'org.apache.tomee:arquillian-tomee-remote:7.0.2' + + testCompile 'org.jboss.shrinkwrap:shrinkwrap-api' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-spi' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-impl-base' + + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven-archive' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven-archive' + + testCompile 'org.jboss.arquillian.extension:arquillian-rest-client-api:1.0.0.Alpha4' + testCompile 'org.jboss.arquillian.extension:arquillian-rest-client-impl-3x:1.0.0.Alpha4' + + testCompile 'org.jboss.arquillian.extension:arquillian-warp-api:1.0.0.Alpha4' + testCompile 'org.jboss.arquillian.extension:arquillian-rest-warp-impl-jaxrs-2.0:1.0.0.Alpha4' + +} + +test { + systemProperty "tomee.classifier", "plus" +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} diff --git a/comments/c-tests/build.gradle b/comments/c-tests/build.gradle new file mode 100644 index 0000000..bd9c967 --- /dev/null +++ b/comments/c-tests/build.gradle @@ -0,0 +1,61 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.13.0' + classpath "io.spring.gradle:dependency-management-plugin:1.0.0.RELEASE" + } +} + +apply plugin: 'java' +apply plugin: "io.spring.dependency-management" +apply plugin: 'mongo' + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.12.Final' + mavenBom 'org.jboss.shrinkwrap:shrinkwrap-bom:1.2.6' + mavenBom 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-bom:2.2.4' + } +} + +repositories { + mavenCentral() +} + +test { + testLogging { + showStandardStreams = true + } + + runWithMongoDb = true +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.arquillian.algeron:arquillian-algeron-pact-provider-core:1.0.1' + testCompile 'au.com.dius:pact-jvm-provider_2.11:3.5.0' + testCompile 'org.arquillian.algeron:arquillian-algeron-pact-provider-assertj:1.0.1' + testCompile 'org.assertj:assertj-core:3.8.0' + testCompile 'io.rest-assured:rest-assured:3.0.2' + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-container' + testCompile 'org.apache.tomee:arquillian-tomee-remote:7.0.2' + testCompile ('com.lordofthejars:nosqlunit-mongodb:0.10.0') + + + testCompile 'org.jboss.shrinkwrap:shrinkwrap-api' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-spi' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-impl-base' + + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven-archive' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven-archive' +} diff --git a/comments/c-tests/src/test/java/book/comments/boundary/CommentsProviderTest.java b/comments/c-tests/src/test/java/book/comments/boundary/CommentsProviderTest.java new file mode 100644 index 0000000..c9fae04 --- /dev/null +++ b/comments/c-tests/src/test/java/book/comments/boundary/CommentsProviderTest.java @@ -0,0 +1,128 @@ +package book.comments.boundary; + +import book.comments.MongoClientProvider; +import com.lordofthejars.nosqlunit.annotation.UsingDataSet; +import com.lordofthejars.nosqlunit.core.LoadStrategyEnum; +import com.lordofthejars.nosqlunit.mongodb.ManagedMongoDb; +import com.lordofthejars.nosqlunit.mongodb.MongoDbRule; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.arquillian.algeron.pact.provider.assertj + .PactProviderAssertions; +import org.arquillian.algeron.pact.provider.spi.Provider; +import org.arquillian.algeron.pact.provider.spi.State; +import org.arquillian.algeron.pact.provider.spi.Target; +import org.arquillian.algeron.provider.core.retriever.ContractsFolder; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.MalformedURLException; +import java.net.URL; + +import static com.lordofthejars.nosqlunit.mongodb.ManagedMongoDb + .MongoServerRuleBuilder.newManagedMongoDbRule; +import static com.lordofthejars.nosqlunit.mongodb + .MongoDbConfigurationBuilder.mongoDb; + +// tag::test[] + +@RunWith(Arquillian.class) +@ContractsFolder(value = "/tmp/mypacts") // <1> +@Provider("comments_service") // <2> +public class CommentsProviderTest { + + static { // <3> + System.setProperty("MONGO_HOME", + "/mongodb-osx-x86_64-3.2.7"); + } + + @ClassRule // <4> + public static ManagedMongoDb managedMongoDb = + newManagedMongoDbRule().build(); + + @Rule // <5> + public MongoDbRule remoteMongoDbRule = new MongoDbRule(mongoDb + ().databaseName("test").host("localhost").build()); + + @Deployment(testable = false) // <6> + public static WebArchive createDeployment() { + final WebArchive webArchive = ShrinkWrap.create(WebArchive + .class).addPackage(CommentsResource.class + .getPackage()).addClass(MongoClientProvider.class) + .addAsWebInfResource("test-resources.xml", + "resources.xml").addAsWebInfResource + (EmptyAsset.INSTANCE, "beans.xml") + .addAsLibraries(Maven.resolver().resolve("org" + + ".mongodb:mongodb-driver:3.2.2") + .withTransitivity().as(JavaArchive.class)); + + return webArchive; + } + + private static final String commentObject = "{" + " 'comment' " + + ": '%s'," + " 'rate' : %d," + " 'gameId': %d" + "}"; + + @State("A game with id (\\d+) with rate (\\d+) and message (.+)") + public void insertGame(int gameId, int rate, String message) + throws MalformedURLException { // <7> + + RestAssured.given().body(toJson(String.format + (commentObject, message, rate, gameId))) + .contentType(ContentType.JSON).post(new URL + (commentsService, "comments")).then().statusCode(201); + + } + + @ArquillianResource + URL commentsService; + + @ArquillianResource // <8> + Target target; + + @Test + @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) // <9> + public void should_provide_valid_answers() { + PactProviderAssertions.assertThat(target).withUrl + (commentsService).satisfiesContract(); + } + + // end::test[] + + public static String toJson(String jsonString) { + StringBuilder builder = new StringBuilder(); + boolean single_context = false; + for (int i = 0; i < jsonString.length(); i++) { + char ch = jsonString.charAt(i); + if (ch == '\\') { + i = i + 1; + if (i < jsonString.length()) { + ch = jsonString.charAt(i); + if (!(single_context && ch == '\'')) { + // unescape ' inside single quotes + builder.append('\\'); + } + } + } else if (ch == '\'') { + // Turn ' into ", for proper JSON string + ch = '"'; + single_context = !single_context; + } + builder.append(ch); + } + + return builder.toString(); + } + + // tag::test[] +} +// end::test[] \ No newline at end of file diff --git a/comments/c-tests/src/test/resources/test-resources.xml b/comments/c-tests/src/test/resources/test-resources.xml new file mode 100644 index 0000000..e31c958 --- /dev/null +++ b/comments/c-tests/src/test/resources/test-resources.xml @@ -0,0 +1,11 @@ + + + + address = ${mongodb.address:-localhost} + port = ${mongodb.port:-27017} + database = ${mongodb.database:-test} + + + \ No newline at end of file diff --git a/comments/gradle/wrapper/gradle-wrapper.properties b/comments/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fc8a50d --- /dev/null +++ b/comments/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 10 09:54:44 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip diff --git a/comments/gradlew b/comments/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/comments/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/comments/gradlew.bat b/comments/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/comments/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/comments/i-tests/build.gradle b/comments/i-tests/build.gradle new file mode 100644 index 0000000..9587fe7 --- /dev/null +++ b/comments/i-tests/build.gradle @@ -0,0 +1,63 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.sourcemuse.gradle.plugin:gradle-mongo-plugin:0.13.0' + classpath "io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE" + } +} + +apply plugin: 'java' +apply plugin: "io.spring.dependency-management" +apply plugin: 'mongo' + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.11.Final' + mavenBom 'org.jboss.shrinkwrap:shrinkwrap-bom:1.2.6' + mavenBom 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-bom:2.2.4' + } +} + +repositories { + mavenCentral() +} + +mongo { + port 27017 + logging 'console' +} + +test { + testLogging { + showStandardStreams = true + } + + runWithMongoDb = true +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.4.1' + testCompile group: 'org.jboss.arquillian.junit', name: 'arquillian-junit-container' + testCompile 'org.apache.tomee:arquillian-tomee-remote:7.0.1' + + testCompile ('com.lordofthejars:nosqlunit-mongodb:0.10.0') + + + testCompile 'org.jboss.shrinkwrap:shrinkwrap-api' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-spi' + testCompile 'org.jboss.shrinkwrap:shrinkwrap-impl-base' + + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-spi-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-api-maven-archive' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven' + testCompile 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-impl-maven-archive' +} diff --git a/comments/i-tests/src/test/java/book/comments/CommentsTest.java b/comments/i-tests/src/test/java/book/comments/CommentsTest.java new file mode 100644 index 0000000..c11bdb2 --- /dev/null +++ b/comments/i-tests/src/test/java/book/comments/CommentsTest.java @@ -0,0 +1,72 @@ +package book.comments; + +import book.comments.boundary.Comments; +import com.lordofthejars.nosqlunit.annotation.ShouldMatchDataSet; +import com.lordofthejars.nosqlunit.annotation.UsingDataSet; +import com.lordofthejars.nosqlunit.core.LoadStrategyEnum; +import com.lordofthejars.nosqlunit.mongodb.MongoDbRule; +import org.bson.Document; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; + +import static com.lordofthejars.nosqlunit.mongodb + .MongoDbConfigurationBuilder.mongoDb; + +//tag::test[] +@RunWith(Arquillian.class) // <1> +public class CommentsTest { + + @Deployment + public static WebArchive createDeployment() { + final WebArchive webArchive = ShrinkWrap.create(WebArchive + .class).addClasses(Comments.class, + MongoClientProvider.class).addAsWebInfResource + (EmptyAsset.INSTANCE, "beans.xml").addAsLibraries( + // <2> + Maven.resolver().resolve("org" + + ".mongodb:mongodb-driver:3.2.2", "com" + + ".lordofthejars:nosqlunit-mongodb:0.10.0") + .withTransitivity().as(JavaArchive.class)) + .addAsWebInfResource("resources-test.xml", + "resources.xml") // <3> + .addAsWebInfResource("expected-insert-comments" + + ".json", + "classes/book/comments/expected-insert" + + "-comments.json"); // <4> + + + return webArchive; + } + + @Rule //<5> + public MongoDbRule remoteMongoDbRule = new MongoDbRule(mongoDb + ().databaseName("test").host("localhost").build()); + + + @Inject // <6> + private Comments comments; + + @Test + @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) // <7> + @ShouldMatchDataSet(location = "expected-insert-comments.json") + // <8> + public void shouldInsertAComment() { + final Document document = new Document("comment", "This " + + "Game is Awesome").append("rate", 5).append + ("gameId", 1); + + comments.createComment(document); + } + +} +//end::test[] diff --git a/comments/i-tests/src/test/resources/expected-insert-comments.json b/comments/i-tests/src/test/resources/expected-insert-comments.json new file mode 100644 index 0000000..10a347d --- /dev/null +++ b/comments/i-tests/src/test/resources/expected-insert-comments.json @@ -0,0 +1,9 @@ +{ + "comments": [ + { + "comment" : "This Game is Awesome", + "rate": 5, + "gameId": 1 + } + ] +} \ No newline at end of file diff --git a/comments/i-tests/src/test/resources/resources-test.xml b/comments/i-tests/src/test/resources/resources-test.xml new file mode 100644 index 0000000..3c8c472 --- /dev/null +++ b/comments/i-tests/src/test/resources/resources-test.xml @@ -0,0 +1,11 @@ + + + + address = localhost + port = 27017 + database = test + + + \ No newline at end of file diff --git a/comments/settings.gradle b/comments/settings.gradle new file mode 100644 index 0000000..4ef93b3 --- /dev/null +++ b/comments/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'comments' + +include 'i-tests' +include 'c-tests' diff --git a/comments/src/main/java/book/comments/MongoClientProvider.java b/comments/src/main/java/book/comments/MongoClientProvider.java new file mode 100644 index 0000000..9545431 --- /dev/null +++ b/comments/src/main/java/book/comments/MongoClientProvider.java @@ -0,0 +1,26 @@ +package book.comments; + +import com.mongodb.MongoClient; + +/** + * Provides the MongoClient + */ +public class MongoClientProvider { + + private final MongoClient mongoClient; + private final String database; + + public MongoClientProvider(final String address, final int + port, final String database) { + this.mongoClient = new MongoClient(address, port); + this.database = database; + } + + public MongoClient getMongoClient() { + return this.mongoClient; + } + + public String getDatabase() { + return database; + } +} diff --git a/comments/src/main/java/book/comments/boundary/Comments.java b/comments/src/main/java/book/comments/boundary/Comments.java new file mode 100644 index 0000000..9541e69 --- /dev/null +++ b/comments/src/main/java/book/comments/boundary/Comments.java @@ -0,0 +1,57 @@ +package book.comments.boundary; + +import book.comments.MongoClientProvider; +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoCollection; +import org.bson.Document; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.enterprise.context.Dependent; +import java.util.Arrays; +import java.util.Optional; + +// tag::test[] +@Dependent +public class Comments { + + private static final String COMMENTS_COLLECTION = "comments"; + private static final String MATCH = "{$match:{gameId: %s}}"; + private static final String GROUP = "{ $group : " + " " + + " { _id : \"$gameId\", " + " comments: { " + + "$push: \"$comment\" }, " + " rate: { $avg: " + + "\"$rate\"} " + " count: { $sum: 1 } " + " " + + " }" + "}"; + + @Resource(name = "mongodb") + private MongoClientProvider mongoDbProvider; + + private MongoCollection commentsCollection; + + @PostConstruct + public void initComentsCollection() { + commentsCollection = mongoDbProvider.getMongoClient() + .getDatabase(mongoDbProvider.getDatabase()) + .getCollection(COMMENTS_COLLECTION); + } + + public String createComment(final Document comment) { + commentsCollection.insertOne(comment); + return comment.getObjectId("_id").toHexString(); + } + + public Optional getCommentsAndRating(final int gameId) { + final AggregateIterable result = + commentsCollection.aggregate + (createAggregationExpression(gameId)); + return Optional.ofNullable(result.first()); + } + + private java.util.List createAggregationExpression + (final int gameId) { + return Arrays.asList(Document.parse(String.format(MATCH, + gameId)), Document.parse(GROUP)); + } + +} +// end::test[] diff --git a/comments/src/main/java/book/comments/boundary/CommentsInternalResource.java b/comments/src/main/java/book/comments/boundary/CommentsInternalResource.java new file mode 100644 index 0000000..7ab4a81 --- /dev/null +++ b/comments/src/main/java/book/comments/boundary/CommentsInternalResource.java @@ -0,0 +1,40 @@ +package book.comments.boundary; + +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.ejb.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +@Path("/comments") +@Singleton +@Lock(LockType.READ) +public class CommentsInternalResource { + + @GET + @Path("/version") + @Produces(MediaType.TEXT_PLAIN) + public Response getServiceVersion() throws IOException { + final InputStream manifestStream = CommentsInternalResource + .class.getResourceAsStream("/META-INF/MANIFEST.MF"); + final Manifest manifest = new Manifest(); + manifest.read(manifestStream); + final Attributes mainAttribs = manifest.getMainAttributes(); + final String version = mainAttribs.getValue + ("Implementation-Version"); + + if (version != null) { + return Response.ok(version).build(); + } else { + return Response.ok().build(); + } + } + +} diff --git a/comments/src/main/java/book/comments/boundary/CommentsResource.java b/comments/src/main/java/book/comments/boundary/CommentsResource.java new file mode 100644 index 0000000..3673ccd --- /dev/null +++ b/comments/src/main/java/book/comments/boundary/CommentsResource.java @@ -0,0 +1,59 @@ +package book.comments.boundary; + +import org.bson.Document; + +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.ejb.Singleton; +import javax.inject.Inject; +import javax.json.JsonObject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Optional; + +// tag::jaxrs[] +@Path("/comments") // <1> +@Singleton +@Lock(LockType.READ) +public class CommentsResource { + + @Inject + private Comments comments; + + @Inject + private DocumentToJsonObject transformer; + + @GET // <2> + @Path("/{gameId}") + @Produces(MediaType.APPLICATION_JSON) // <3> + public Response getCommentsOfGivenGame(@PathParam("gameId") + final Integer + gameId) { // + // <4> + + final Optional commentsAndRating = comments + .getCommentsAndRating(gameId); + + final JsonObject json = transformer.transform + (commentsAndRating.orElse(new Document())); + return Response.ok(json).build(); // <5> + } + // end::jaxrs[] + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response createComment(final JsonObject jsonObject) { + + final Document commentDocument = transformer.transform + (jsonObject); + comments.createComment(commentDocument); + + return Response.created(URI.create(commentDocument + .getObjectId("_id").toHexString())).build(); + } + + // tag::jaxrs[] +} +// end::jaxrs[] \ No newline at end of file diff --git a/comments/src/main/java/book/comments/boundary/DocumentToJsonObject.java b/comments/src/main/java/book/comments/boundary/DocumentToJsonObject.java new file mode 100644 index 0000000..119eb37 --- /dev/null +++ b/comments/src/main/java/book/comments/boundary/DocumentToJsonObject.java @@ -0,0 +1,54 @@ +package book.comments.boundary; + + +import org.bson.Document; + +import javax.enterprise.context.ApplicationScoped; +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import java.util.List; + +//DataMapper +@ApplicationScoped +public class DocumentToJsonObject { + + public JsonObject transform(final Document document) { + + if (document.containsKey("comments") && document + .containsKey("rate")) { + + final JsonArrayBuilder messagesJsonBuilder = Json + .createArrayBuilder(); + final List comments = (List) document + .get("comments"); + comments.forEach(messagesJsonBuilder::add); + + final JsonObjectBuilder commentsJsonBuilder = Json + .createObjectBuilder().add("rate", document + .getDouble("rate")).add("total", + document.getInteger("count")).add + ("comments", messagesJsonBuilder); + + return commentsJsonBuilder.build(); + } else { + return Json.createObjectBuilder().build(); + } + } + + public Document transform(final JsonObject comment) { + if (comment.containsKey("comment") && comment.containsKey + ("rate") && comment.containsKey("gameId")) { + + return new Document("comment", comment.getString + ("comment")).append("rate", comment.getInt + ("rate")).append("gameId", comment.getInt + ("gameId")); + } else { + throw new IllegalArgumentException("A comments does not" + + " contains the mandatory fields comment and " + + "rate."); + } + } +} diff --git a/comments/src/main/java/book/comments/boundary/JaxRsApplication.java b/comments/src/main/java/book/comments/boundary/JaxRsApplication.java new file mode 100644 index 0000000..0f23c77 --- /dev/null +++ b/comments/src/main/java/book/comments/boundary/JaxRsApplication.java @@ -0,0 +1,20 @@ +/** + * Tomitribe Confidential + *

+ * Copyright(c) Tomitribe Corporation. 2014 + *

+ * The source code for this program is not published or otherwise + * divested + * of its trade secrets, irrespective of what has been deposited + * with the + * U.S. Copyright Office. + *

+ */ +package book.comments.boundary; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/comments/src/main/webapp/WEB-INF/resources.xml b/comments/src/main/webapp/WEB-INF/resources.xml new file mode 100644 index 0000000..e31c958 --- /dev/null +++ b/comments/src/main/webapp/WEB-INF/resources.xml @@ -0,0 +1,11 @@ + + + + address = ${mongodb.address:-localhost} + port = ${mongodb.port:-27017} + database = ${mongodb.database:-test} + + + \ No newline at end of file diff --git a/comments/src/test/java/book/comments/boundary/CommentsResourceTest.java b/comments/src/test/java/book/comments/boundary/CommentsResourceTest.java new file mode 100644 index 0000000..438d322 --- /dev/null +++ b/comments/src/test/java/book/comments/boundary/CommentsResourceTest.java @@ -0,0 +1,54 @@ +package book.comments.boundary; + +import book.comments.MongoClientProvider; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.extension.rest.client + .ArquillianResteasyResource; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.ws.rs.core.Response; + +// tag::test[] +@RunWith(Arquillian.class) // <1> +public class CommentsResourceTest { + + @Deployment(testable = false) // <2> + public static WebArchive createDeployment() { + final WebArchive webArchive = ShrinkWrap.create(WebArchive + .class).addPackage(CommentsResource.class + .getPackage()).addClass(MongoClientProvider.class) + .addAsWebInfResource("test-resources.xml", + "resources.xml").addAsWebInfResource + (EmptyAsset.INSTANCE, "beans.xml") + .addAsLibraries(Maven.resolver().resolve("org" + + ".mongodb:mongodb-driver:3.2.2") + .withTransitivity().as(JavaArchive.class)); + + System.out.println("webArchive = " + webArchive.toString + (true)); + + return webArchive; + } + + @Test + public void getCommentsOfGivenGame(@ArquillianResteasyResource + final CommentsResource + resource) throws + Exception { // <3> + Assert.assertNotNull(resource); + + final Response game = resource.getCommentsOfGivenGame(1); + // <4> + Assert.assertNotNull(game); + } + +} +// end::test[] \ No newline at end of file diff --git a/comments/src/test/java/book/comments/boundary/CommentsTest.java b/comments/src/test/java/book/comments/boundary/CommentsTest.java new file mode 100644 index 0000000..954be76 --- /dev/null +++ b/comments/src/test/java/book/comments/boundary/CommentsTest.java @@ -0,0 +1,26 @@ +package book.comments.boundary; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +// tag::test[] +@RunWith(Arquillian.class) // <1> +public class CommentsTest { + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, CommentsTest + .class.getSimpleName() + ".war").addClasses + (Comments.class); + } + + @Test + public void doNothing() { + + } + +} +// end::test[] \ No newline at end of file diff --git a/comments/src/test/java/book/comments/boundary/CommentsWarpTest.java b/comments/src/test/java/book/comments/boundary/CommentsWarpTest.java new file mode 100644 index 0000000..e8949f3 --- /dev/null +++ b/comments/src/test/java/book/comments/boundary/CommentsWarpTest.java @@ -0,0 +1,114 @@ +package book.comments.boundary; + +import book.comments.MongoClientProvider; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OverProtocol; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.extension.rest.warp.api.HttpMethod; +import org.jboss.arquillian.extension.rest.warp.api.RestContext; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.warp.Inspection; +import org.jboss.arquillian.warp.Warp; +import org.jboss.arquillian.warp.WarpTest; +import org.jboss.arquillian.warp.servlet.AfterServlet; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.plugins.providers.RegisterBuiltin; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.ws.rs.core.Response; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +// tag::test[] +@WarpTest // <1> +@RunWith(Arquillian.class) +public class CommentsWarpTest { + + @BeforeClass + public static void beforeClass() { // <2> + // initializes the rest easy client framework + RegisterBuiltin.register(ResteasyProviderFactory + .getInstance()); + } + + @Deployment + @OverProtocol("Servlet 3.0") // <3> + public static WebArchive createDeployment() { + final WebArchive webArchive = ShrinkWrap.create(WebArchive + .class).addPackage(CommentsResource.class + .getPackage()).addClass(MongoClientProvider.class) + .addAsWebInfResource("test-resources.xml", + "resources.xml").addAsWebInfResource + (EmptyAsset.INSTANCE, "beans.xml") + .addAsLibraries(Maven.resolver().resolve("org" + + ".mongodb:mongodb-driver:3.2.2") + .withTransitivity().as(JavaArchive.class)); + + System.out.println("webArchive = " + webArchive.toString + (true)); + + return webArchive; + } + + private CommentsResource resource; + + @ArquillianResource + private URL contextPath; // <4> + + @Before + public void before() { // <5> + final ResteasyClient client = new ResteasyClientBuilder() + .build(); + final ResteasyWebTarget target = client.target(contextPath + .toExternalForm()); + resource = target.proxy(CommentsResource.class); + } + + @Test + @RunAsClient // <6> + public void getCommentsOfGivenGame() { + + Warp.initiate(() -> { // <7> + + final Response commentsOfGivenGame = resource + .getCommentsOfGivenGame(1); + Assert.assertNotNull(commentsOfGivenGame); + + }).inspect(new Inspection() { + + private static final long serialVersionUID = 1L; + + @ArquillianResource + private RestContext restContext; // <8> + + @AfterServlet // <9> + public void testGetCommentsOfGivenGame() { + + assertEquals(HttpMethod.GET, restContext + .getHttpRequest().getMethod()); // <10> + assertEquals(200, restContext.getHttpResponse() + .getStatusCode()); + assertEquals("application/json", restContext + .getHttpResponse().getContentType()); + assertNotNull(restContext.getHttpResponse() + .getEntity()); + } + }); + } +} +// end::test[] \ No newline at end of file diff --git a/comments/src/test/resources/test-resources.xml b/comments/src/test/resources/test-resources.xml new file mode 100644 index 0000000..e31c958 --- /dev/null +++ b/comments/src/test/resources/test-resources.xml @@ -0,0 +1,11 @@ + + + + address = ${mongodb.address:-localhost} + port = ${mongodb.port:-27017} + database = ${mongodb.database:-test} + + + \ No newline at end of file diff --git a/game-aggregator-service/gradle/wrapper/gradle-wrapper.properties b/game-aggregator-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8cb4c39 --- /dev/null +++ b/game-aggregator-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jun 15 15:11:35 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip diff --git a/game-aggregator-service/gradlew b/game-aggregator-service/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/game-aggregator-service/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/game-aggregator-service/gradlew.bat b/game-aggregator-service/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/game-aggregator-service/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/game/pom.xml b/game/pom.xml new file mode 100644 index 0000000..1ce52b2 --- /dev/null +++ b/game/pom.xml @@ -0,0 +1,198 @@ + + + + 4.0.0 + org.gamer + game + 1.0.0-SNAPSHOT + + war + + + 2017.4.0 + + 1.8 + 1.8 + false + UTF-8 + + + + + + + + org.wildfly.swarm + bom + ${version.wildfly.swarm} + import + pom + + + + org.jboss.arquillian + arquillian-bom + 1.1.13.Final + import + pom + + + + + + + + gameservice + + + org.wildfly.swarm + wildfly-swarm-plugin + ${version.wildfly.swarm} + + + + package + + + + + + + + + + + javax + javaee-api + 7.0 + provided + + + + org.wildfly.swarm + cdi + + + org.wildfly.swarm + ejb + + + org.wildfly.swarm + jaxrs-cdi + + + slf4j-api + org.slf4j + + + + + org.wildfly.swarm + jaxrs-jsonp + + + org.wildfly.swarm + jaxrs-validator + + + org.wildfly.swarm + jpa + + + org.wildfly.swarm + datasources + + + com.h2database + h2 + 1.4.195 + + + + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + + org.wildfly.swarm + arquillian + 2017.4.0 + test + + + + + + com.github.tomakehurst + wiremock + 2.2.1 + test + + + slf4j-api + org.slf4j + + + junit + junit + + + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 3.5.2 + test + + + org.mockito + mockito-core + 2.8.47 + test + + + org.jboss.resteasy + resteasy-client + 3.0.21.Final + test + + + org.jboss.resteasy + resteasy-jackson-provider + 3.0.21.Final + test + + + org.jboss.logging + jboss-logging + 3.3.0.Final + + + commons-io + commons-io + 2.5 + + + com.thetransactioncompany + cors-filter + 2.1 + + + org.slf4j + slf4j-simple + 1.7.21 + test + + + + diff --git a/game/src/main/java/book/games/Main.java b/game/src/main/java/book/games/Main.java new file mode 100644 index 0000000..a64ff6d --- /dev/null +++ b/game/src/main/java/book/games/Main.java @@ -0,0 +1,65 @@ +package book.games; + +import book.games.boundary.ExecutorServiceProducer; +import book.games.boundary.Games; +import book.games.boundary.GamesResource; +import book.games.boundary.IgdbGateway; +import book.games.control.GamesService; +import book.games.entity.Game; +import book.games.entity.LocalDatePersistenceConverter; +import book.games.entity.ReleaseDate; +import book.games.entity.SearchResult; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.ClassLoaderAsset; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.wildfly.swarm.Swarm; +import org.wildfly.swarm.jaxrs.JAXRSArchive; +import org.wildfly.swarm.jpa.JPAFraction; + +public class Main { + + public static Swarm createSwarm() throws Exception { + + final Swarm swarm = new Swarm(); + + // Prevent JPA Fraction from installing it's default + // datasource fraction + swarm.fraction(new JPAFraction().defaultDatasource + ("jboss/datasources/games")); + + return swarm; + } + + public static JAXRSArchive createJAXRSArchive() throws Exception { + final JAXRSArchive deployment = ShrinkWrap.create + (JAXRSArchive.class); + deployment.addClass(GamesResource.class); + deployment.addClass(Games.class); + deployment.addClass(Game.class); + deployment.addClass(GamesService.class); + deployment.addClass(LocalDatePersistenceConverter.class); + deployment.addClass(ReleaseDate.class); + deployment.addClass(IgdbGateway.class); + deployment.addClass(SearchResult.class); + deployment.addClass(ExecutorServiceProducer.class); + deployment.addAsWebInfResource(new ClassLoaderAsset + ("META-INF/persistence.xml", Main.class + .getClassLoader()), + "classes/META-INF/persistence.xml"); + deployment.addAsWebInfResource(EmptyAsset.INSTANCE, "beans" + + ".xml"); + + deployment.addAllDependencies(); + + return deployment; + } + + public static void main(final String... args) throws Exception { + + final Swarm swarm = createSwarm(); + final JAXRSArchive deployment = createJAXRSArchive(); + + swarm.start().deploy(deployment); + + } +} diff --git a/game/src/main/java/book/games/boundary/ExecutorServiceProducer.java b/game/src/main/java/book/games/boundary/ExecutorServiceProducer.java new file mode 100644 index 0000000..b907837 --- /dev/null +++ b/game/src/main/java/book/games/boundary/ExecutorServiceProducer.java @@ -0,0 +1,17 @@ +package book.games.boundary; + +import javax.annotation.Resource; +import javax.enterprise.concurrent.ManagedExecutorService; +import javax.enterprise.context.ApplicationScoped; +import java.util.concurrent.ExecutorService; + +@ApplicationScoped +public class ExecutorServiceProducer { + + @Resource + ManagedExecutorService managedExecutorService; + + public ExecutorService getManagedExecutorService() { + return managedExecutorService; + } +} diff --git a/game/src/main/java/book/games/boundary/Games.java b/game/src/main/java/book/games/boundary/Games.java new file mode 100644 index 0000000..657fbb4 --- /dev/null +++ b/game/src/main/java/book/games/boundary/Games.java @@ -0,0 +1,40 @@ +package book.games.boundary; + + +import book.games.entity.Game; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.Optional; + +// tag::repo[] +@Stateless // <1> +public class Games { + + @PersistenceContext // <2> + EntityManager em; + + public Long create(final Game request) { + final Game game = em.merge(request); // <3> + return game.getId(); + } + + public Optional findGameById(final Long gameId) { + Optional g = Optional.ofNullable(em.find(Game.class, + gameId)); + + if (g.isPresent()) { + //Force load of lazy collections before detach + Game game = g.get(); + game.getReleaseDates().size(); + game.getPublishers().size(); + game.getDevelopers().size(); + em.detach(game); + } + + return g; + } + +} +// end::repo[] \ No newline at end of file diff --git a/game/src/main/java/book/games/boundary/GamesResource.java b/game/src/main/java/book/games/boundary/GamesResource.java new file mode 100644 index 0000000..4ae734b --- /dev/null +++ b/game/src/main/java/book/games/boundary/GamesResource.java @@ -0,0 +1,111 @@ +package book.games.boundary; + +import book.games.control.GamesService; +import book.games.entity.Game; +import book.games.entity.SearchResult; + +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.validation.constraints.NotNull; +import javax.ws.rs.*; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collector; + +// tag::jaxrs[] +@Path("/") +@javax.ejb.Singleton // <1> +@Lock(LockType.READ) +public class GamesResource { + + @Inject + GamesService gamesService; + + @Inject // <2> + ExecutorServiceProducer managedExecutorService; + + @GET + @Produces(MediaType.APPLICATION_JSON) + @javax.ejb.Asynchronous // <3> + public void searchGames(@Suspended final AsyncResponse + response, // <4> + @NotNull @QueryParam("query") final + String query) { + + response.setTimeoutHandler(asyncResponse -> asyncResponse + .resume(Response.status(Response.Status + .SERVICE_UNAVAILABLE).entity("TIME OUT !") + .build())); + response.setTimeout(15, TimeUnit.SECONDS); + + managedExecutorService.getManagedExecutorService().submit( + () -> { // <5> + try { + + final Collector + jsonCollector = Collector.of + (Json::createArrayBuilder, + JsonArrayBuilder::add, (left, + right) -> { + left.add(right); + return left; + }); + + final List searchResults = + gamesService.searchGames(query); + + final JsonArrayBuilder mappedGames = searchResults + .stream().map(SearchResult::convertToJson) + .collect(jsonCollector); + + final Response.ResponseBuilder ok = Response.ok + (mappedGames.build()); + response.resume(ok.build()); // <6> + } catch (final Throwable e) { + response.resume(e); // <7> + } + }); + } + // end::jaxrs[] + + @GET + @Path("{gameId}") + @Produces(MediaType.APPLICATION_JSON) + @javax.ejb.Asynchronous + public void searchGameById(@Suspended final AsyncResponse + response, @PathParam + ("gameId") final long gameId) { + + response.setTimeoutHandler(asyncResponse -> asyncResponse + .resume(Response.status(Response.Status + .SERVICE_UNAVAILABLE).entity("TIME OUT !") + .build())); + response.setTimeout(15, TimeUnit.SECONDS); + + managedExecutorService.getManagedExecutorService().submit( + () -> { + try { + final Game game = gamesService.searchGameById(gameId); + if (game != null) { + response.resume(Response.ok(game.convertToJson + ()).build()); + } else { + response.resume(Response.status(404)); + } + } catch (final Throwable e) { + response.resume(e); + } + }); + } + + // tag::jaxrs[] +} +// end::jaxrs[] \ No newline at end of file diff --git a/game/src/main/java/book/games/boundary/IgdbGateway.java b/game/src/main/java/book/games/boundary/IgdbGateway.java new file mode 100644 index 0000000..5331387 --- /dev/null +++ b/game/src/main/java/book/games/boundary/IgdbGateway.java @@ -0,0 +1,94 @@ +package book.games.boundary; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.ejb.Singleton; +import javax.json.Json; +import javax.json.JsonArray; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +@Singleton +@Lock(LockType.READ) +public class IgdbGateway { + + private Client client; + private String apiKey; + + private WebTarget igdb; + + @PostConstruct + public void postConstruct() { + + this.apiKey = Optional.ofNullable(System.getenv + ("IGDB_API_KEY")).orElse(Optional.ofNullable(System + .getProperty("IGDB_API_KEY")).orElse("dummy")); + + final String host = Optional.ofNullable(System.getenv + ("IGDB_HOST")).orElse(Optional.ofNullable(System + .getProperty("IGDB_HOST")).orElse + ("https://igdbcom-internet-game-database-v1.p" + "" + + ".mashape.com")); + + this.client = ClientBuilder.newClient(); + this.igdb = this.client.target(host); + } + + @PreDestroy + private void preDestroy() { + if (null != this.client) { + try { + this.client.close(); + } catch (final Exception e) { + //no-op + } + } + } + + public JsonArray searchGameById(final long gameId) throws + IOException { + try (InputStream content = igdb.path("games/{gameId}") + .queryParam("fields", "*").resolveTemplate + ("gameId", gameId) + + .request(MediaType.APPLICATION_JSON_TYPE).header + ("X-Mashape-Key", apiKey).get(InputStream + .class)) { + + return Json.createReader(content).readArray(); + } + } + + public JsonArray searchGames(final String q) throws IOException { + + WebTarget webTarget = igdb.path("games/").queryParam + ("fields", "name").queryParam("limit", 50) + .queryParam("offset", 0).queryParam("order", + "release_dates.date%3Adesc"); + + webTarget = safeQuery(webTarget, q); + + Invocation.Builder builder = webTarget.request(MediaType + .APPLICATION_JSON_TYPE).header("X-Mashape-Key", + apiKey).accept("Accept", MediaType.APPLICATION_JSON); + + try (InputStream content = builder.get(InputStream.class)) { + return Json.createReader(content).readArray(); + } + + } + + private WebTarget safeQuery(WebTarget webTarget, String q) { + return null != q && q.length() > 0 ? webTarget.queryParam + ("search", q) : webTarget; + } + +} diff --git a/game/src/main/java/book/games/control/GamesService.java b/game/src/main/java/book/games/control/GamesService.java new file mode 100644 index 0000000..3acc0e9 --- /dev/null +++ b/game/src/main/java/book/games/control/GamesService.java @@ -0,0 +1,81 @@ +package book.games.control; + +import book.games.boundary.Games; +import book.games.boundary.IgdbGateway; +import book.games.entity.Game; +import book.games.entity.SearchResult; + +import javax.ejb.EJB; +import javax.enterprise.context.Dependent; +import javax.json.JsonArray; +import javax.json.JsonObject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +// tag::service[] +@Dependent // <1> +public class GamesService { + + @EJB // <2> + Games games; + + @EJB + IgdbGateway igdbGateway; + // end::service[] + + public List searchGames(final String query) + throws IOException { + + final JsonArray games = igdbGateway.searchGames(query); + + final List mappedGames = new ArrayList<>(); + + if (games.size() > 0) { + mappedGames.addAll(extractGameInformation(games)); + } + + return mappedGames; + + } + + // tag::service[] + public Game searchGameById(final long gameId) throws IOException { + + final Optional foundGame = games.findGameById(gameId) + ; // <3> + if (isGameInSiteDatabase(foundGame)) { + return foundGame.get(); + } else { + final JsonArray jsonByGameId = igdbGateway + .searchGameById(gameId); // <4> + final Game game = Game.fromJson(jsonByGameId); + games.create(game); + return game; + } + + } + // end::service[] + + private boolean isGameInSiteDatabase(final Optional + foundGame) { + return foundGame.isPresent(); + } + + private List extractGameInformation(final + JsonArray + games) { + + return games.stream().map(value -> { + JsonObject game = (JsonObject) value; + return new SearchResult(game.getInt("id"), game + .getString("name")); + + }).collect(Collectors.toList()); + } + + // tag::service[] +} +// end::service[] \ No newline at end of file diff --git a/game/src/main/java/book/games/entity/Game.java b/game/src/main/java/book/games/entity/Game.java new file mode 100644 index 0000000..9a55df4 --- /dev/null +++ b/game/src/main/java/book/games/entity/Game.java @@ -0,0 +1,226 @@ +package book.games.entity; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.persistence.*; +import java.io.Serializable; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +// tag::orm[] +@Entity +public class Game implements Serializable { + + @Id + @Column(name = "id", updatable = false, nullable = false) + private Long id; + @Version + @Column(name = "version") + private int version; + + @Column // <1> + private String title; + + @Column + private String cover; + + @ElementCollection + @CollectionTable(name = "ReleaseDate", joinColumns = + @JoinColumn(name = "OwnerId")) + private List releaseDates = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "Publisher", joinColumns = @JoinColumn + (name = "OwnerId")) + private List publishers = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "Developer", joinColumns = @JoinColumn + (name = "OwnerId")) + private List developers = new ArrayList<>(); + // end::orm[] + + public Long getId() { + return this.id; + } + + public void setId(final Long id) { + this.id = id; + } + + public int getVersion() { + return this.version; + } + + public void setVersion(final int version) { + this.version = version; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Game)) { + return false; + } + final Game other = (Game) obj; + if (id != null) { + if (!id.equals(other.id)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + public String getTitle() { + return title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public String getCover() { + return null != cover ? cover : "upload.wikimedia" + "" + + ".org/wikipedia/commons/b/b7" + + "/No_free_shield_available.png"; + } + + public void setCover(final String cover) { + this.cover = cover; + } + + public List getReleaseDates() { + return releaseDates; + } + + public void setReleaseDates(final List + releaseDates) { + this.releaseDates = releaseDates; + } + + public void addReleaseDate(final ReleaseDate releaseDate) { + this.releaseDates.add(releaseDate); + } + + public void setDevelopers(final List developers) { + this.developers = developers; + } + + public List getDevelopers() { + return developers; + } + + public void addDeveloper(final String developer) { + this.developers.add(developer); + } + + public void setPublishers(final List publishers) { + this.publishers = publishers; + } + + public List getPublishers() { + return publishers; + } + + public void addPublisher(final String publisher) { + this.publishers.add(publisher); + } + + // tag::domain[] + + public JsonObject convertToJson() { // <2> + + final JsonArrayBuilder developers = Json.createArrayBuilder(); + this.getDevelopers().forEach(developers::add); + + final JsonArrayBuilder publishers = Json.createArrayBuilder(); + this.getPublishers().forEach(publishers::add); + + final JsonArrayBuilder releaseDates = Json + .createArrayBuilder(); + this.getReleaseDates().forEach(releaseDate -> { + final String platform = releaseDate.getPlatformName(); + final String date = releaseDate.getReleaseDate().format + (DateTimeFormatter.ISO_DATE); + releaseDates.add(Json.createObjectBuilder().add + ("platform", platform).add("release_date", date)); + }); + + return Json.createObjectBuilder().add("id", this.getId()) + .add("title", this.getTitle()).add("cover", this + .getCover()).add("developers", developers) + .add("publishers", publishers).add("release_dates", + releaseDates).build(); + } + // end::domain[] + + public static Game fromJson(final JsonArray jsonObject) { + final Game game = new Game(); + + final JsonObject gameJsonObject = jsonObject.getJsonObject(0); + + game.setId((long) gameJsonObject.getInt("id")); + game.setTitle(gameJsonObject.getString("name")); + + if (gameJsonObject.containsKey("cover")) { + final JsonObject cover = gameJsonObject.getJsonObject + ("cover"); + game.setCover(cover.getString("url", "")); + } + + if (gameJsonObject.containsKey("release_dates")) { + final JsonArray releaseDates = gameJsonObject + .getJsonArray("release_dates"); + releaseDates.stream().map(jsonValue -> { + JsonObject releaseDateJson = (JsonObject) jsonValue; + return new ReleaseDate(releaseDateJson.getString + ("platform_name", ""), LocalDate.parse + (releaseDateJson.getString("release_date", + "1950-08-25"), DateTimeFormatter + .ISO_DATE)); + }).forEach(game::addReleaseDate); + } + + if (gameJsonObject.containsKey("companies")) { + final JsonArray companiesJson = gameJsonObject + .getJsonArray("companies"); + + //Add developers + companiesJson.stream().filter(jsonValue -> { + JsonObject companyJson = (JsonObject) jsonValue; + return companyJson.getBoolean("developer"); + }).map(jsonValue -> { + JsonObject companyJson = (JsonObject) jsonValue; + return companyJson.getString("name", ""); + }).forEach(game::addDeveloper); + + //Add publishers + companiesJson.stream().filter(jsonValue -> { + JsonObject companyJson = (JsonObject) jsonValue; + return companyJson.getBoolean("publisher"); + }).map(jsonValue -> { + JsonObject companyJson = (JsonObject) jsonValue; + return companyJson.getString("name", ""); + }).forEach(game::addPublisher); + } + + return game; + } + + // tag::orm[] +} +// end::orm[] \ No newline at end of file diff --git a/game/src/main/java/book/games/entity/LocalDatePersistenceConverter.java b/game/src/main/java/book/games/entity/LocalDatePersistenceConverter.java new file mode 100644 index 0000000..465623d --- /dev/null +++ b/game/src/main/java/book/games/entity/LocalDatePersistenceConverter.java @@ -0,0 +1,23 @@ +package book.games.entity; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import java.sql.Date; +import java.time.LocalDate; + +@Converter(autoApply = true) +public class LocalDatePersistenceConverter implements + AttributeConverter { + + @Override + public Date convertToDatabaseColumn(LocalDate entityValue) { + return (entityValue == null) ? null : Date.valueOf + (entityValue); + } + + @Override + public LocalDate convertToEntityAttribute(Date databaseValue) { + return databaseValue.toLocalDate(); + } + +} diff --git a/game/src/main/java/book/games/entity/ReleaseDate.java b/game/src/main/java/book/games/entity/ReleaseDate.java new file mode 100644 index 0000000..ffdac7e --- /dev/null +++ b/game/src/main/java/book/games/entity/ReleaseDate.java @@ -0,0 +1,36 @@ +package book.games.entity; + +import javax.persistence.Embeddable; +import java.time.LocalDate; + +@Embeddable +public final class ReleaseDate { + + private String platformName; + private LocalDate releaseDate; + + public ReleaseDate() { + super(); + } + + public ReleaseDate(String platformName, LocalDate releaseDate) { + this.platformName = platformName; + this.releaseDate = releaseDate; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public String getPlatformName() { + return platformName; + } +} diff --git a/game/src/main/java/book/games/entity/SearchResult.java b/game/src/main/java/book/games/entity/SearchResult.java new file mode 100644 index 0000000..6f89638 --- /dev/null +++ b/game/src/main/java/book/games/entity/SearchResult.java @@ -0,0 +1,59 @@ +package book.games.entity; + +import javax.json.Json; +import javax.json.JsonObject; +import java.io.Serializable; +import java.util.Objects; + +public class SearchResult implements Serializable { + + private long id; + + private String name; + + public SearchResult() { + } + + public SearchResult(final long id, final String name) { + this.id = id; + this.name = name; + } + + public void setId(final long id) { + this.id = id; + } + + public void setName(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public long getId() { + return id; + } + + public JsonObject convertToJson() { + return Json.createObjectBuilder().add("id", this.id).add + ("name", this.name).build(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SearchResult result = (SearchResult) o; + return id == result.id && Objects.equals(name, result.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} diff --git a/game/src/main/resources/META-INF/gamesCreate.ddl b/game/src/main/resources/META-INF/gamesCreate.ddl new file mode 100644 index 0000000..e69de29 diff --git a/game/src/main/resources/META-INF/gamesDrop.ddl b/game/src/main/resources/META-INF/gamesDrop.ddl new file mode 100644 index 0000000..e69de29 diff --git a/game/src/main/resources/META-INF/persistence.xml b/game/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..584ef8b --- /dev/null +++ b/game/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,14 @@ + + + + Games Persistence Unit + + false + + + + + + + + diff --git a/game/src/main/webapp/WEB-INF/beans.xml b/game/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 0000000..ae0f4bf --- /dev/null +++ b/game/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/game/src/main/webapp/WEB-INF/web.xml b/game/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..5f15c0a --- /dev/null +++ b/game/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,55 @@ + + + + CORS + com.thetransactioncompany.cors.CORSFilter + true + + + cors.allowGenericHttpRequests + true + + + + cors.allowOrigin + * + + + + cors.allowSubdomains + false + + + + cors.supportedMethods + GET, HEAD, POST, DELETE, OPTIONS + + + + cors.supportedHeaders + * + + + + cors.supportsCredentials + true + + + + cors.maxAge + 3600 + + + + + + + CORS + * + + + \ No newline at end of file diff --git a/game/src/test/java/book/games/arquillian/ArquillianAbstractTest.java b/game/src/test/java/book/games/arquillian/ArquillianAbstractTest.java new file mode 100644 index 0000000..9496ce9 --- /dev/null +++ b/game/src/test/java/book/games/arquillian/ArquillianAbstractTest.java @@ -0,0 +1,53 @@ +package book.games.arquillian; + +import book.games.boundary.ExecutorServiceProducer; +import book.games.boundary.Games; +import book.games.boundary.GamesResource; +import book.games.boundary.IgdbGateway; +import book.games.control.GamesService; +import book.games.entity.Game; +import book.games.entity.ReleaseDate; +import book.games.entity.SearchResult; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Rule; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +// tag::test[] +public abstract class ArquillianAbstractTest { + + @Before + public void before() { + // <1> + stubFor(get(anyUrl()).withQueryParam("search", equalTo + ("The" + " Legend of Zelda: Breath of the Wild")) + .willReturn(aResponse().withStatus(200).withHeader + ("Content-Type", "application/json") + .withBody("[{\"id\":7346,\"name\":\"The " + + "Legend of Zelda: Breath of the " + + "Wild\"}]"))); + } + + // <2> + @Rule + public WireMockRule wireMockRule = new WireMockRule(8071); + + // <3> + public static WebArchive createBaseDeployment(final String name) { + return ShrinkWrap.create(WebArchive.class, name + ".war") + .addClasses(GamesResource.class, + ExecutorServiceProducer.class, GamesService + .class, IgdbGateway.class, + SearchResult.class, Games.class, Game + .class, ReleaseDate.class) + .addAsResource("test-persistence.xml", + "META-INF/persistence.xml") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans" + + ".xml"); + } +} +// end::test[] diff --git a/game/src/test/java/book/games/arquillian/ArquillianBasicTest.java b/game/src/test/java/book/games/arquillian/ArquillianBasicTest.java new file mode 100644 index 0000000..b856b21 --- /dev/null +++ b/game/src/test/java/book/games/arquillian/ArquillianBasicTest.java @@ -0,0 +1,67 @@ +// tag::test[] +package book.games.arquillian; + +import book.games.boundary.Games; +import book.games.boundary.IgdbGateway; +import book.games.control.GamesService; +import book.games.entity.Game; +import book.games.entity.ReleaseDate; +import book.games.entity.SearchResult; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.ejb.EJB; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +// <1> +@RunWith(Arquillian.class) +public class ArquillianBasicTest { + + // <2> + @Deployment + public static WebArchive createDeployment() { + + // <3> + //return ShrinkWrap.create(JavaArchive.class + //return ShrinkWrap.create(EnterpriseArchive.class + return ShrinkWrap.create(WebArchive.class, + ArquillianBasicTest.class.getName() + ".war") + .addClasses(IgdbGateway.class, GamesService.class, + SearchResult.class, Games.class, Game + .class, ReleaseDate.class) + .addAsResource("test-persistence.xml", + "META-INF/persistence.xml") // <4> + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans" + + ".xml"); // <5> + } + + // <6> + @Inject + private GamesService service; + + // <7> + @EJB + private Games games; + + // <8> + @PersistenceContext + private EntityManager em; + + // <9> + @Test + public void test() { + // <10> + Assert.assertNotNull(this.service); + Assert.assertNotNull(this.games); + Assert.assertNotNull(this.em); + } +} +//end::test[] diff --git a/game/src/test/java/book/games/arquillian/ArquillianRemoteTest.java b/game/src/test/java/book/games/arquillian/ArquillianRemoteTest.java new file mode 100644 index 0000000..94bede4 --- /dev/null +++ b/game/src/test/java/book/games/arquillian/ArquillianRemoteTest.java @@ -0,0 +1,53 @@ +package book.games.arquillian; + +import book.games.boundary.IgdbGateway; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; +import javax.json.JsonArray; +import javax.json.JsonObject; + +// tag::test[] +@RunWith(Arquillian.class) +public class ArquillianRemoteTest extends ArquillianAbstractTest { + // <1> + + // <2> + @Deployment + public static WebArchive createDeployment() { + return createBaseDeployment(ArquillianRemoteTest.class + .getName()).addClass(ArquillianRemoteTest.class) + .addClass(ArquillianAbstractTest.class) + .addAsLibrary(Maven.resolver().resolve("com.github" + + ".tomakehurst:wiremock-standalone:2.2.1") + .withoutTransitivity().asSingleFile()); + } + + // <3> + @Inject + private IgdbGateway gateway; + + // <4> + @Test + public void testSearchGames() throws Exception { + Assert.assertNotNull(this.gateway); + + final JsonArray json = gateway.searchGames("The Legend of " + + "Zelda: Breath of the Wild"); + Assert.assertNotNull(json); + + final JsonObject game = json.getJsonObject(0); + + Assert.assertEquals("Unexpected id", 7346, game + .getJsonNumber("id").intValue()); + Assert.assertEquals("Unexpected name", "The Legend of " + + "Zelda: Breath of the Wild", game.getString("name")); + } +} +//end::test[] \ No newline at end of file diff --git a/game/src/test/java/book/games/arquillian/ArquillianResourceTest.java b/game/src/test/java/book/games/arquillian/ArquillianResourceTest.java new file mode 100644 index 0000000..0a05ff6 --- /dev/null +++ b/game/src/test/java/book/games/arquillian/ArquillianResourceTest.java @@ -0,0 +1,59 @@ +package book.games.arquillian; + +import book.games.entity.SearchResult; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import java.net.URL; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +// tag::test[] +@RunWith(Arquillian.class) +public class ArquillianResourceTest extends ArquillianAbstractTest +{ // <1> + + // <2> + @Deployment(testable = false) + public static WebArchive createDeployment() { + return createBaseDeployment(ArquillianResourceTest.class + .getName()); + } + + @Test + @RunAsClient // <3> + public void testSearch(@ArquillianResource final URL url) + throws Exception { // <4> + // <5> + final Client client = ClientBuilder.newBuilder().build(); + final WebTarget target = client.target(url.toExternalForm() + + "?query=The Legend of Zelda: Breath of the Wild"); + + // <6> + final Future futureResponse = target.request() + .async().get(); + final Response response = futureResponse.get(5, TimeUnit + .SECONDS); + + // <7> + final List results = response.readEntity(new GenericType>() { + }); + + Assert.assertEquals("Unexpected title", "The Legend of " + + "Zelda: Breath of the Wild", results.get(0).getName + ()); + } +} +// end::test[] \ No newline at end of file diff --git a/game/src/test/java/book/games/boundary/GamesResourceTest.java b/game/src/test/java/book/games/boundary/GamesResourceTest.java new file mode 100644 index 0000000..318bc2a --- /dev/null +++ b/game/src/test/java/book/games/boundary/GamesResourceTest.java @@ -0,0 +1,128 @@ +package book.games.boundary; + +import book.games.control.GamesService; +import book.games.entity.SearchResult; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// tag::test[] +@RunWith(MockitoJUnitRunner.class) +public class GamesResourceTest { + + @Mock + GamesService gamesService; + + @Mock + ExecutorServiceProducer executorServiceProducer; + + @Mock + AsyncResponse asyncResponse; // <1> + + @Captor // <2> + ArgumentCaptor argumentCaptorResponse; + + private static final ExecutorService executorService = + Executors.newSingleThreadExecutor(); // <3> + + @Before + public void setupExecutorServiceProducer() { + when(executorServiceProducer.getManagedExecutorService()) + .thenReturn(executorService); + } + + @AfterClass // <4> + public static void stopExecutorService() { + executorService.shutdown(); + } + + @Test + public void restAPIShouldSearchGamesByTheirNames() throws + IOException, InterruptedException { + + final GamesResource gamesResource = new GamesResource(); + gamesResource.managedExecutorService = + executorServiceProducer; + gamesResource.gamesService = gamesService; + + when(gamesService.searchGames("zelda")).thenReturn + (getSearchResults()); + + gamesResource.searchGames(asyncResponse, "zelda"); + executorService.awaitTermination(2, TimeUnit.SECONDS); // <5> + + verify(asyncResponse).resume(argumentCaptorResponse.capture + ()); // <6> + + final Response response = argumentCaptorResponse.getValue() + ; // <7> + + assertThat(response.getStatusInfo().getFamily()).isEqualTo + (Response.Status.Family.SUCCESSFUL); + + assertThat((JsonArray) response.getEntity()).hasSize(2) + .containsExactlyInAnyOrder(Json.createObjectBuilder + ().add("id", 1).add("name", "The Legend Of " + + "" + "Zelda").build(), Json + .createObjectBuilder().add("id", 2).add + ("name", "Zelda II: The " + + "Adventure of Link").build() + + ); + } + + // end::test[] + + // tag::subtest[] + @Test + public void exceptionShouldBePropagatedToCaller() throws + IOException, InterruptedException { + final GamesResource gamesResource = new GamesResource(); + gamesResource.managedExecutorService = + executorServiceProducer; + gamesResource.gamesService = gamesService; + + when(gamesService.searchGames("zelda")).thenThrow + (IOException.class); // <1> + + gamesResource.searchGames(asyncResponse, "zelda"); + executorService.awaitTermination(1, TimeUnit.SECONDS); + + verify(gamesService).searchGames("zelda"); + verify(asyncResponse).resume(any(IOException.class)); // <2> + } + // end::subtest[] + + private List getSearchResults() { + final List searchResults = new ArrayList<>(); + + searchResults.add(new SearchResult(1, "The Legend Of Zelda")); + searchResults.add(new SearchResult(2, "Zelda II: The " + + "Adventure of Link")); + + return searchResults; + } + // tag::test[] +} +//end::test[] \ No newline at end of file diff --git a/game/src/test/java/book/games/boundary/GamesTest.java b/game/src/test/java/book/games/boundary/GamesTest.java new file mode 100644 index 0000000..af1ac42 --- /dev/null +++ b/game/src/test/java/book/games/boundary/GamesTest.java @@ -0,0 +1,81 @@ +package book.games.boundary; + +import book.games.entity.Game; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.persistence.EntityManager; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// tag::test[] +@RunWith(MockitoJUnitRunner.class) // <1> +public class GamesTest { + + private static final long GAME_ID = 123L; + + @Mock // <2> + EntityManager entityManager; + + @Test + public void shouldCreateAGame() { + final Game game = new Game(); + game.setId(GAME_ID); + game.setTitle("Zelda"); + + final Games games = new Games(); + + when(entityManager.merge(game)).thenReturn(game); // <3> + games.em = entityManager; // <4> + + games.create(game); // <5> + + verify(entityManager).merge(game); // <6> + + } + // end::test[] + + // tag::subtest[] + @Test + public void shouldFindAGameById() { + final Game game = new Game(); + game.setId(GAME_ID); + game.setTitle("Zelda"); + + final Games games = new Games(); + when(entityManager.find(Game.class, GAME_ID)).thenReturn(game); + games.em = entityManager; + + final Optional foundGame = games.findGameById(GAME_ID); + // <1> + + verify(entityManager).find(Game.class, GAME_ID); + assertThat(foundGame).isNotNull().hasValue(game) + .usingFieldByFieldValueComparator(); // <2> + } + + @Test + public void shouldReturnAnEmptyOptionalIfElementNotFound() { + final Game game = new Game(); + game.setId(GAME_ID); + game.setTitle("Zelda"); + + final Games games = new Games(); + when(entityManager.find(Game.class, GAME_ID)).thenReturn(null); + games.em = entityManager; + + final Optional foundGame = games.findGameById(GAME_ID); + + verify(entityManager).find(Game.class, GAME_ID); + assertThat(foundGame).isNotPresent(); // <3> + } + // end::subtest[] + + // tag::test[] +} +// end::test[] \ No newline at end of file diff --git a/game/src/test/java/book/games/boundary/IgdbGatewayTest.java b/game/src/test/java/book/games/boundary/IgdbGatewayTest.java new file mode 100644 index 0000000..2fa094d --- /dev/null +++ b/game/src/test/java/book/games/boundary/IgdbGatewayTest.java @@ -0,0 +1,49 @@ +package book.games.boundary; + +import book.games.entity.Game; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.json.JsonArray; +import javax.json.JsonValue; + +import static org.junit.Assert.assertTrue; + +public class IgdbGatewayTest { + + private static IgdbGateway gateway = new IgdbGateway(); + + @BeforeClass + public static void beforeClass() { + gateway.postConstruct(); + } + + @AfterClass + public static void afterClass() { + gateway.postConstruct(); + } + + @Test + public void searchGameById() throws Exception { + JsonArray json = gateway.searchGameById(7346); + Game game = Game.fromJson(json); + assertTrue(game.getTitle().contains("The Legend of Zelda: " + + "Breath of the Wild")); + } + + @Test + public void searchGames() throws Exception { + JsonArray games = gateway.searchGames("Zelda"); + boolean found = false; + for (final JsonValue game : games) { + if (game.toString().contains("7346")) { + found = true; + break; + } + } + + assertTrue(found); + } + +} \ No newline at end of file diff --git a/game/src/test/java/book/games/control/GamesServiceTest.java b/game/src/test/java/book/games/control/GamesServiceTest.java new file mode 100644 index 0000000..fc37a55 --- /dev/null +++ b/game/src/test/java/book/games/control/GamesServiceTest.java @@ -0,0 +1,121 @@ +package book.games.control; + +import book.games.boundary.Games; +import book.games.boundary.IgdbGateway; +import book.games.entity.Game; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.json.Json; +import javax.json.JsonArray; +import java.io.IOException; +import java.io.StringReader; +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.*; + +// tag::test[] +@RunWith(MockitoJUnitRunner.class) +public class GamesServiceTest { + + @Mock + Games games; + + @Mock + IgdbGateway igdbGateway; + + @Test + public void shouldReturnGameIfItIsCachedInInternalDatabase() + throws IOException { + + final Game game = new Game(); + game.setId(123L); + game.setTitle("Zelda"); + game.setCover("ZeldaCover"); + + when(games.findGameById(123L)).thenReturn(Optional.of(game)); + + final GamesService gamesService = new GamesService(); + gamesService.games = games; + gamesService.igdbGateway = igdbGateway; + + final Game foundGame = gamesService.searchGameById(123L); + assertThat(foundGame).isEqualToComparingFieldByField(game); + // <1> + verify(igdbGateway, times(0)).searchGameById(anyInt()); // <2> + verify(games).findGameById(123L); + + } + // end::test[] + + // tag::subtest[] + @Test + public void + shouldReturnGameFromIgdbSiteIfGameIsNotInInternalDatabase() + throws IOException { + + final JsonArray returnedGame = createTestJsonArray(); + + when(games.findGameById(123L)).thenReturn(Optional.empty()); + when(igdbGateway.searchGameById(123L)).thenReturn + (returnedGame); + + final GamesService gamesService = new GamesService(); + gamesService.games = games; + gamesService.igdbGateway = igdbGateway; + + final Game foundGame = gamesService.searchGameById(123L); + assertThat(foundGame.getTitle()).isEqualTo("Battlefield 4"); + + + Assertions.assertThat(foundGame.getReleaseDates()) // <1> + .hasSize(1).extracting("platformName", + "releaseDate").contains(tuple("PlayStation 3", + LocalDate.of(2013, 10, 29))); + + assertThat(foundGame.getDevelopers()).hasSize(1).contains + ("EA Digital Illusions CE"); + + assertThat(foundGame.getPublishers()).hasSize(1).contains + ("Electronic Arts"); + + verify(games).create(anyObject()); // <2> + verify(igdbGateway).searchGameById(123L); // <3> + + } + + // end::subtest[] + + private JsonArray createTestJsonArray() { + final String content = "[\n" + " {\n" + " " + + "\"id\":123,\n" + " \"name\":\"Battlefield " + + "4\",\n" + " \"release_dates\":[\n" + " " + + "" + " {\n" + " " + + "\"platform_name\":\"PlayStation 3\",\n" + " " + + "" + " \"release_date\":\"2013-10-29\"\n" + " " + + " " + " }\n" + " ],\n" + " " + + "\"companies\":[\n" + " {\n" + " " + + " \"developer\":true," + "\n" + " " + + "\"publisher\":false,\n" + " " + " " + + "\"name\":\"EA Digital Illusions CE\"\n" + " " + + " },\n" + " {\n" + " " + + "\"developer\":false,\n" + " " + + "\"publisher\":true,\n" + " " + + "\"name\":\"Electronic Arts\"\n" + " }\n" + + " ]\n" + " }\n" + "]"; + + return Json.createReader(new StringReader(content)) + .readArray(); + } + + // tag::test[] +} +// end::test[] \ No newline at end of file diff --git a/game/src/test/java/integrationtests/GamesTest.java b/game/src/test/java/integrationtests/GamesTest.java new file mode 100644 index 0000000..085eee0 --- /dev/null +++ b/game/src/test/java/integrationtests/GamesTest.java @@ -0,0 +1,54 @@ +package integrationtests; + +import book.games.Main; +import book.games.boundary.Games; +import book.games.entity.Game; +import book.games.entity.ReleaseDate; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.ClassLoaderAsset; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.wildfly.swarm.Swarm; +import org.wildfly.swarm.arquillian.CreateSwarm; +import org.wildfly.swarm.undertow.WARArchive; + +import javax.ejb.EJB; + +@RunWith(Arquillian.class) +public class GamesTest { + + @Deployment + public static Archive createDeployment() throws Exception { + final WARArchive warArchive = ShrinkWrap.create(WARArchive + .class); + warArchive.addClasses(Games.class, Game.class, ReleaseDate + .class, Main.class); + + warArchive.addAsWebInfResource(new ClassLoaderAsset + ("test-persistence.xml", GamesTest.class + .getClassLoader()), + "classes/META-INF/persistence.xml"); + + warArchive.addAllDependencies(); + + return warArchive; + } + + @CreateSwarm + public static Swarm newContainer() throws Exception { + return Main.createSwarm(); + } + + + @EJB + Games games; + + @Test + public void test() { + System.out.println(games); + } + +} diff --git a/game/src/test/resources/arquillian.xml b/game/src/test/resources/arquillian.xml new file mode 100644 index 0000000..be407f7 --- /dev/null +++ b/game/src/test/resources/arquillian.xml @@ -0,0 +1,25 @@ + + + + + + + + + -DIGDB_API_KEY=dummy -DIGDB_HOST=http://127.0.0.1:8071 + + + + + + + + + -DIGDB_API_KEY=dummyKey -DIGDB_HOST=http://127.0.0.1:8071 + + + \ No newline at end of file diff --git a/game/src/test/resources/gamesCreate.ddl b/game/src/test/resources/gamesCreate.ddl new file mode 100644 index 0000000..e69de29 diff --git a/game/src/test/resources/gamesDrop.ddl b/game/src/test/resources/gamesDrop.ddl new file mode 100644 index 0000000..e69de29 diff --git a/game/src/test/resources/test-persistence.xml b/game/src/test/resources/test-persistence.xml new file mode 100644 index 0000000..584ef8b --- /dev/null +++ b/game/src/test/resources/test-persistence.xml @@ -0,0 +1,14 @@ + + + + Games Persistence Unit + + false + + + + + + + + diff --git a/video/Dockerfile b/video/Dockerfile new file mode 100644 index 0000000..ad485cf --- /dev/null +++ b/video/Dockerfile @@ -0,0 +1,7 @@ +FROM java:8-jdk + +ADD build/libs/video-service-*.jar /video-service.jar + +EXPOSE 8080 +RUN bash -c 'touch /video-service.jar' +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom","-jar","/video-service.jar"] diff --git a/video/README.adoc b/video/README.adoc new file mode 100644 index 0000000..5ccf0eb --- /dev/null +++ b/video/README.adoc @@ -0,0 +1,20 @@ += Welcome to the Video Service + +This service is a SpringBoot MVC based service that is designed to serve suggested links to named games. + +=== Prerequisite + +If you have obtained an API key then ensure it is added to the shell using your profile file. + +[source,shell,indent=0] +.home/profile +---- +... +export YOUTUBE_API_KEY="Your-Key" +---- + +To build the service run: + +---- +$ gradle build +---- diff --git a/video/build.gradle b/video/build.gradle new file mode 100644 index 0000000..765c1e5 --- /dev/null +++ b/video/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE") + } +} + +apply plugin: 'java' +apply plugin: 'spring-boot' + +group = 'org.gamer' +version = '1.0-SNAPSHOT' + +jar { + baseName = 'video-service' + version = '0.1.0' +} + + +repositories { + mavenCentral() +} + +configurations { + compile.exclude module: "spring-boot-starter-tomcat" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.5.2' + compile 'com.google.apis:google-api-services-youtube:v3-rev183-1.22.0' + compile 'org.springframework.boot:spring-boot-starter-web:1.4.5.RELEASE' + compile 'org.springframework.boot:spring-boot-starter-undertow:1.4.5.RELEASE' + compile 'org.springframework.boot:spring-boot-starter-actuator:1.4.5.RELEASE' + compile 'org.springframework.boot:spring-boot-starter-data-redis:1.4.5.RELEASE' + compile 'com.orange.redis-embedded:embedded-redis:0.5' +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} diff --git a/video/c-tests/build.gradle b/video/c-tests/build.gradle new file mode 100644 index 0000000..38a1c48 --- /dev/null +++ b/video/c-tests/build.gradle @@ -0,0 +1,33 @@ +buildscript { + repositories { + mavenCentral() + } +} + + +apply plugin: 'java' +apply plugin: 'io.spring.dependency-management' + +repositories { + mavenCentral() +} + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.12.Final' + } +} + +dependencies { + + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.8.0' + testCompile('org.jboss.arquillian.junit:arquillian-junit-standalone') + testCompile 'org.arquillian.cube:arquillian-cube-docker:1.0.0.Alpha17' + testCompile 'org.arquillian.cube:assertj-docker-java:1.0.0.Alpha17' +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + diff --git a/video/c-tests/src/test/java/book/video/VideoServiceContainerTest.java b/video/c-tests/src/test/java/book/video/VideoServiceContainerTest.java new file mode 100644 index 0000000..4ded035 --- /dev/null +++ b/video/c-tests/src/test/java/book/video/VideoServiceContainerTest.java @@ -0,0 +1,26 @@ +package book.video; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.assertions.DockerJavaAssertions; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Test; +import org.junit.runner.RunWith; + +// tag::test[] +@RunWith(Arquillian.class) +public class VideoServiceContainerTest { + + + @ArquillianResource + DockerClient docker; + + @Test + public void should_create_valid_dockerfile() { + DockerJavaAssertions.assertThat(docker).container + ("videoservice").hasExposedPorts("8080/tcp") // <1> + .isRunning(); + } + +} +// end::test[] \ No newline at end of file diff --git a/video/c-tests/src/test/resources/arquillian.xml b/video/c-tests/src/test/resources/arquillian.xml new file mode 100644 index 0000000..647cd2b --- /dev/null +++ b/video/c-tests/src/test/resources/arquillian.xml @@ -0,0 +1,26 @@ + + + + + + dev + + videoservice: + build: ../. + environment: + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} + ports: + - "8080:8080" + links: + - redis:redis + redis: + image: redis:3.2.6 + + + + \ No newline at end of file diff --git a/video/gradle/wrapper/gradle-wrapper.properties b/video/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..754dd92 --- /dev/null +++ b/video/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jul 22 08:38:20 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip diff --git a/video/gradlew b/video/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/video/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/video/gradlew.bat b/video/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/video/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/video/i-tests/build.gradle b/video/i-tests/build.gradle new file mode 100644 index 0000000..9333034 --- /dev/null +++ b/video/i-tests/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'java' +apply plugin: 'io.spring.dependency-management' + +buildscript { + repositories { + mavenCentral() + } +} + +repositories { + mavenCentral() +} + +dependencyManagement { + imports { + mavenBom 'org.jboss.arquillian:arquillian-bom:1.1.12.Final' + } +} + +dependencies { + + testCompile rootProject + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:3.8.0' + testCompile 'org.springframework.boot:spring-boot-starter-test:1.4.5.RELEASE' + testCompile('com.lordofthejars:nosqlunit-redis:0.10.0') + testCompile('org.jboss.arquillian.junit:arquillian-junit-container') + testCompile('org.jboss.arquillian.extension:arquillian-service-container-spring:1.1.0.Alpha1') + testCompile('org.jboss.arquillian.extension:arquillian-service-integration-spring-inject:1.1.0.Alpha1') + testCompile('org.jboss.arquillian.extension:arquillian-service-integration-spring:1.1.0.Alpha1') + testCompile('org.jboss.arquillian.extension:arquillian-service-integration-spring-inject:1.1.0.Alpha1') + testCompile('org.jboss.arquillian.extension:arquillian-service-integration-spring-javaconfig:1.1.0.Alpha1') + testCompile('com.github.kstyrc:embedded-redis:0.6') +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + diff --git a/video/i-tests/src/test/java/book/video/YoutubeVideosArquillianTest.java b/video/i-tests/src/test/java/book/video/YoutubeVideosArquillianTest.java new file mode 100644 index 0000000..b35c442 --- /dev/null +++ b/video/i-tests/src/test/java/book/video/YoutubeVideosArquillianTest.java @@ -0,0 +1,62 @@ +package book.video; + +import book.video.boundary.YouTubeVideos; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.spring.integration.test.annotation + .SpringAnnotationConfiguration; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.jedis + .JedisConnectionFactory; + +// tag::test[] +// <1> +@RunWith(Arquillian.class) +@SpringAnnotationConfiguration(classes = + {YoutubeVideosArquillianTest.class, ControllerConfiguration + .class, ThreadExecutorConfiguration.class}) +@Configuration +public class YoutubeVideosArquillianTest { + + // <2> + @Bean + public JedisConnectionFactory jedisConnectionFactory() { + final JedisConnectionFactory jcf = new + JedisConnectionFactory(); + jcf.setHostName("localhost"); + return jcf; + } + + @Primary + @Bean + public YouTubeVideos getYouTubeVideos() { + return new YouTubeVideos(); + } + + // <3> + @Deployment + public static JavaArchive createTestArchive() { + return ShrinkWrap.create(JavaArchive.class, "spring-test" + + ".jar").addClasses(YouTubeVideos.class); + } + + // <4> + @Autowired + private YouTubeVideos youtubeVideos; + + // <5> + @Test + public void test() { + Assert.assertNotNull(this.youtubeVideos); + } + +} +// end::test[] diff --git a/video/i-tests/src/test/java/book/video/YoutubeVideosTest.java b/video/i-tests/src/test/java/book/video/YoutubeVideosTest.java new file mode 100644 index 0000000..17d9ee3 --- /dev/null +++ b/video/i-tests/src/test/java/book/video/YoutubeVideosTest.java @@ -0,0 +1,56 @@ +package book.video; + +import book.video.boundary.YouTubeVideos; +import com.lordofthejars.nosqlunit.annotation.ShouldMatchDataSet; +import com.lordofthejars.nosqlunit.annotation.UsingDataSet; +import com.lordofthejars.nosqlunit.core.LoadStrategyEnum; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context + .junit4.SpringJUnit4ClassRunner; +import redis.embedded.RedisServer; + +import java.util.Collections; + +// tag::test[] +// <1> +@RunWith(SpringJUnit4ClassRunner.class) +// <2> +@SpringBootTest(classes = {Main.class}) +public class YoutubeVideosTest { + + private static RedisServer redisServer; + + @BeforeClass + public static void beforeClass() throws Exception { + redisServer = new RedisServer(); + redisServer.start(); + } + + @AfterClass + public static void afterClass() { + redisServer.stop(); + } + + // @Rule + // public RedisRule redisRule = new RedisRule + // (newManagedRedisConfiguration().build()); + + // <3> + @Autowired + YouTubeVideos youtubeVideos; + + @Test + // <4> + @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) + @ShouldMatchDataSet(location = "expected-videos.json") + public void shouldCacheGamesInRedis() { + youtubeVideos.createYouTubeLinks("123", Collections + .singletonList("https://www.youtube.com/embed/7889")); + } +} +// end::test[] \ No newline at end of file diff --git a/video/i-tests/src/test/resources/book/video/expected-videos.json b/video/i-tests/src/test/resources/book/video/expected-videos.json new file mode 100644 index 0000000..a3b6edd --- /dev/null +++ b/video/i-tests/src/test/resources/book/video/expected-videos.json @@ -0,0 +1,14 @@ +{ + "data": [ + {"list": [ + { + "key": "123", + "values": [ + { + "value": "https://www.youtube.com/embed/7889" + } + ] + }] + } + ] +} \ No newline at end of file diff --git a/video/settings.gradle b/video/settings.gradle new file mode 100644 index 0000000..5020100 --- /dev/null +++ b/video/settings.gradle @@ -0,0 +1,3 @@ +include 'i-tests' +include 'c-tests' +rootProject.name = 'video' diff --git a/video/shutdown.bat b/video/shutdown.bat new file mode 100644 index 0000000..3bf0fb5 --- /dev/null +++ b/video/shutdown.bat @@ -0,0 +1 @@ +curl -X POST localhost:8080/shutdown \ No newline at end of file diff --git a/video/shutdown.sh b/video/shutdown.sh new file mode 100644 index 0000000..59c1d9b --- /dev/null +++ b/video/shutdown.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +curl -X POST localhost:8080/shutdown \ No newline at end of file diff --git a/video/src/main/java/book/video/ControllerConfiguration.java b/video/src/main/java/book/video/ControllerConfiguration.java new file mode 100644 index 0000000..e967ee5 --- /dev/null +++ b/video/src/main/java/book/video/ControllerConfiguration.java @@ -0,0 +1,24 @@ +package book.video; + +import book.video.controller.YouTubeVideoLinkCreator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection + .RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +@Configuration +public class ControllerConfiguration { + + @Bean + public YouTubeVideoLinkCreator createYouTubeVideoLinkCreator() { + return new YouTubeVideoLinkCreator(); + } + + @Bean + StringRedisTemplate template(final RedisConnectionFactory + connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } + +} diff --git a/video/src/main/java/book/video/Futures.java b/video/src/main/java/book/video/Futures.java new file mode 100644 index 0000000..61b604d --- /dev/null +++ b/video/src/main/java/book/video/Futures.java @@ -0,0 +1,19 @@ +package book.video; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +public class Futures { + public static CompletableFuture toCompletable(Future + future, Executor executor) { + return CompletableFuture.supplyAsync(() -> { + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }, executor); + } +} diff --git a/video/src/main/java/book/video/Main.java b/video/src/main/java/book/video/Main.java new file mode 100644 index 0000000..2ee5250 --- /dev/null +++ b/video/src/main/java/book/video/Main.java @@ -0,0 +1,30 @@ +// tag::app[] +package book.video; + +import org.springframework.boot.actuate.system + .ApplicationPidFileWriter; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableAsync; + +import java.util.HashMap; +import java.util.Map; + +// <1> +@SpringBootApplication +@EnableAsync +public class Main { + + // <2> + public static void main(final String[] args) { + + final Map map = new HashMap<>(); + map.put("endpoints.shutdown.enabled", true); + map.put("endpoints.shutdown.sensitive", false); + + new SpringApplicationBuilder(Main.class).listeners(new + ApplicationPidFileWriter("./video.pid")) + .logStartupInfo(true).properties(map).run(args); + } +} +// tag::app[] diff --git a/video/src/main/java/book/video/ThreadExecutorConfiguration.java b/video/src/main/java/book/video/ThreadExecutorConfiguration.java new file mode 100644 index 0000000..8878082 --- /dev/null +++ b/video/src/main/java/book/video/ThreadExecutorConfiguration.java @@ -0,0 +1,19 @@ +package book.video; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent + .ThreadPoolTaskExecutor; + +@Configuration +public class ThreadExecutorConfiguration { + + @Bean + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new + ThreadPoolTaskExecutor(); + taskExecutor.afterPropertiesSet(); + return taskExecutor; + } +} diff --git a/video/src/main/java/book/video/boundary/VideosResource.java b/video/src/main/java/book/video/boundary/VideosResource.java new file mode 100644 index 0000000..219a158 --- /dev/null +++ b/video/src/main/java/book/video/boundary/VideosResource.java @@ -0,0 +1,31 @@ +package book.video.boundary; + +import book.video.controller.VideoServiceController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@CrossOrigin(origins = {"http://localhost:8080", + "http://localhost:8181", "http://localhost:8282", + "http://localhost:8383"}) + +@RestController // <1> +public class VideosResource { + + @Autowired // <2> + VideoServiceController videoServiceController; + + @RequestMapping(value = "/", produces = "application/json") // <3> + public ResponseEntity> getVideos( + @RequestParam ("videoId") final long videoId, + @RequestParam("gameName") final String gameName) { + final List linksFromGame = videoServiceController + .getLinksFromGame(Long.toString(videoId), gameName); + return ResponseEntity.ok(linksFromGame); + } +} diff --git a/video/src/main/java/book/video/boundary/YouTubeGateway.java b/video/src/main/java/book/video/boundary/YouTubeGateway.java new file mode 100644 index 0000000..5e446da --- /dev/null +++ b/video/src/main/java/book/video/boundary/YouTubeGateway.java @@ -0,0 +1,81 @@ +package book.video.boundary; + +import book.video.controller.YouTubeVideoLinkCreator; +import book.video.entity.YoutubeLink; +import book.video.entity.YoutubeLinks; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.SearchListResponse; +import com.google.api.services.youtube.model.SearchResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + + +@Service +public class YouTubeGateway { + + private static final long NUMBER_OF_VIDEOS_RETURNED = 3; + + private final YouTubeVideoLinkCreator youTubeVideoLinkCreator; + + private String apiKey = ""; + private YouTube youtube; + + @Autowired + public YouTubeGateway(final YouTubeVideoLinkCreator + youTubeVideoLinkCreator) { + this.youTubeVideoLinkCreator = youTubeVideoLinkCreator; + } + + @PostConstruct + public void initClient() { + this.apiKey = Optional.ofNullable(System.getenv + ("YOUTUBE_API_KEY")).orElse(Optional.ofNullable + (System.getProperty("YOUTUBE_API_KEY")).orElseGet( + () -> "AIzaSyDIQ0uq4ZpV-X4wBCmo4xea0aJRMoyG7kI")); + this.youtube = new YouTube.Builder(new NetHttpTransport(), + new JacksonFactory(), null).setApplicationName + ("Gamer Video Microservice").build(); + } + + public YoutubeLinks findYoutubeLinks(final String gameName) + throws IOException { + final YouTube.Search.List search = youtube.search().list + ("id,snippet"); + search.setKey(apiKey); + search.setQ(gameName); + + search.setType("video"); + search.setMaxResults(NUMBER_OF_VIDEOS_RETURNED); + search.setFields("items(id/kind,id/videoId,snippet/title)"); + search.setOrder("rating"); + + + final SearchListResponse searchResponse = search.execute(); + final List searchResultList = searchResponse + .getItems(); + + final Set collect = searchResultList.stream() + .filter(searchResult -> "youtube#video".equals + (searchResult.getId().getKind())).map + (converterToYoutubeLink).peek(youtubeLink + -> youtubeLink.setYouTubeVideoLinkCreator + (youTubeVideoLinkCreator::createEmbeddedUrl)).collect(Collectors.toSet()); + + return new YoutubeLinks(collect); + } + + private static final Function + converterToYoutubeLink = searchResult -> new + YoutubeLink(searchResult.getId().getVideoId()); + +} diff --git a/video/src/main/java/book/video/boundary/YouTubeVideos.java b/video/src/main/java/book/video/boundary/YouTubeVideos.java new file mode 100644 index 0000000..9c2b5f2 --- /dev/null +++ b/video/src/main/java/book/video/boundary/YouTubeVideos.java @@ -0,0 +1,40 @@ +package book.video.boundary; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class YouTubeVideos { + + @Autowired + StringRedisTemplate redisTemplate; + + public void createYouTubeLinks(final String gameId, final + List youtubeLinks) { + final ListOperations + stringStringListOperations = redisTemplate + .opsForList(); + stringStringListOperations.leftPushAll(gameId, youtubeLinks); + } + + public boolean isGameInserted(final String gameId) { + final ListOperations + stringStringListOperations = redisTemplate + .opsForList(); + return stringStringListOperations.size(gameId) > 0; + } + + public List getYouTubeLinks(final String gameId) { + final ListOperations + stringStringListOperations = redisTemplate + .opsForList(); + final Long size = stringStringListOperations.size(gameId); + return stringStringListOperations.range(gameId, 0, size); + + } + +} diff --git a/video/src/main/java/book/video/controller/VideoServiceController.java b/video/src/main/java/book/video/controller/VideoServiceController.java new file mode 100644 index 0000000..eb01ef6 --- /dev/null +++ b/video/src/main/java/book/video/controller/VideoServiceController.java @@ -0,0 +1,46 @@ +package book.video.controller; + +import book.video.boundary.YouTubeGateway; +import book.video.boundary.YouTubeVideos; +import book.video.entity.YoutubeLinks; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.List; + +@Component +public class VideoServiceController { + + private final YouTubeGateway youtubeGateway; + + private final YouTubeVideos youtubeVideos; + + @Autowired + public VideoServiceController(final YouTubeGateway + youtubeGateway, final + YouTubeVideos youtubeVideos) { + this.youtubeGateway = youtubeGateway; + this.youtubeVideos = youtubeVideos; + } + + public List getLinksFromGame(final String gameId, final + String gameName) { + if (youtubeVideos.isGameInserted(gameId)) { + return youtubeVideos.getYouTubeLinks(gameId); + } else { + try { + final YoutubeLinks youtubeLinks = youtubeGateway + .findYoutubeLinks(gameName); + final List youtubeLinksAsString = + youtubeLinks.getYoutubeLinksAsString(); + youtubeVideos.createYouTubeLinks(gameId, + youtubeLinksAsString); + return youtubeLinksAsString; + } catch (final IOException e) { + throw new IllegalArgumentException(e); + } + } + } + +} diff --git a/video/src/main/java/book/video/controller/YouTubeVideoLinkCreator.java b/video/src/main/java/book/video/controller/YouTubeVideoLinkCreator.java new file mode 100644 index 0000000..252598e --- /dev/null +++ b/video/src/main/java/book/video/controller/YouTubeVideoLinkCreator.java @@ -0,0 +1,21 @@ +package book.video.controller; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +// tag::test[] +public class YouTubeVideoLinkCreator { + + private static final String EMBED_URL = "https://www.youtube" + + ".com/embed/"; + + public URL createEmbeddedUrl(final String videoId) { + try { + return URI.create(EMBED_URL + videoId).toURL(); + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } +} +// end::test[] diff --git a/video/src/main/java/book/video/entity/YoutubeLink.java b/video/src/main/java/book/video/entity/YoutubeLink.java new file mode 100644 index 0000000..b357ec3 --- /dev/null +++ b/video/src/main/java/book/video/entity/YoutubeLink.java @@ -0,0 +1,35 @@ +package book.video.entity; + +import java.net.URL; +import java.util.function.Function; + +// tag::test[] +public class YoutubeLink { + + private final String videoId; + Function youTubeVideoLinkCreator; // <1> + + public YoutubeLink(final String videoId) { + this.videoId = videoId; + } + + public void setYouTubeVideoLinkCreator(final Function youTubeVideoLinkCreator) { + this.youTubeVideoLinkCreator = youTubeVideoLinkCreator; + } + + public URL getEmbedUrl() { + if (youTubeVideoLinkCreator != null) { + return youTubeVideoLinkCreator.apply(this.videoId); + } else { + throw new IllegalStateException + ("YouTubeVideoLinkCreator not set"); + } + } + + public String getVideoId() { + return videoId; + } + +} +// tag::test[] diff --git a/video/src/main/java/book/video/entity/YoutubeLinks.java b/video/src/main/java/book/video/entity/YoutubeLinks.java new file mode 100644 index 0000000..e3f1cb5 --- /dev/null +++ b/video/src/main/java/book/video/entity/YoutubeLinks.java @@ -0,0 +1,33 @@ +package book.video.entity; + +import java.net.MalformedURLException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class YoutubeLinks { + + private Set links; + + public YoutubeLinks(final Set youtubeLinks) { + this.links = new HashSet<>(youtubeLinks); + } + + public void addYoutubeLink(final YoutubeLink youtubeLink) + throws MalformedURLException { + this.links.add(youtubeLink); + } + + public List getYoutubeLinksAsString() { + return links.stream().map(youtubeLink -> youtubeLink + .getEmbedUrl().toString()).collect(Collectors + .toList()); + } + + public Set getYoutubeLinks() { + return Collections.unmodifiableSet(links); + } + +} diff --git a/video/src/test/java/YouTubeSearchTest.java b/video/src/test/java/YouTubeSearchTest.java new file mode 100644 index 0000000..af10381 --- /dev/null +++ b/video/src/test/java/YouTubeSearchTest.java @@ -0,0 +1,62 @@ +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.ResourceId; +import com.google.api.services.youtube.model.SearchListResponse; +import com.google.api.services.youtube.model.SearchResult; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +public class YouTubeSearchTest { + + private static final long NUMBER_OF_VIDEOS_RETURNED = 3; + private static final String KEY = Optional.ofNullable(System + .getenv("YOUTUBE_API_KEY")).orElse(Optional.ofNullable + (System.getProperty("YOUTUBE_API_KEY")).orElseGet(() -> + "dummy")); + + private YouTube youtube; + + //@Ignore //Until we have a valid API key + @Test + public void connect() throws IOException { + + youtube = new YouTube.Builder(new NetHttpTransport(), new + JacksonFactory(), null).setApplicationName("Gamer " + + "Video Microservice").build(); + final YouTube.Search.List search = youtube.search().list + ("id,snippet"); + search.setKey(KEY); + search.setQ("Zelda"); + + search.setType("video"); + search.setMaxResults(NUMBER_OF_VIDEOS_RETURNED); + search.setFields("items(id/kind,id/videoId,snippet/title)"); + search.setOrder("rating"); + + final SearchListResponse searchResponse = search.execute(); + final List searchResultList = searchResponse + .getItems(); + if (searchResultList != null) { + for (final SearchResult singleVideo : searchResultList) { + final ResourceId rId = singleVideo.getId(); + if (rId.getKind().equals("youtube#video")) { + + System.out.println(" Video Id " + rId + .getVideoId()); + System.out.println(" Title " + singleVideo + .getSnippet().getTitle()); + + System.out.println + ("\n-------------------------------------------------------------\n"); + + } + } + } + + } + +} diff --git a/video/src/test/java/book/video/controller/YouTubeVideoLinkCreatorTest.java b/video/src/test/java/book/video/controller/YouTubeVideoLinkCreatorTest.java new file mode 100644 index 0000000..bb03936 --- /dev/null +++ b/video/src/test/java/book/video/controller/YouTubeVideoLinkCreatorTest.java @@ -0,0 +1,25 @@ +package book.video.controller; + +import org.junit.Test; + +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::test[] +public class YouTubeVideoLinkCreatorTest { + + @Test // <1> + public void shouldReturnYouTubeEmbeddedUrlForGivenVideoId() { + // <2> + final YouTubeVideoLinkCreator youTubeVideoLinkCreator = new + YouTubeVideoLinkCreator(); // <3> + + final URL embeddedUrl = youTubeVideoLinkCreator + .createEmbeddedUrl("1234"); // <4> + + assertThat(embeddedUrl).hasHost("www.youtube.com").hasPath + ("/embed/1234"); // <5> + } +} +// end::test[] \ No newline at end of file diff --git a/video/src/test/java/book/video/entity/YouTubeLinkTest.java b/video/src/test/java/book/video/entity/YouTubeLinkTest.java new file mode 100644 index 0000000..aad368f --- /dev/null +++ b/video/src/test/java/book/video/entity/YouTubeLinkTest.java @@ -0,0 +1,25 @@ +package book.video.entity; + +import book.video.controller.YouTubeVideoLinkCreator; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +// tag::test[] +public class YouTubeLinkTest { + + @Test + public void shouldCalculateEmbedYouTubeLink() { + final YoutubeLink youtubeLink = new YoutubeLink("1234"); + + final YouTubeVideoLinkCreator youTubeVideoLinkCreator = new + YouTubeVideoLinkCreator(); // <1> + youtubeLink.setYouTubeVideoLinkCreator + (youTubeVideoLinkCreator::createEmbeddedUrl); // <2> + + assertThat(youtubeLink.getEmbedUrl()).hasHost("www.youtube" + + ".com").hasPath("/embed/1234"); + } + +} +// end::test[] \ No newline at end of file diff --git a/web/LICENSE b/web/LICENSE new file mode 100644 index 0000000..ad410e1 --- /dev/null +++ b/web/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..23ee5b7 --- /dev/null +++ b/web/README.md @@ -0,0 +1,22 @@ +# Apache TomEE JAX-RS AngularJS Starter Project + +[![Try it out in Codenvy](https://tomitribe.github.io/codenvy/tryitout.svg)](https://codenvy.com/f?id=dztffm6dfrw4s3ld) + +The only thing better than a Maven archetype is a repo you can fork with everything already setup. Skip the documentation and just fork-and-code. This starter project contains: + + - Java + -- Phone.java + -- PhoneService.java + -- PhoneServiceTest.java + + - JavaScript + -- angular.js + -- controllers.js + + - CSS + -- bootstrap.css + -- app.css + +Everything ready-to-run with a simple `maven clean install tomee:run` + +Delete the sample code, replace with your own and you're good to go. diff --git a/web/pom.xml b/web/pom.xml new file mode 100644 index 0000000..2ddae5b --- /dev/null +++ b/web/pom.xml @@ -0,0 +1,217 @@ + + + 4.0.0 + + org.mannning.microservices + gamer-web + 1.0.0-SNAPSHOT + + Gamer :: Web + war + + + 1.1.12.Final + 7.0-1 + 4.12 + 7.0.4 + UTF-8 + false + 1.8 + 1.8 + + + + + + + org.jboss.arquillian + arquillian-bom + ${version.arquillian-bom} + import + pom + + + org.jboss.arquillian.extension + arquillian-drone-bom + 2.0.0.Final + pom + import + + + + + + + + + + org.jboss.arquillian.graphene + graphene-webdriver + 2.1.0.Final + pom + test + + + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + + org.apache.tomee + javaee-api + ${version.javaee-api} + provided + + + org.apache.tomcat + tomcat-catalina + 8.5.3 + provided + + + + org.apache.tomee + arquillian-tomee-remote + ${version.tomee} + test + + + org.apache.tomee + tomee-jaxrs + ${version.tomee} + test + + + junit + junit + ${version.junit} + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-depchain + 2.2.6 + test + pom + + + org.mongodb + mongodb-driver + 3.2.2 + test + + + com.squareup.okhttp3 + okhttp + 3.8.1 + test + + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 2.0.0 + test + + + com.github.kstyrc + embedded-redis + 0.6 + + + + + + + gamerweb + + + + + maven-war-plugin + 3.0.0 + + + + + org.apache.tomee.maven + tomee-maven-plugin + ${version.tomee} + + target/gamerwebapp + plus + true + true + true + + + + + gamerwebapp + validate + + false + false + + + build + + + + + + + + + maven-resources-plugin + 2.7 + + + + + create-commentsservice + validate + + copy-resources + + + target/commentsservice + true + + + target/gamerwebapp + false + + + + + + + + create-gameaggregatorservice + validate + + copy-resources + + + target/gameaggregatorservice + true + + + target/gamerwebapp + false + + + + + + + + + + diff --git a/web/src/main/java/book/web/Phone.java b/web/src/main/java/book/web/Phone.java new file mode 100644 index 0000000..8efba8e --- /dev/null +++ b/web/src/main/java/book/web/Phone.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, + * Version 2.0 + * (the "License"); you may not use this file except in compliance + * with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package book.web; + + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class Phone { + + private int age; + private String carrier; + private String id; + private String name; + private String snippet; + + public Phone() { + } + + public Phone(final int age, final String carrier, final String + id, final String name, final String snippet) { + this.age = age; + this.carrier = carrier; + this.id = id; + this.name = name; + this.snippet = snippet; + } + + public int getAge() { + return age; + } + + public void setAge(final int age) { + this.age = age; + } + + public String getCarrier() { + return carrier; + } + + public void setCarrier(final String carrier) { + this.carrier = carrier; + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getSnippet() { + return snippet; + } + + public void setSnippet(final String snippet) { + this.snippet = snippet; + } + + @Override + public String toString() { + return "Phone{" + "age=" + age + ", carrier='" + carrier + + '\'' + ", id='" + id + '\'' + ", name='" + name + + '\'' + ", snippet='" + snippet + '\'' + '}'; + } +} diff --git a/web/src/main/java/book/web/PhoneService.java b/web/src/main/java/book/web/PhoneService.java new file mode 100644 index 0000000..486c049 --- /dev/null +++ b/web/src/main/java/book/web/PhoneService.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, + * Version 2.0 + * (the "License"); you may not use this file except in compliance + * with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package book.web; + +import javax.ejb.Lock; +import javax.ejb.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.Arrays; +import java.util.List; + +import static javax.ejb.LockType.READ; + +@Path("/phone") +@Singleton +@Lock(READ) +public class PhoneService { + + @GET + @Path("list") + @Produces(MediaType.APPLICATION_JSON) + public List getPhones() { + + return Arrays.asList(new Phone(0, "", + "motorola-xoom-with-wi-fi", "Motorola XOOM" + + "(tm) with Wi-Fi", "The Next, Next " + + "Generation\r\n\r\nExperience the future " + + "with Motorola XOOM with Wi-Fi, the world's" + + " first tablet powered by Android 3.0 " + + "(Honeycomb)."), + + new Phone(1, "", "motorola-xoom", "MOTOROLA XOOM" + + "(tm)", "The Next, Next " + + "Generation\n\nExperience the future with " + + "MOTOROLA XOOM, the world's first tablet " + + "powered by Android 3.0 (Honeycomb)."), + + new Phone(8, "", "samsung-galaxy-tab", "Samsung " + + "Galaxy Tab(tm)", "Feel Free to Tab(tm). " + + "The Samsung Galaxy Tab(tm) brings you an " + + "ultra-mobile entertainment experience " + + "through its 7\" display, high-power " + + "processor and Adobe(R) Flash(R) Player " + + "compatibility."), + + new Phone(11, "Verizon", "droid-pro-by-motorola", + "DROID(tm) Pro by Motorola", "The next " + + "generation of DOES."), + + new Phone(18, "", "t-mobile-g2", "T-Mobile G2", + "The T-Mobile G2 with Google is the first " + + "smartphone built for 4G speeds on " + + "T-Mobile's new network. Get the " + + "information you need, faster than " + + "you ever thought possible.") + + ); + + } + + +} diff --git a/web/src/main/java/book/web/ResourceEndpoints.java b/web/src/main/java/book/web/ResourceEndpoints.java new file mode 100644 index 0000000..e225298 --- /dev/null +++ b/web/src/main/java/book/web/ResourceEndpoints.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, + * Version 2.0 + * (the "License"); you may not use this file except in compliance + * with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package book.web; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import java.util.HashMap; +import java.util.Map; + +@Path("/") +public class ResourceEndpoints { + + @GET + public Map map() { + return new HashMap<>(); + } +} diff --git a/web/src/main/java/book/web/RestApplication.java b/web/src/main/java/book/web/RestApplication.java new file mode 100644 index 0000000..0b8e9ef --- /dev/null +++ b/web/src/main/java/book/web/RestApplication.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, + * Version 2.0 + * (the "License"); you may not use this file except in compliance + * with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package book.web; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/api") +public class RestApplication extends Application { +} diff --git a/web/src/main/tomee/conf/tomee.xml b/web/src/main/tomee/conf/tomee.xml new file mode 100644 index 0000000..10504e0 --- /dev/null +++ b/web/src/main/tomee/conf/tomee.xml @@ -0,0 +1,18 @@ + + + + BrokerXmlConfig broker:(vm://localbroker)?useJmx=false + ServerUrl vm://localbroker?waitForStart=30000&async=true + DataSource + StartupTimeout 10 seconds + + + + ResourceAdapter Default JMS Resource Adapter + TransactionSupport xa + PoolMaxSize 50 + PoolMinSize 2 + ConnectionMaxWaitTime 30 seconds + + + \ No newline at end of file diff --git a/web/src/main/webapp/WEB-INF/web.xml b/web/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..4ae5594 --- /dev/null +++ b/web/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,42 @@ + + + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + *,http://localhost:8181 + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + + Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + false + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + + \ No newline at end of file diff --git a/web/src/main/webapp/assets/css/app.css b/web/src/main/webapp/assets/css/app.css new file mode 100644 index 0000000..e8d83bc --- /dev/null +++ b/web/src/main/webapp/assets/css/app.css @@ -0,0 +1,6 @@ +/* app css stylesheet */ + +body { + padding-top: 20px; +} + diff --git a/web/src/main/webapp/assets/css/bootstrap.css b/web/src/main/webapp/assets/css/bootstrap.css new file mode 100644 index 0000000..5169324 --- /dev/null +++ b/web/src/main/webapp/assets/css/bootstrap.css @@ -0,0 +1,5784 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +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 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +@media print { + * { + color: #000 !important; + text-shadow: none !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: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #428bca; + text-decoration: none; +} +a:hover, +a:focus { + color: #2a6496; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #999; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 200; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +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: #999; +} +.text-primary { + color: #428bca; +} +a.text-primary:hover { + color: #3071a9; +} +.text-success { + color: #3c763d; +} +a.text-success:hover { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #428bca; +} +a.bg-primary:hover { + background-color: #3071a9; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #999; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +blockquote:before, +blockquote:after { + content: ""; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + white-space: nowrap; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: 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: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-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; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr.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: 15px; + overflow-x: scroll; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + 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; + /* IE8-9 */ + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +input[type="date"] { + line-height: 34px; +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + display: block; + min-height: 20px; + padding-left: 20px; + margin-top: 10px; + margin-bottom: 10px; +} +.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; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +.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: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.has-feedback .form-control-feedback { + position: absolute; + top: 25px; + right: 0; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + 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: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.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; + padding-left: 0; + margin-top: 0; + margin-bottom: 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 { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +.form-horizontal .form-control-static { + padding-top: 7px; +} +@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; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus, +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + color: #333; + background-color: #ebebeb; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #428bca; + border-color: #357ebd; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + color: #fff; + background-color: #3276b1; + border-color: #285e8e; +} +.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: #428bca; + border-color: #357ebd; +} +.btn-primary .badge { + color: #428bca; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + color: #fff; + background-color: #47a447; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + color: #fff; + background-color: #39b3d7; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ed9c28; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + color: #fff; + background-color: #d2322d; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #428bca; + 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: #2a6496; + 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: #999; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 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 .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height .35s ease; + transition: height .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; + font-size: 14px; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #999; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px solid; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group > .btn: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-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +[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-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.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: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #999; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #999; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #428bca; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #428bca; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + max-height: 340px; + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + 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-right: 0; + padding-left: 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; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.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; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 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: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } + .navbar-nav.navbar-right:last-child { + margin-right: -15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .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; + padding-left: 0; + margin-top: 0; + margin-bottom: 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; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 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-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } + .navbar-text.navbar-right:last-child { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #999; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #999; +} +.navbar-inverse .navbar-nav > li > a { + color: #999; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #999; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #999; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #999; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #428bca; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #2a6496; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #999; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +.label[href]:hover, +.label[href]:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #999; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #808080; +} +.label-primary { + background-color: #428bca; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3071a9; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #999; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #428bca; + background-color: #fff; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.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-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #428bca; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable { + padding-right: 35px; +} +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-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: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media, +.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 { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +a.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +a.list-group-item:focus { + 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: #fff; + background-color: #428bca; + border-color: #428bca; +} +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: #e1edf7; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +a.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +a.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +a.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group { + 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-left-radius: 3px; + border-top-right-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-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child 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 #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + overflow: hidden; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #428bca; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #428bca; + border-color: #428bca; +} +.panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: #428bca; +} +.panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #428bca; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #ebccd1; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: auto; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -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; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.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: #fff; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + min-height: 16.42857143px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 20px; +} +.modal-footer { + padding: 19px 20px 20px; + margin-top: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 12px; + line-height: 1.4; + visibility: visible; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + right: 5px; + bottom: 0; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + 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 { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + 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; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .5) 0%), color-stop(rgba(0, 0, 0, .0001) 100%)); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .0001) 0%), color-stop(rgba(0, 0, 0, .5) 100%)); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: none; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; +} +.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%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + .carousel-caption { + right: 20%; + left: 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 { + display: table; + content: " "; +} +.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-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +@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; + } +} diff --git a/web/src/main/webapp/assets/js/angular.js b/web/src/main/webapp/assets/js/angular.js new file mode 100644 index 0000000..57be3a8 --- /dev/null +++ b/web/src/main/webapp/assets/js/angular.js @@ -0,0 +1,333 @@ +/* + AngularJS v1.6.3 + (c) 2010-2017 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(w){'use strict';function M(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.6.3/"+(a?a+"/":"")+d;for(d=1;dc)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f").append(a).html();try{return a[0].nodeType===Ia?P(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+P(b)})}catch(c){return P(d)}}function Nc(a){try{return decodeURIComponent(a)}catch(b){}}function Oc(a){var b={};p((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Nc(e),u(e)&&(f=u(f)?Nc(f):!0,ua.call(b,e)?H(b[e])? +b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Zb(a){var b=[];p(a,function(a,c){H(a)?p(a,function(a){b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))}):b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))});return b.length?b.join("&"):""}function db(a){return $(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function $(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function te(a, +b){var d,c,e=Ja.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))} +function Pc(a,b,d){G(d)||(d={});d=R({strictDi:!1},d);var c=function(){a=F(a);if(a.injector()){var c=a[0]===w.document?"document":xa(a);throw Fa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector", +d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;w&&e.test(w.name)&&(d.debugInfoEnabled=!0,w.name=w.name.replace(e,""));if(w&&!f.test(w.name))return c();w.name=w.name.replace(f,"");ea.resumeBootstrap=function(a){p(a,function(a){b.push(a)});return c()};E(ea.resumeDeferredBootstrap)&&ea.resumeDeferredBootstrap()}function we(){w.name="NG_ENABLE_DEBUG_INFO!"+w.name;w.location.reload()}function xe(a){a=ea.element(a).injector();if(!a)throw Fa("test");return a.get("$$testability")} +function Qc(a,b){b=b||"_";return a.replace(ye,function(a,c){return(c?b:"")+a.toLowerCase()})}function ze(){var a;if(!Rc){var b=sb();(na=x(b)?w.jQuery:b?w[b]:void 0)&&na.fn.on?(F=na,R(na.fn,{scope:Oa.scope,isolateScope:Oa.isolateScope,controller:Oa.controller,injector:Oa.injector,inheritedData:Oa.inheritedData}),a=na.cleanData,na.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=na._data(f,"events"))&&c.$destroy&&na(f).triggerHandler("$destroy");a(b)}):F=W;ea.element=F;Rc=!0}}function fb(a, +b,d){if(!a)throw Fa("areq",b||"?",d||"required");return a}function tb(a,b,d){d&&H(a)&&(a=a[a.length-1]);fb(E(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ka(a,b){if("hasOwnProperty"===a)throw Fa("badname",b);}function Sc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g")+c[2];for(c=c[0];c--;)d=d.lastChild;f=ab(f,d.childNodes); +d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";p(f,function(a){e.appendChild(a)});return e}function W(a){if(a instanceof W)return a;var b;D(a)&&(a=S(a),b=!0);if(!(this instanceof W)){if(b&&"<"!==a.charAt(0))throw cc("nosel");return new W(a)}if(b){b=w.document;var d;a=(d=dg.exec(a))?[b.createElement(d[1])]:(d=bd(a,b))?d.childNodes:[];dc(this,a)}else E(a)?cd(a):dc(this,a)}function ec(a){return a.cloneNode(!0)}function yb(a,b){b||hb(a);if(a.querySelectorAll)for(var d= +a.querySelectorAll("*"),c=0,e=d.length;cl&&this.remove(q.key);return b}},get:function(a){if(l";b=ta.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function Ma(a, +b){try{a.addClass(b)}catch(c){}}function ca(a,b,c,d,e){a instanceof F||(a=F(a));var f=Na(a,b,a,c,d,e);ca.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw fa("multilink");fb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==wa(d)&&ma.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?F(ha(g,F("

").append(a).html())): +c?Oa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);ca.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Na(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,n,q,r;if(J)for(r=Array(c.length),m=0;mz.priority)break;if(w=z.scope)z.templateUrl||(G(w)?($("new/isolated scope",C||J,z,y),C=z):$("new/isolated scope",C,z,y)),J=J||z;v=z.name;if(!u&&(z.replace&&(z.templateUrl||z.template)||z.transclude&&!z.$$tlb)){for(w=A+1;u=a[w++];)if(u.transclude&&!u.$$tlb||u.replace&&(u.templateUrl||u.template)){Ma=!0;break}u=!0}!z.templateUrl&& +z.controller&&(B=B||V(),$("'"+v+"' controller",B[v],z,y),B[v]=z);if(w=z.transclude)if(I=!0,z.$$tlb||($("transclusion",t,z,y),t=z),"element"===w)X=!0,q=z.priority,T=y,y=d.$$element=F(ca.$$createComment(v,d[v])),b=y[0],ka(f,va.call(T,0),b),T[0].$$parentNode=T[0].parentNode,N=ic(Ma,T,e,q,g&&g.name,{nonTlbTranscludeDirective:t});else{var ja=V();if(G(w)){T=[];var P=V(),kb=V();p(w,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;P[a]=b;ja[b]=null;kb[b]=c});p(y.contents(),function(a){var b=P[Ba(wa(a))]; +b?(kb[b]=!0,ja[b]=ja[b]||[],ja[b].push(a)):T.push(a)});p(kb,function(a,b){if(!a)throw fa("reqslot",b);});for(var gc in ja)ja[gc]&&(ja[gc]=ic(Ma,ja[gc],e))}else T=F(ec(b)).contents();y.empty();N=ic(Ma,T,e,void 0,void 0,{needsNewScope:z.$$isolateScope||z.$$newScope});N.$$slots=ja}if(z.template)if(O=!0,$("template",L,z,y),L=z,w=E(z.template)?z.template(y,d):z.template,w=Ea(w),z.replace){g=z;T=bc.test(w)?nd(ha(z.templateNamespace,S(w))):[];b=T[0];if(1!==T.length||1!==b.nodeType)throw fa("tplrt",v,""); +ka(f,y,b);D={$attr:{}};w=hc(b,[],D);var Y=a.splice(A+1,a.length-(A+1));(C||J)&&aa(w,C,J);a=a.concat(w).concat(Y);da(d,D);D=a.length}else y.html(w);if(z.templateUrl)O=!0,$("template",L,z,y),L=z,z.replace&&(g=z),n=ga(a.splice(A,a.length-A),y,d,f,I&&N,h,k,{controllerDirectives:B,newScopeDirective:J!==z&&J,newIsolateScopeDirective:C,templateDirective:L,nonTlbTranscludeDirective:t}),D=a.length;else if(z.compile)try{Q=z.compile(y,d,N);var Z=z.$$originalDirective||z;E(Q)?m(null,bb(Z,Q),Na,M):Q&&m(bb(Z,Q.pre), +bb(Z,Q.post),Na,M)}catch(ea){c(ea,xa(y))}z.terminal&&(n.terminal=!0,q=Math.max(q,z.priority))}n.scope=J&&!0===J.scope;n.transcludeOnThisElement=I;n.templateOnThisElement=O;n.transclude=N;l.hasElementTranscludeDirective=X;return n}function U(a,b,c,d){var e;if(D(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw fa("ctreq",b,a);}else if(H(b))for(e= +[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=Wb(c,{$$start:k,$$end:l}));if(!c.$$bindings){var J=m=c,r=c.name,B={isolateScope:null,bindToController:null};G(J.scope)&&(!0===J.bindToController?(B.bindToController=d(J.scope,r,!0),B.isolateScope={}):B.isolateScope=d(J.scope,r,!1));G(J.bindToController)&&(B.bindToController=d(J.bindToController,r,!0));if(B.bindToController&&!J.controller)throw fa("noctrl", +r);m=m.$$bindings=B;G(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function Z(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return z.HTML;var c=wa(a);if("src"===b||"ngSrc"===b){if(-1===["img","video","audio","source","track"].indexOf(c))return z.RESOURCE_URL}else if("xlinkHref"===b||"form"===c&&"action"===b||"link"===c&&"href"===b)return z.RESOURCE_URL}function qa(a, +c,d,e,f){var g=oa(a,e),h=k[e]||f,l=b(d,!f,g,h);if(l){if("multiple"===e&&"select"===wa(a))throw fa("selmulti",xa(a));if(m.test(e))throw fa("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=V());var k=f[e];k!==d&&(l=k&&b(k,!0,g,h),d=k);l&&(f[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function ka(a,b,c){var d=b[0],e= +b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Ia&&""===d.nodeValue.trim())&&sg.call(a,b,1)}return a}function qg(a,b){if(b&&D(b))return b;if(D(a)){var d=qd.exec(a);if(d)return d[3]}}function wf(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Ka(b,"controller");G(b)? +R(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!G(a.$scope))throw M("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&D(k)&&(n=k);if(D(f)){k=f.match(qd);if(!k)throw rd("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Sc(g.$scope,m,!0)||(b?Sc(c,m,!0):void 0);if(!f)throw rd("ctrlreg",m);tb(f,m,!0)}if(h)return h=(H(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n, +l,m||f.name),R(function(){var a=d.invoke(f,l,g,m);a!==l&&(G(a)||E(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function xf(){this.$get=["$window",function(a){return F(a.document)}]}function yf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function zf(){this.$get= +["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function kc(a){return G(a)?ga(a)?a.toISOString():cb(a):a}function Ef(){this.$get=function(){return function(a){if(!a)return"";var b=[];Hc(a,function(a,c){null===a||x(a)||(H(a)?p(a,function(a){b.push($(c)+"="+$(kc(a)))}):b.push($(c)+"="+$(kc(a))))});return b.join("&")}}}function Ff(){this.$get=function(){return function(a){function b(a,e,f){null===a||x(a)||(H(a)?p(a,function(a,c){b(a,e+"["+(G(a)?c:"")+"]")}):G(a)&&!ga(a)?Hc(a,function(a, +c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push($(e)+"="+$(kc(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function lc(a,b){if(D(a)){var d=a.replace(tg,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(sd))||(c=(c=d.match(ug))&&vg[c[0]].test(d));c&&(a=Lc(d))}}return a}function td(a){var b=V(),d;D(a)?p(a.split("\n"),function(a){d=a.indexOf(":");var e=P(S(a.substr(0,d)));a=S(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):G(a)&&p(a,function(a,d){var f=P(d),g=S(a);f&&(b[f]=b[f]? +b[f]+", "+g:g)});return b}function ud(a){var b;return function(d){b||(b=td(a));return d?(d=b[P(d)],void 0===d&&(d=null),d):b}}function vd(a,b,d,c){if(E(c))return c(a,b,d);p(c,function(c){a=c(a,b,d)});return a}function Df(){var a=this.defaults={transformResponse:[lc],transformRequest:[function(a){return G(a)&&"[object File]"!==ma.call(a)&&"[object Blob]"!==ma.call(a)&&"[object FormData]"!==ma.call(a)?cb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:qa(mc),put:qa(mc),patch:qa(mc)}, +xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return u(a)?(b=!!a,this):b};var d=this.interceptors=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(c,e,f,g,h,k,l,m){function n(b){function d(a,b){for(var c=0,e=b.length;ca?b:k.reject(b)}if(!G(b))throw M("$http")("badreq",b);if(!D(m.valueOf(b.url)))throw M("$http")("badreq",b.url);var g=R({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=function(b){var c=a.headers,d=R({},b.headers), +f,g,h,c=R({},c.common,c[P(b.method)]);a:for(f in c){g=P(f);for(h in d)if(P(h)===g)continue a;d[f]=c[f]}return e(d,qa(b))}(b);g.method=vb(g.method);g.paramSerializer=D(g.paramSerializer)?l.get(g.paramSerializer):g.paramSerializer;c.$$incOutstandingRequestCount();var h=[],n=[];b=k.resolve(g);p(t,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&n.push(a.response,a.responseError)});b=d(b,h);b=b.then(function(b){var c=b.headers,d=vd(b.data,ud(c), +void 0,b.transformRequest);x(d)&&p(c,function(a,b){"content-type"===P(b)&&delete c[b]});x(b.withCredentials)&&!x(a.withCredentials)&&(b.withCredentials=a.withCredentials);return q(b,d).then(f,f)});b=d(b,n);return b=b.finally(function(){c.$$completeOutstandingRequest(A)})}function q(c,d){function g(a){if(a){var c={};p(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function l(a,c,d,e){function f(){q(c,a,d,e)}O&&(200<=a&&300>a?O.put(Q,[a, +c,td(d),e]):O.remove(Q));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function q(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?C.resolve:C.reject)({data:a,status:b,headers:ud(d),config:c,statusText:e})}function J(a){q(a.data,a.status,qa(a.headers()),a.statusText)}function t(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var C=k.defer(),z=C.promise,O,X,T=c.headers,s="jsonp"===P(c.method),Q=c.url;s?Q=m.getTrustedResourceUrl(Q):D(Q)||(Q=m.valueOf(Q));Q=r(Q,c.paramSerializer(c.params)); +s&&(Q=I(Q,c.jsonpCallbackParam));n.pendingRequests.push(c);z.then(t,t);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=G(c.cache)?c.cache:G(a.cache)?a.cache:N);O&&(X=O.get(Q),u(X)?X&&E(X.then)?X.then(J,J):H(X)?q(X[1],X[0],qa(X[2]),X[3]):q(X,200,{},"OK"):O.put(Q,z));x(X)&&((X=wd(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(T[c.xsrfHeaderName||a.xsrfHeaderName]=X),e(c.method,Q,d,l,T,c.timeout,c.withCredentials,c.responseType,g(c.eventHandlers),g(c.uploadEventHandlers))); +return z}function r(a,b){0=l&&(y.resolve(t),p(v.$$intervalId),delete g[v.$$intervalId]);K||a.$apply()},k);g[v.$$intervalId]=y;return v}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].promise.catch(A),g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete g[a.$$intervalId],!0):!1};return f}]}function nc(a){a=a.split("/");for(var b=a.length;b--;)a[b]= +db(a[b]);return a.join("/")}function yd(a,b){var d=Ca(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Z(d.port)||xg[d.protocol]||null}function zd(a,b){if(yg.test(a))throw lb("badpath",a);var d="/"!==a.charAt(0);d&&(a="/"+a);var c=Ca(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=Oc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function oc(a,b){return a.slice(0,b.length)=== +b}function ka(a,b){if(oc(b,a))return b.substr(a.length)}function Aa(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function mb(a){return a.replace(/(#.+)|#$/,"$1")}function pc(a,b,d){this.$$html5=!0;d=d||"";yd(a,this);this.$$parse=function(a){var d=ka(b,a);if(!D(d))throw lb("ipthprfx",a,b);zd(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Zb(this.$$search),d=this.$$hash?"#"+db(this.$$hash):"";this.$$url=nc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+ +this.$$url.substr(1);this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;u(f=ka(a,c))?(g=f,g=d&&u(f=ka(d,f))?b+(ka("/",f)||f):a+g):u(f=ka(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function qc(a,b,d){yd(a,this);this.$$parse=function(c){var e=ka(a,c)||ka(b,c),f;x(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",x(e)&&(a=c,this.replace())):(f=ka(d,e),x(f)&&(f=e));zd(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;oc(f, +e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=nc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"");this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(b,d){return Aa(a)===Aa(b)?(this.$$parse(b),!0):!1}}function Ad(a,b,d){this.$$html5=!0;qc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)), +!0;var f,g;a===Aa(c)?f=c:(g=ka(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=nc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url;this.$$urlUpdatedByLocation=!0}}function Kb(a){return function(){return this[a]}}function Bd(a,b){return function(d){if(x(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Jf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0}; +this.hashPrefix=function(b){return u(b)?(a=b,this):a};this.html5Mode=function(a){if(Ha(a))return b.enabled=a,this;if(G(a)){Ha(a.enabled)&&(b.enabled=a.enabled);Ha(a.requireBase)&&(b.requireBase=a.requireBase);if(Ha(a.rewriteLinks)||D(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state= +f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var n=c.url(),q;if(b.enabled){if(!m&&b.requireBase)throw lb("nobase");q=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?pc:Ad}else q=Aa(n),m=qc;var r=q.substr(0,Aa(q).lastIndexOf("/")+1);l=new m(q,r,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var I=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&& +2!==a.which&&2!==a.button){for(var h=F(a.target);"a"!==wa(h[0]);)if(h[0]===f[0]||!(h=h.parent())[0])return;if(!D(e)||!x(h.attr(e))){var e=h.prop("href"),k=h.attr("href")||h.attr("xlink:href");G(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Ca(e.animVal).href);I.test(e)||!e||h.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(e,k)||(a.preventDefault(),l.absUrl()!==c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}}});mb(l.absUrl())!==mb(n)&&c.url(l.absUrl(),!0);var p=!0; +c.onUrlChange(function(a,b){oc(a,r)?(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=mb(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(p=!1,k(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(p||l.$$urlUpdatedByLocation){l.$$urlUpdatedByLocation=!1;var a=mb(c.url()),b=mb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(p||m)p= +!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))})}l.$$replace=!1});return l}]}function Kf(){var a=!0,b=this;this.debugEnabled=function(b){return u(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||A;a=!1;try{a=!!e.apply}catch(f){}return a?function(){var a=[];p(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}var f=za||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function zg(a){return a+""}function Ag(a, +b){return"undefined"!==typeof a?a:b}function Cd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function U(a,b){var d,c,e;switch(a.type){case s.Program:d=!0;p(a.body,function(a){U(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:U(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant; +a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:U(a.test,b);U(a.alternate,b);U(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:U(a.object,b);a.computed&&U(a.property,b);a.constant=a.object.constant&& +(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=e=a.filter?!b(a.callee.name).$stateful:!1;c=[];p(a.arguments,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=e?c:[a];break;case s.AssignmentExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];p(a.elements,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant= +d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];p(a.properties,function(a){U(a.value,b);d=d&&a.value.constant&&!a.computed;a.value.constant||c.push.apply(c,a.value.toWatch);a.computed&&(U(a.key,b),a.key.constant||c.push.apply(c,a.key.toWatch))});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Dd(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Ed(a){return a.type=== +s.Identifier||a.type===s.MemberExpression}function Fd(a){if(1===a.body.length&&Ed(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function Gd(a){return 0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression)}function Hd(a,b){this.astBuilder=a;this.$filter=b}function Id(a,b){this.astBuilder=a;this.$filter= +b}function rc(a){return E(a.valueOf)?a.valueOf():Bg.call(a)}function Lf(){var a=V(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||c||(a=rc(a),"object"!==typeof a)?a===b||a!==a&&b!==b:!1}function g(a,b,c,d,e){var g=d.inputs,h;if(1===g.length){var k=f,g=g[0];return a.$watch(function(a){var b=g(a); +f(b,k,d.literal)||(h=d(a,void 0,void 0,[b]),k=b&&rc(b));return h},b,c,e)}for(var l=[],m=[],n=0,L=g.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++,f)ua.call(e,b)||(p--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1p&&(x=4-p,u[x]||(u[x]=[]),u[x].push({msg:E(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){r=!1;break a}}catch(F){f(F)}if(!(q=t.$$watchersCount&&t.$$childHead||t!==this&&t.$$nextSibling))for(;t!==this&&!(q=t.$$nextSibling);)t=t.$parent}while(t=q);if((r||y.length)&&!p--)throw K.$$phase=null,d("infdig",b,u);}while(r||y.length);for(K.$$phase=null;Bza)throw ta("iequirks");var c=qa(oa);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;p(oa,function(a,b){var d=P(b);c[("parse_as_"+d).replace(uc,gb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(uc,gb)]=function(b){return f(a, +b)};c[("trust_as_"+d).replace(uc,gb)]=function(b){return g(a,b)}});return c}]}function Rf(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=Z((/android (\d+)/.exec(P((a.navigator||{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h=g.body&&g.body.style,k=!1,l=!1;h&&(k=!!("transition"in h||"webkitTransition"in +h),l=!!("animation"in h||"webkitAnimation"in h));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&za)return!1;if(x(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ga(),transitions:k,animations:l,android:e}}]}function Tf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(h,k){g.totalPendingRequests++;if(!D(h)||x(d.get(h)))h=f.getTrustedResourceUrl(h);var l= +c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==lc}):l===lc&&(l=null);return c.get(h,R({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){d.put(h,a.data);return a.data},function(a){k||(a=Dg("tpload",h,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function Uf(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding"); +var g=[];p(a,function(a){var c=ea.element(a).data("$binding");c&&p(c,function(c){d?(new RegExp("(^|\\s)"+Kd(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;hc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===wc;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===wc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Ud&&(d=d.splice(0,Ud-1),b=c-1,c=1);return{d:d,e:b,i:c}}function Lg(a,b,d,c){var e=a.d,f= +e.length-a.i;b=x(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;fh;)k.unshift(0),h++;0=b.lgSize&&h.unshift(k.splice(-b.lgSize,k.length).join(""));k.length>b.gSize;)h.unshift(k.splice(-b.gSize,k.length).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+ +k+b.posSuf}function Lb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Lb(f,b,c,e)}}function nb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Vd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Wd(a){return function(b){var d= +Vd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Lb(b,a)}}function xc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Pd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));h.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;h=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]|| +0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;D(c)&&(c=Mg.test(c)?Z(c):b(c));ba(c)&&(c=new Date(c));if(!ga(c)||!isFinite(c.getTime()))return c;for(;d;)(l=Ng.exec(d))?(h=ab(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=Mc(f,m),c=Yb(c,f,!0));p(h,function(b){k=Og[b];g+=k?k(c,a.DATETIME_FORMATS,m): +"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Fg(){return function(a,b){x(b)&&(b=2);return cb(a,b)}}function Gg(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):Z(b);if(da(b))return a;ba(a)&&(a=a.toString());if(!ra(a))return a;d=!d||isNaN(d)?0:Z(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?yc(a,d,d+b):0===d?yc(a,b,a.length):yc(a,Math.max(0,d+b),d)}}function yc(a,b,d){return D(a)?a.slice(b,d):va.call(a,b,d)}function Rd(a){function b(b){return b.map(function(b){var c= +1,d=Ya;if(E(b))d=b;else if(D(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,k=b.type;if(d===k){var k=a.value,l=b.value;"string"===d?(k=k.toLowerCase(),l=l.toLowerCase()):"object"===d&&(G(k)&&(k=a.index),G(l)&&(l=b.index));k!==l&&(c= +kb||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut",m)}b.on("change",l);if(ae[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown", +function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Ob(a,b){return function(d,c){var e,f;if(ga(d))return d;if(D(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(Pg.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+ +1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},p(e,function(a,c){c=s};g.$observe("min",function(a){s=q(a);h.$validate()})}if(u(g.max)||g.ngMax){var t;h.$validators.max=function(a){return!n(a)||x(t)||d(a)<=t};g.$observe("max",function(a){t= +q(a);h.$validate()})}}}function Ac(a,b,d,c){(c.$$hasNativeValidators=G(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?void 0:a})}function be(a){a.$$parserName="number";a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(Qg.test(b))return parseFloat(b)});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!ba(b))throw qb("numfmt",b);b=b.toString()}return b})}function Sa(a){u(a)&&!ba(a)&&(a=parseFloat(a));return da(a)?void 0:a}function Bc(a){var b= +a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ce(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Bc(a):0,h=e?Bc(b):0,k=f?Bc(d):0,g=Math.max(g,h,k),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function de(a,b,d,c,e){if(u(c)){a=a(c);if(!a.constant)throw qb("constexpr",d,c);return a(b)}return e}function Cc(a,b){function d(a,b){if(!a|| +!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/,bc=/<|&#?\w+;/,bg=/<([\w:-]+)/,cg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ha={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"", +"
"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var jg=w.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Oa=W.prototype={ready:cd,toString:function(){var a=[];p(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?F(this[a]):F(this[this.length+a])},length:0,push:Tg,sort:[].sort,splice:[].splice},Gb={};p("multiple selected checked disabled readOnly required open".split(" "), +function(a){Gb[P(a)]=a});var hd={};p("input select option textarea button form details".split(" "),function(a){hd[a]=!0});var pd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};p({data:fc,removeData:hb,hasData:function(a){for(var b in ib[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b/,mg=/^[^(]*\(\s*([^)]*)\)/m,Wg=/,/,Xg=/^\s*(_?)(\S+?)\1\s*$/,kg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,ya=M("$injector");eb.$$annotate=function(a,b,d){var c;if("function"=== +typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw D(d)&&d||(d=a.name||ng(a)),ya("strictdi",d);b=jd(a);p(b[1].split(Wg),function(a){a.replace(Xg,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,tb(a[b],"fn"),c=a.slice(0,b)):tb(a,"fn",!0);return c};var fe=M("$animate"),qf=function(){this.$get=A},rf=function(){var a=new Hb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=D(b)?b.split(" "):H(b)?b:[],p(b,function(b){b&&(d=!0,a[b]=c)})); +return d}function f(){p(b,function(b){var c=a.get(b);if(c){var d=og(b.attr("class")),e="",f="";p(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});p(b,function(a){e&&Db(a,e);f&&Cb(a,f)});a.delete(b)}});b.length=0}return{enabled:A,on:A,off:A,pin:A,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.set(g,k),b.push(g),1===b.length&&c.$$postDigest(f); +g=new d;g.complete();return g}}}]},of=["$provide",function(a){var b=this,d=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw fe("notcsel",c);var f=c+"-animation";b.$$registeredAnimations[c.substr(1)]=f;a.factory(f,d)};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,fe("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a, +c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Rb[a]=!0});var $g={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},sc=function(a){this.options=a};sc.prototype={constructor:sc,lex:function(a){this.text=a;this.index= +0;for(this.tokens=[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a, +this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)- +56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=u(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ua("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression, +operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")? +a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=sa(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")): +"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(",")) +}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break; +b={type:s.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}"); +return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw Ua("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ua("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ua("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c, +e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression},$locals:{type:s.LocalsExpression}}};Hd.prototype={compile:function(a){var b=this;a=this.astBuilder.ast(a);this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};U(a,b.$filter);var d="",c;this.stage="assign"; +if(c=Fd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Dd(a.body);b.stage="inputs";p(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var h=b.nextId();b.recurse(a,h);b.return_(h);b.state.inputs.push(d);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);d='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+ +d+this.watchFns()+"return fn;";d=(new Function("$filter","getStringValue","ifDefined","plus",d))(this.$filter,zg,Ag,Cd);this.state=this.stage=void 0;d.literal=Gd(a);d.constant=a.constant;return d},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;p(b,function(b){a.push("var "+b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"}, +filterPrefix:function(){var a=[],b=this;p(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m,n;c=c||A;if(!f&&u(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e, +!0));else switch(a.type){case s.Program:p(a.body,function(b,c){k.recurse(b.expression,void 0,void 0,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case s.UnaryExpression:this.recurse(a.argument,void 0,void 0,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right, +void 0,void 0,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId(); +d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.isNull(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));c(b);break;case s.MemberExpression:g=d&& +(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,void 0,function(){k.if_(k.notNull(g),function(){a.computed?(h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.computedMember(g,h),k.assign(b,m),d&&(d.computed=!0,d.name=h)):(e&&1!==e&&k.if_(k.isNull(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}")),m=k.nonComputedMember(g, +a.property.name),k.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],p(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)):(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){p(a.arguments,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})}); +m=g.name?k.member(g.context,g.name,g.computed)+"("+l.join(",")+")":h+"("+l.join(",")+")";k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};this.recurse(a.left,void 0,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);m=k.member(g.context,g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];p(a.elements,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})}); +m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case s.ObjectExpression:l=[];n=!1;p(a.properties,function(a){a.computed&&(n=!0)});n?(b=b||this.nextId(),this.assign(b,"{}"),p(a.properties,function(a){a.computed?(g=k.nextId(),k.recurse(a.key,g)):g=a.key.type===s.Identifier?a.key.name:""+a.key.value;h=k.nextId();k.recurse(a.value,h);k.assign(k.member(b,g,a.computed),h)})):(p(a.properties,function(b){k.recurse(b.value,a.constant?void 0:k.nextId(),void 0,function(a){l.push(k.escape(b.key.type===s.Identifier? +b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case s.ThisExpression:this.assign(b,"s");c(b||"s");break;case s.LocalsExpression:this.assign(b,"l");c(b||"l");break;case s.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)|| +(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a, +b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g, +stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(D(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(ba(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ua("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}}; +Id.prototype={compile:function(a){var b=this;a=this.astBuilder.ast(a);U(a,b.$filter);var d,c;if(d=Fd(a))c=this.recurse(d);d=Dd(a.body);var e;d&&(e=[],p(d,function(a,c){var d=b.recurse(a);a.input=d;e.push(d);a.watchId=c}));var f=[];p(a.body,function(a){f.push(b.recurse(a.expression))});d=0===a.body.length?A:1===a.body.length?f[0]:function(a,b){var c;p(f,function(d){c=d(a,b)});return c};c&&(d.assign=function(a,b,d){return c(a,d,b)});e&&(d.inputs=e);d.literal=Gd(a);d.constant=a.constant;return d},recurse:function(a, +b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test), +this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return f.identifier(a.name,b,d);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c,e,b,d);case s.CallExpression:return g=[],p(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c, +d,f){for(var n=[],q=0;q":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c, +e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,h){var k=a(e,f,g,h),l,m;null!=k&&(l=b(e,f,g, +h),l+="",c&&1!==c&&k&&!k[l]&&(k[l]={}),m=k[l]);return d?{context:k,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};var tc=function(a,b,d){this.lexer=a;this.$filter=b;this.options=d;this.ast=new s(a,d);this.astCompiler=d.csp?new Id(this.ast,b):new Hd(this.ast,b)};tc.prototype={constructor:tc, +parse:function(a){return this.astCompiler.compile(a)}};var ta=M("$sce"),oa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},uc=/_([a-z])/g,Dg=M("$compile"),aa=w.document.createElement("a"),Md=Ca(w.location.href);Nd.$inject=["$document"];$c.$inject=["$provide"];var Ud=22,Td=".",wc="0";Od.$inject=["$locale"];Qd.$inject=["$locale"];var Og={yyyy:Y("FullYear",4,0,!1,!0),yy:Y("FullYear",2,0,!0,!0),y:Y("FullYear",1,0,!1,!0),MMMM:nb("Month"),MMM:nb("Month",!0),MM:Y("Month",2,1),M:Y("Month", +1,1),LLLL:nb("Month",!1,!0),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",1),sss:Y("Milliseconds",3),EEEE:nb("Day"),EEE:nb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Lb(Math[0=a.getFullYear()? +b.ERANAMES[0]:b.ERANAMES[1]}},Ng=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,Mg=/^-?\d+$/;Pd.$inject=["$locale"];var Hg=la(P),Ig=la(vb);Rd.$inject=["$parse"];var Fe=la({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ma.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};p(Gb,function(a,b){function d(a, +d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=Ba("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});p(pd,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(Sg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});p(["src","srcset","href"],function(a){var b= +Ba("ng-"+a);wb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===ma.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),za&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Nb={$addControl:A,$$renameControl:function(a,b){a.$name=b},$removeControl:A,$setValidity:A,$setDirty:A,$setPristine:A,$setSubmitted:A};Mb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Mb.prototype= +{$rollbackViewValue:function(){p(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){p(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ka(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];p(this.$pending,function(b,d){this.$setValidity(d, +null,a)},this);p(this.$error,function(b,d){this.$setValidity(d,null,a)},this);p(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);$a(this.$$controls,a);a.$$parentForm=Nb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Va);this.$$animate.addClass(this.$$element,Sb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Va,Sb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;p(this.$$controls, +function(a){a.$setPristine()})},$setUntouched:function(){p(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;this.$$parentForm.$setSubmitted()}};Zd({clazz:Mb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&($a(c,d),0===c.length&&delete a[b])}});var ge=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign: +d(a).assign||A}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Mb,compile:function(d,f){d.addClass(Va).addClass(ob);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var q=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",q);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",q)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n); +var r=g?c(n.$name):A;g&&(r(a,n),e.$observe(g,function(b){n.$name!==b&&(r(a,void 0),n.$$parentForm.$$renameControl(n,b),r=c(n.$name),r(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);r(a,void 0);R(n,Nb)})}}}}}]},Ge=ge(),Se=ge(!0),Pg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,ah=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,bh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/, +Qg=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,he=/^(\d{4,})-(\d{2})-(\d{2})$/,ie=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Ec=/^(\d{4,})-W(\d\d)$/,je=/^(\d{4,})-(\d\d)$/,ke=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ae=V();p(["date","datetime-local","month","time","week"],function(a){ae[a]=!0});var le={text:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);zc(c)},date:pb("date",he,Ob(he,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":pb("datetimelocal",ie,Ob(ie,"yyyy MM dd HH mm ss sss".split(" ")), +"yyyy-MM-ddTHH:mm:ss.sss"),time:pb("time",ke,Ob(ke,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:pb("week",Ec,function(a,b){if(ga(a))return a;if(D(a)){Ec.lastIndex=0;var d=Ec.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Vd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:pb("month",je,Ob(je,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Ac(a,b,d,c);be(c);Ra(a,b,d,c,e,f);var g, +h;if(u(d.min)||d.ngMin)c.$validators.min=function(a){return c.$isEmpty(a)||x(g)||a>=g},d.$observe("min",function(a){g=Sa(a);c.$validate()});if(u(d.max)||d.ngMax)c.$validators.max=function(a){return c.$isEmpty(a)||x(h)||a<=h},d.$observe("max",function(a){h=Sa(a);c.$validate()});if(u(d.step)||d.ngStep){var k;c.$validators.step=function(a,b){return c.$isEmpty(b)||x(k)||ce(b,g||0,k)};d.$observe("step",function(a){k=Sa(a);c.$validate()})}},url:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);zc(c);c.$$parserName= +"url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||ah.test(d)}},email:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);zc(c);c.$$parserName="email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||bh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==S(d.ngTrim);x(d.name)&&b.attr("name",++rb);b.on("click",function(a){var g;b[0].checked&&(g=d.value,e&&(g=S(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=S(a));b[0].checked=a===c.$viewValue}; +d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);d.$observe(a,c)}function h(a){n=Sa(a);da(c.$modelValue)||(m?(a=b.val(),n>a&&(a=n,b.val(a)),c.$setViewValue(a)):c.$validate())}function k(a){q=Sa(a);da(c.$modelValue)||(m?(a=b.val(),q=n},g("min",h));e&&(c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||x(q)||b<=q},g("max",k));f&&(c.$validators.step=m?function(){return!p.stepMismatch}:function(a, +b){return c.$isEmpty(b)||x(r)||ce(b,n||0,r)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,h){var k=de(h,a,"ngTrueValue",d.ngTrueValue,!0),l=de(h,a,"ngFalseValue",d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return pa(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:A,button:A,submit:A,reset:A,file:A},Uc=["$browser","$sniffer","$filter", +"$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(le[P(g.type)]||le.text)(e,f,g,h[0],b,a,d,c)}}}}],ch=/^(true|false|\d+)$/,kf=function(){function a(a,d,c){var e=u(c)?c:9===za?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return ch.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Ke=["$compile",function(a){return{restrict:"AC", +compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=$b(a)})}}}}],Me=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=x(a)?"":a})}}}}],Le=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c, +e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],jf=la({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ne=Cc("",!0),Pe=Cc("Odd",0),Oe=Cc("Even",1),Qe=Qa({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Re=[function(){return{restrict:"A", +scope:!0,controller:"@",priority:500}}],Zc={},dh={blur:!0,focus:!0};p("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=Ba("ng-"+a);Zc[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b]);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};dh[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ue=["$animate","$compile", +function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=ub(h.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),h=null))})}}}],Ve=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400, +terminal:!0,transclude:"element",controller:ea.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,q){var r=0,p,s,t,x=function(){s&&(s.remove(),s=null);p&&(p.$destroy(),p=null);t&&(d.leave(t).done(function(a){!1!==a&&(s=null)}),s=t,t=null)};c.$watch(f,function(f){var m=function(a){!1===a||!u(h)||h&&!c.$eval(h)||b()},s=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&s===r){var b=c.$new();n.template=a;a=q(b,function(a){x();d.enter(a,null,e).done(m)}); +p=b;t=a;p.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||s!==r||(x(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(x(),n.template=null)})}}}}],mf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){ma.call(d[0]).match(/SVG/)?(d.empty(),a(bd(e.template,w.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],We=Qa({priority:450,compile:function(){return{pre:function(a, +b,d){a.$eval(d.ngInit)}}}}),hf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?S(e):e;c.$parsers.push(function(a){if(!x(a)){var b=[];a&&p(a.split(g),function(a){a&&b.push(f?S(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},ob="ng-valid",Yd="ng-invalid",Va="ng-pristine",Sb="ng-dirty",qb=M("ngModel");Pb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" "); +Pb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);E(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){E(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw qb("nonassign",this.$$attr.ngModel,xa(this.$$element));},$render:A,$isEmpty:function(a){return x(a)|| +""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Sb);this.$$animate.addClass(this.$$element,Va)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element, +Va);this.$$animate.addClass(this.$$element,Sb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!da(this.$modelValue)){var a= +this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;p(k.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(p(k.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;p(k.$asyncValidators,function(e, +g){var k=e(a,b);if(!k||!E(k.then))throw qb("nopromise",k);f(g,void 0);c.push(k.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?k.$$q.all(c).then(function(){g(d)},A):g(!0)}function f(a,b){h===k.$$currentValidationRunId&&k.$setValidity(a,b)}function g(a){h===k.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var h=this.$$currentValidationRunId,k=this;(function(){var a=k.$$parserName||"parse";if(x(k.$$parserValid))f(a,null);else return k.$$parserValid||(p(k.$validators,function(a, +b){f(b,null)}),p(k.$asyncValidators,function(a,b){f(b,null)})),f(a,k.$$parserValid),k.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;if(this.$$parserValid= +x(a)?void 0:!0)for(var d=0;de||c.$isEmpty(b)|| +b.length<=e}}}}},Xc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Z(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};w.angular.bootstrap?w.console&&console.log("WARNING: Tried to load angular more than once."):(ze(),Ce(ea),ea.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM", +"PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5, +6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, +c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),F(function(){ue(w.document,Pc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(''); + diff --git a/web/src/main/webapp/assets/js/controllers.js b/web/src/main/webapp/assets/js/controllers.js new file mode 100644 index 0000000..3ca4af8 --- /dev/null +++ b/web/src/main/webapp/assets/js/controllers.js @@ -0,0 +1,67 @@ +'use strict'; + +/* Controllers */ + +var gamerApp = angular.module('gamerApp', ['ngRoute']); + +gamerApp.config(['$locationProvider', '$routeProvider', + function ($locationProvider, $routeProvider) { + + $locationProvider.hashPrefix(''); + + $routeProvider.when("/", { + templateUrl: 'detail.html' + }).when("/detail/:id", { + templateUrl: 'detail.html', + controller: DetailCntl + }).otherwise({ + redirectTo: '/' + }); + } +]); + +AppCntl.$inject = ['$scope', '$route']; + +function AppCntl($scope, $route, $location) { + console.log("Routing to " + $route); + $scope.$route = $route; +} + +function DetailCntl($scope, $route, $location, $http) { + + $scope.init = function () { + console.log("Calling game service for " + $route.current.params.id); + $http.get("http://localhost:8181/" + $route.current.params.id).then(success, error); + }; + + $scope.init(); + + function success(response) { + var data = response.data; + console.log(data); + $scope.selectedGame = data; + } + + function error(error) { + console.log("Unexpected error:"); + console.log(error); + } +} + +gamerApp.controller('gamerCtrl', function ($scope, $http) { + + $scope.searchText = ""; + + $scope.searchGame = function () { + $http.get("http://localhost:8181?query=" + $scope.searchText).then(success, error); + }; + + function success(response) { + $scope.games = response.data; + } + + function error(error) { + console.log("Unexpected error: " + error); + } + +}); diff --git a/web/src/main/webapp/assets/js/route.js b/web/src/main/webapp/assets/js/route.js new file mode 100644 index 0000000..acdb350 --- /dev/null +++ b/web/src/main/webapp/assets/js/route.js @@ -0,0 +1,17 @@ +/* + AngularJS v1.6.3 + (c) 2010-2017 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(J,d){'use strict';function A(d){k&&d.get("$route")}function B(t,u,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,c,m){function v(){l&&(g.cancel(l),l=null);n&&(n.$destroy(),n=null);p&&(l=g.leave(p),l.done(function(a){!1!==a&&(l=null)}),p=null)}function E(){var b=t.current&&t.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),c=t.current;p=m(b,function(b){g.enter(b,null,p||f).done(function(b){!1===b||!d.isDefined(w)||w&&!a.$eval(w)||u()}); +v()});n=c.scope=b;n.$emit("$viewContentLoaded");n.$eval(k)}else v()}var n,p,l,w=b.autoscroll,k=b.onload||"";a.$on("$routeChangeSuccess",E);E()}}}function C(d,k,g){return{restrict:"ECA",priority:-400,link:function(a,f){var b=g.current,c=b.locals;f.html(c.$template);var m=d(f.contents());if(b.controller){c.$scope=a;var v=k(b.controller,c);b.controllerAs&&(a[b.controllerAs]=v);f.data("$ngControllerController",v);f.children().data("$ngControllerController",v)}a[b.resolveAs||"$resolve"]=c;m(a)}}}var x, +y,F,G,z=d.module("ngRoute",[]).info({angularVersion:"1.6.3"}).provider("$route",function(){function t(a,f){return d.extend(Object.create(a),f)}function u(a,d){var b=d.caseInsensitiveMatch,c={originalPath:a,regexp:a},g=c.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[?*])?/g,function(a,b,d,c){a="?"===c||"*?"===c?"?":null;c="*"===c||"*?"===c?"*":null;g.push({name:d,optional:!!a});b=b||"";return""+(a?"":b)+"(?:"+(a?b:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([/$*])/g, +"\\$1");c.regexp=new RegExp("^"+a+"$",b?"i":"");return c}x=d.isArray;y=d.isObject;F=d.isDefined;G=d.noop;var g={};this.when=function(a,f){var b;b=void 0;if(x(f)){b=b||[];for(var c=0,m=f.length;c + IGDB Game ID: {{selectedGame.id}} +
+

Full Name: {{selectedGame.title}}

+
+ +
+
+ Search for and select a game... +
\ No newline at end of file diff --git a/web/src/main/webapp/favicon.ico b/web/src/main/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..af89ddb5fdb93dcb7d6a73aef9dd1bfa3aba3593 GIT binary patch literal 32988 zcmeI42aps+7l0QfDoR#Fk&`SIK}0bjp%4%O$r(XGQ8E@{M6!Y+K?Ot!l0iWfMJz#* zflvm5fs7;tgfoB{egAyz^;YlP%+Aj49*6(UtJ>Y!3Ell(zkazV6v`IL9;#I;|T+yOw|MP@GlbVJ?`K|Nn{_LU9s&=7JdFvVLw$Kpk!+AIO z`|rQ^TA$P``T_S-l0^aBnfEM;zG$8g@XwkQ2UxM|YruT#S&zclCo8;f& z!-wU`C!du1_3O*6x85p`KmNGv*|R4@C+2Lyf8V}+(xy!t$(}vCSa!PlWXqOKZomC@ z`TFaxGmHj)@&DtGKjf27K9RL+*UGVD$C5IxfByML#*Q5;=bUp+7+a1r-&Az#)vK2@ zZroU|yz)w^Q>Tt>+_+I5dgvh~ORv8AYDJfN_3BBE966+J-MSI-)DQm9bjXk)a@l2< zNuff86#et$$sn%`&&MBsEGJH!kSSB9NcZmDrCYadGGW35Ie743 z824xgjP3K!KUZ@xd-iO}moJ~RY}qo5>+$2qmF&u!H?Q>V+ZVf6HRlI^9zYI#`st^N z##^>*k->upOMwCf1fE>CZe6+|vbO*F>#vCOn>KAye2FY!jI;+YXkdPkwebS{aK#l@ z$c7CYBATF0Wa)L+T_^i2eGB--Up-`e{rTsgYR$0@S&vJWED>;~?{VYC$%79*C_{%1 zmBot}%MU;Npq_l@nP=3T4;wa2kWa}3<_A0C#v5;x{rmSvG|k$%_uhNus;jP2H0NTX z`0EbP4Jj!pa_Oa)Dp`(PFxN6`51EZE!?>7p&4ap)Wc*WZ_R+t5&U&vSrIg;BC@?<5sO&h53MIG6JSfovOxwu6*N-HRW!E}%CxBoqI=d-p2(+Hp4jqd#WOoT|BMHgKp#flY^>C>mnhaY|@efso~ zi!Z)d&OiTrdG5LAZzv` z?JHKSD9e^D3%7!;j6SVWrHZnj!xz#H;>90bvU250>CvNy(%H~p`SRt;7IT_&eGY%7 zfB*iXX=Wdf-~ZSVi~(JG+ikZg-^^@-`9|hfu3TBZ{PIilk~;Q_Keq0xufD46t%3y$ zDw;p>#1l%#Xee>tRL)4?c+kb@#DwKO*h@7>`Lql^w~S_yc4me;Js)%fOUvnU|)y!aeMv$ z=%bI6j%U3+^2j3s?VZ36{@jbLf-M7087s8ImLU#-?T8Mv+;PVp z0)L?Qv2n4v1`Zsk;ySd$@sT4(?2mi?r_Tio7ARY*c=6&gapFYBap;Ep;LqII{hQpm zbF26R@d^54oMzJfOefC*JS9lEj z#0zbvReOQPbM2!uOmi`P;&d)Yp+!@$E^=|20s{HY}c-xqaJg6)(`&J#P|-ZS#T%* zh#!INm=W;XZ@;M+1@Z=;g!P4O2ydXXu_uth`0mIM^rp7kV;zql{ArVQ`~36ID}912 z+`4sZtX;)@AM%pe1wOkee~|6itk`eF0$Bfk!7u*0ee|H0Y|-~7;};u3uQ&TIaxz|f zDPH`Ofm5t)Kx=F?JKp*~@g*l+8%z8D1^kf%_(U3C`!6w@cz7TB(Dr@z-6zBWk)w97 z+kf`!8T}u zzSVFh@kfReBO(@s%`<7zB*&bT9viV6;#kC#{Q@@jEw|i~4u5hN#K<(9Dg24UV8alb zA}&ch-0E8o!||83N=%u&mS6pc-h;Pt<;oR7gVwEEhkbfFJrnq2>*Mb-7UBo?yo4k2 z)=Abd<0Y0JtG`2A@4ox4lKI%r#2Uznu@=aS*kd3WjlY)h#J!07ImMvd@aH^vh5Y&R ztK4O@`?v<3!Izk?xz5QSl6SxsutT!&r|n(4b}3(0=XA)mHfq#J#Z7wl>=_|5B)hY&!Uqj6V zGCdx!#;4{KIZkfd36PKIOK^wB{nQL4ia+r|a+r2KbNS_$tJoB^GKu<8k8cp*{zS@&-E z6Z5s#o5V9td_U&j^pB_s@*4|s>N`>Vv1Q2xIMIN(d@^da$XDt-hwg`bRxEK-C!Ec5 ziQpwo{ zJR=3u8EV_&efB?R>HY7#Cm_Z8)UrNj@p}R?b)SA3pJbYyfQ;sIhL%OTZvG$o#}}uD zi2MV&6?@((9n!`LH~fiblb^oXSW7JYq1J^MMl8OfTmQsCXcL^g@`S|B%>JiM zn>e0o?khmYn?`#30@BRODiaQxU5q7ED}-eG$wDb!Ei3 zp&zw;)VFbs*daA{#IlK@y7kZ4*ds%}uvV>Fsuq{MFx2v~9|f9#Be6mqm-nh=BTu1g z6NzoG|1etri~*pQk2*;+9zr@4x?k%-utO z;>^_i!Bg;ng?rTgxemQ`uE*X#u}UvsPFRz~xisyW2c4@n=}%0RdssIwzx=Ywu{3Yq zT-E60q|$dQQv3+c|y*^Uv_Zo zpEwsWC}>69eXKyd3OPfr1boR6!RPjR4Behv|N447zw8w?c@z2P2E?4%m#A||n*MJ0 z$8w(hx7mNR`9n5R_mzzPk-zNyLG~i!sR5w=!mXyt!d2z-i04v^!dT+PpBOOnjERiqz^i|1D$M@b1LpG)|$k~(E zhsV5f#If}6)n6jlgZ`EOm1%Jji~gA%r`SuJz64`ljp_>$Ny z=3Ya8n#01tGazO)Q;9VK)$ATZRiQU?#Mg-pmzmnJq_O~721!^atH~i1u1KKsgt$p@R z=s6@-<_6e)*x$ract@5U+}fv(5MPD;Uevt00sABvhugk1xAtid*{J;!xAvipjyX8p z6Rmxx`~99vYd@p)dxqL4*h$u_m{m^2!%j*h@03tJ#GlVMq4#W}RRdghJhBz*EOxn9&!p|d9(7EtEavgm}EE(NPj08UzJ;VD|kOjyyYi)&} z*Zm^%iTx4ZaN4I$42pLj5oflJQ!Sa1ifpHjjy>gOeBNHSN#8ujy3_GkYrLt{pE4dD zCq(Dr*Yd6nWDK;U9t<0SV`7bIdmmD**k&sF&k0zE?4c(HF>BVW@GZzrd~K8d;fuxr z>?JRpI9sTsQZ zfj-DLYV+wI-EJR0w1QW7H;)~#0f-HAj|Thm&=$Ho=_k&^dnTkkAGbeeJ)fvQw%5Wp zfPex41p*2L6bL8~P#~Z{K!Jb)0R;jI1QZA;5KthXKtO?j0s#dA3Ir4gC=gH}pg=%@ bfC2#p0ty5a2q+LxAfP}%fq(-4F$(+-VH=Z$ literal 0 HcmV?d00001 diff --git a/web/src/main/webapp/index.html b/web/src/main/webapp/index.html new file mode 100644 index 0000000..f8e8eb8 --- /dev/null +++ b/web/src/main/webapp/index.html @@ -0,0 +1,37 @@ + + + + + + + Google Phone Gallery + + + + + + + + + +
+
+
+ + +
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/web/src/test/java/book/web/EndToEndTest.java b/web/src/test/java/book/web/EndToEndTest.java new file mode 100644 index 0000000..559dd02 --- /dev/null +++ b/web/src/test/java/book/web/EndToEndTest.java @@ -0,0 +1,231 @@ +package book.web; + +import book.web.page.Detail; +import book.web.page.Index; +import book.web.page.List; +import book.web.rule.DefaultJavaResolutionStrategy; +import book.web.rule.MicroserviceRule; +import book.web.rule.MongodRule; +import book.web.rule.RedisRule; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.importer.ZipImporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.archive.importer.MavenImporter; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URL; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertThat; + +@SuppressWarnings("ArquillianTooManyDeployment") +@RunWith(Arquillian.class) +public class EndToEndTest { + + // tag::rules[] + @ClassRule + public static RuleChain chain = RuleChain // <1> + .outerRule(new MongodRule("localhost", 27017)) // <2> + .around(new RedisRule(6379)) // <3> + .around(new MicroserviceRule("http://localhost:8899/?videoId=5123&gameName=Zelda").withExecutableJar + (getFile("video/build/libs/video-service-0.1.0.jar"), "--server.port=8899") + .withJavaResolutionStrategy(new DefaultJavaResolutionStrategy()).withTimeout(1, TimeUnit.MINUTES) + ) // <4> + + .around(new MicroserviceRule("http://localhost:8181?query=").withExecutableJar(getFile + ("game/target/gameservice-swarm.jar"), "-Dswarm" + ".http.port=8181").withJavaResolutionStrategy + (new DefaultJavaResolutionStrategy()).withTimeout(1, TimeUnit.MINUTES)); + // end::rules[] + + // tag::deployments[] + @Deployment(name = "commentsservice", testable = false) + @TargetsContainer("commentsservice") + public static Archive commentsservice() throws Exception { + + // Could use the EmbeddedGradleImporter.class to import the + // project output, this way we know we're using the real deal. + // See book.aggr.CommentsGatewayTest#createCommentsDeployment + + return ShrinkWrap.create(ZipImporter.class, "commentsservice.war").importFrom(getFile + ("comments/build/libs/commentsservice.war")).as(WebArchive.class).addAsLibraries(Maven.resolver() + .resolve("org.mongodb:mongodb-driver:3.2.2").withTransitivity().as(JavaArchive.class)).addClass + (MongoClientProvider.class).addAsWebInfResource("test-web.xml", "web.xml").addAsWebInfResource + ("test-resources.xml", "resources.xml"); + } + + @Deployment(name = "gameaggregatorservice", testable = false) + @TargetsContainer("gameaggregatorservice") + public static Archive gameaggregatorservice() throws Exception { + return ShrinkWrap.create(ZipImporter.class, "gameaggregatorservice.war").importFrom(getFile + ("aggregator/build/libs/gameaggregatorservice.war")).as(WebArchive.class).addAsLibraries(Maven + .resolver().resolve("org.mongodb:mongodb-driver:3.2.2").withTransitivity().as(JavaArchive.class)) + .addClass(MongoClientProvider.class).addAsWebInfResource("test-web.xml", "web.xml") + .addAsWebInfResource("test-resources.xml", "resources.xml"); + } + + @Deployment(name = "gamerweb", testable = false) // <1> + @TargetsContainer("gamerweb") // <2> + public static Archive gamerWebService() throws Exception { + //This is the parent project of this test + return ShrinkWrap.create(MavenImporter.class).loadPomFromFile("pom.xml").importBuildOutput().as(WebArchive + .class).addAsWebInfResource("test-web.xml", "web.xml"); + } + // end::deployments[] + + // tag::operate[] + private static final AtomicReference commentsservice = new AtomicReference<>(); + private static final AtomicReference gameaggregatorservice = new AtomicReference<>(); + private static final AtomicReference gamerweb = new AtomicReference<>(); + + @Test + @InSequence(1) + @OperateOnDeployment("commentsservice") + public void testRunningInCommentsService(@ArquillianResource final URL url) throws Exception { + commentsservice.set(url); + Assert.assertNotNull(commentsservice.get()); + assertThat(commentsservice.get().toExternalForm(), containsString("commentsservice")); + } + + @Test + @InSequence(2) + @OperateOnDeployment("gameaggregatorservice") + public void testRunningInGameAggregatorService(@ArquillianResource final URL url) throws Exception { + gameaggregatorservice.set(url); + Assert.assertNotNull(gameaggregatorservice.get()); + assertThat(gameaggregatorservice.get().toExternalForm(), containsString("gameaggregatorservice")); + } + + @Test + @InSequence(4) + @OperateOnDeployment("gamerweb") + public void testRunningInGamerWeb(@ArquillianResource final URL url) throws Exception { + gamerweb.set(url); + Assert.assertNotNull(gamerweb.get()); + assertThat(gamerweb.get().toExternalForm(), containsString("gamerweb")); + } + // end::operate[] + + // tag::exes[] + @Test + @InSequence(5) + public void testSpringBootVideoService() throws Exception { + Assert.assertTrue(connect("http://localhost:8899/?videoId=5123&gameName" + "=Zelda")); + } + + @Test + @InSequence(6) + public void testWildFlyGameService() throws Exception { + Assert.assertTrue(connect("http://localhost:8181?query=")); + } + //end::exes[] + + // tag::drone[] + @Page + @OperateOnDeployment("gamerweb") + private Index page; // <1> + + @Test + @InSequence(7) + public void testTheUI() throws Exception { + + //Just nice to see we have access to this URL + System.out.println("gameaggregatorservice = " + gameaggregatorservice.get().toExternalForm()); + + // <2> + page.navigateTo(gamerweb.get().toExternalForm()); + List list = page.searchFor("Zelda"); + + Assert.assertEquals("", "Zelda", page.getSearchText()); + + Assert.assertThat(list.getResults(), hasItems("The Legend of Zelda: Breath of the Wild")); + + Detail detail = list.getDetail(0); + + Assert.assertTrue(detail.getImageURL().startsWith("http")); + + // <3> + //Just here for debugging & development :: curl + // localhost:9999 to terminate + if (null != System.getProperty("dev.hack")) { + new ServerSocket(9999).accept(); + } + } + // end::drone[] + + /** + * Simple utility method to locate the pre-built microservice + * war files running in the context of either maven or the IDE + * + * @param path Path to war file + * @return File + */ + private static synchronized File getFile(final String path) { + + try { + File f = new File("../code"); + if (f.exists()) { + f = new File(f, path); + return getFile(f); + } + + f = new File(".").getCanonicalFile(); + + if (f.getName().contains("web")) { + f = new File(f.getParent(), path); + } + + return getFile(f); + } catch (final IOException ioe) { + throw new RuntimeException("Failed to get war: " + path, ioe); + } + } + + /** + * Ensures the file is accessible. + * + * @param f File to check + * @return Checked file + */ + private static File getFile(final File f) { + Assert.assertTrue("Failed to find: " + f.toString(), f.exists()); + Assert.assertTrue("Please ensure that the service has been " + "" + "" + "built", f.exists()); + Assert.assertTrue("Unable to set readable", f.setReadable(true)); + Assert.assertTrue("File is not readable", f.canRead()); + Assert.assertTrue("Unable to set executable", f.setExecutable(true)); + Assert.assertTrue("File is not executable", f.canExecute()); + return f; + } + + /** + * Simple connect method to test for a known endpoint + * + * @param url A known endpoint + * @return true if able to connect, else false + */ + private static boolean connect(final String url) throws IOException { + Request request = new Request.Builder().url(url).build(); + return new OkHttpClient().newCall(request).execute().isSuccessful(); + } +} diff --git a/web/src/test/java/book/web/MongoClientProvider.java b/web/src/test/java/book/web/MongoClientProvider.java new file mode 100644 index 0000000..41e2805 --- /dev/null +++ b/web/src/test/java/book/web/MongoClientProvider.java @@ -0,0 +1,26 @@ +package book.web; + +import com.mongodb.MongoClient; + +/** + * Provides the MongoClient + */ +public class MongoClientProvider { + + private final MongoClient mongoClient; + private final String database; + + public MongoClientProvider(final String address, final int + port, final String database) { + this.mongoClient = new MongoClient(address, port); + this.database = database; + } + + public MongoClient getMongoClient() { + return this.mongoClient; + } + + public String getDatabase() { + return database; + } +} diff --git a/web/src/test/java/book/web/page/Detail.java b/web/src/test/java/book/web/page/Detail.java new file mode 100644 index 0000000..9838ac2 --- /dev/null +++ b/web/src/test/java/book/web/page/Detail.java @@ -0,0 +1,17 @@ +package book.web.page; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +public class Detail { + + @Drone + private WebDriver browser; + + + public String getImageURL() { + return browser.findElement(By.id("game-cover")) + .getAttribute("src"); + } +} diff --git a/web/src/test/java/book/web/page/Index.java b/web/src/test/java/book/web/page/Index.java new file mode 100644 index 0000000..1b08745 --- /dev/null +++ b/web/src/test/java/book/web/page/Index.java @@ -0,0 +1,45 @@ +package book.web.page; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.Graphene; +import org.jboss.arquillian.graphene.page.Location; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Represents the index page + */ +@Location("/") // <1> +public class Index { + + @Drone + private WebDriver browser; // <2> + + @FindBy(id = "tms-search") // <3> + private WebElement search; + + @FindBy(id = "tms-button") + private WebElement button; + + @FindBy(className = "col-sm-3") + private List list; // <4> + + // <5> + public void navigateTo(String url) { + browser.manage().window().maximize(); + browser.get(url); + } + + public List searchFor(String text) { + search.sendKeys(text); // <6> + + Graphene.guardAjax(button).click(); // <7> + + return list; // <8> + } + + public String getSearchText() { + return search.getAttribute("value"); + } +} diff --git a/web/src/test/java/book/web/page/List.java b/web/src/test/java/book/web/page/List.java new file mode 100644 index 0000000..9ab3e29 --- /dev/null +++ b/web/src/test/java/book/web/page/List.java @@ -0,0 +1,50 @@ +package book.web.page; + +import org.jboss.arquillian.graphene.Graphene; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Represents the search result list + */ +public class List { + + @FindBy(className = "list-group") + private WebElement list; + + @FindBy(id = "detail-view") + private Detail detail; // <1> + + public Collection getResults() { // <2> + + ArrayList results = new ArrayList<>(); + + if (null != list) { + java.util.List elements = list.findElements + (By.cssSelector("a > p")); + + for (WebElement element : elements) { + results.add(element.getText()); + } + } + + return results; + } + + public Detail getDetail(int index) { + + java.util.List elements = list.findElements(By + .cssSelector("a > p")); + + if (!elements.isEmpty()) { + Graphene.guardAjax(elements.get(index)).click(); // <3> + } + + + return detail; // <4> + } +} diff --git a/web/src/test/java/book/web/rule/DefaultJavaResolutionStrategy.java b/web/src/test/java/book/web/rule/DefaultJavaResolutionStrategy.java new file mode 100644 index 0000000..f6a28ef --- /dev/null +++ b/web/src/test/java/book/web/rule/DefaultJavaResolutionStrategy.java @@ -0,0 +1,32 @@ +package book.web.rule; + +import org.junit.Assert; + +import java.io.File; +import java.io.IOException; + +public class DefaultJavaResolutionStrategy implements + ResolutionStrategy { + + public File getJavaExecutable() { + + //Locate java + final String javaHome = System.getenv("JAVA_HOME"); + final File java; + try { + java = new File(javaHome + "/bin/java" + (System + .getProperty("os.name").toLowerCase().contains + ("win") ? ".exe" : "")) + .getCanonicalFile(); + } catch (IOException e) { + throw new AssertionError("Failed to determine " + + "canonical path to java using JAVA_HOME: " + + javaHome, e); + } + + Assert.assertTrue("Ensure that JAVA_HOME points to " + "a " + + "valid java installation", java.exists()); + + return java; + } +} diff --git a/web/src/test/java/book/web/rule/MicroserviceRule.java b/web/src/test/java/book/web/rule/MicroserviceRule.java new file mode 100644 index 0000000..20dab4b --- /dev/null +++ b/web/src/test/java/book/web/rule/MicroserviceRule.java @@ -0,0 +1,154 @@ +package book.web.rule; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.junit.Assert; +import org.junit.rules.ExternalResource; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; + +public class MicroserviceRule extends ExternalResource { + + private final Logger log = Logger.getLogger(MicroserviceRule.class.getName()); + + private final ReentrantLock lock = new ReentrantLock(); + private final CountDownLatch latch = new CountDownLatch(1); + private final AtomicBoolean poll = new AtomicBoolean(true); + private final AtomicReference url = new AtomicReference<>(); + private File file; + private String[] args; + private ResolutionStrategy strategy = new DefaultJavaResolutionStrategy(); + private long time = 30; + private TimeUnit unit = TimeUnit.SECONDS; + + public MicroserviceRule(URL url) { + this.url.set(url); + } + + public MicroserviceRule(String url) { + try { + this.url.set(new URL(url)); + } catch (MalformedURLException e) { + throw new RuntimeException("Invalid URL: " + url, e); + } + } + + public MicroserviceRule withExecutableJar(File file, String... args) { // <1> + + Assert.assertTrue("The file must exist and be readable: " + file, file.exists() && file.canRead()); + + this.file = file; + this.args = args; + return this; + } + + public MicroserviceRule withJavaResolutionStrategy(ResolutionStrategy strategy) { + this.strategy = (null != strategy ? strategy : this.strategy); + return this; + } + + public MicroserviceRule withTimeout(int time, TimeUnit unit) { + this.time = time; + this.unit = unit; + return this; + } + + private Process process; + + @Override + protected void before() throws Throwable { + + Assert.assertNotNull("The MicroserviceRule requires a valid jar file", this.file); + Assert.assertNotNull("The MicroserviceRule requires a valid url", this.url.get()); + + this.lock.lock(); + + try { + ArrayList args = new ArrayList<>(); + args.add(this.strategy.getJavaExecutable().toString()); // <2> + args.add("-jar"); + args.add(this.file.toString()); + + if (null != this.args) { + args.addAll(Arrays.asList(this.args)); + } + + ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); + pb.directory(file.getParentFile()); + pb.inheritIO(); + process = pb.start(); // <3> + + log.info("Started " + this.file); + + final Thread t = new Thread(() -> { + if (MicroserviceRule.this.connect(MicroserviceRule.this.url.get())) { // <4> + MicroserviceRule.this.latch.countDown(); + } + }, "Connect thread :: " + this.url.get()); + + t.start(); + + if (!latch.await(this.time, this.unit)) { // <5> + throw new RuntimeException("Failed to connect to server within timeout: " + + this.url.get()); + } + + } finally { + this.poll.set(false); + this.lock.unlock(); + } + } + + @Override + protected void after() { + + this.lock.lock(); // <6> + + try { + if (null != process) { + process.destroy(); + process = null; + } + } finally { + this.lock.unlock(); + } + } + + private boolean connect(final URL url) { + + do { + try { + Request request = new Request.Builder().url(url).build(); + + if (new OkHttpClient().newCall(request).execute().isSuccessful()) { // <7> + return true; + } else { + throw new Exception("Unexpected family"); + } + } catch (Exception ignore) { + + if (poll.get()) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + return false; + } + } + } + } while (poll.get()); + + return false; + } + + +} diff --git a/web/src/test/java/book/web/rule/MongodRule.java b/web/src/test/java/book/web/rule/MongodRule.java new file mode 100644 index 0000000..3c0feea --- /dev/null +++ b/web/src/test/java/book/web/rule/MongodRule.java @@ -0,0 +1,51 @@ +package book.web.rule; + +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodProcess; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.runtime.Network; +import org.junit.rules.ExternalResource; + +import java.io.IOException; + +public class MongodRule extends ExternalResource { + + private final MongodStarter starter + = MongodStarter.getDefaultInstance(); + private MongodExecutable mongodExe; + private MongodProcess mongodProcess; + private String host; + private int port; + + public MongodRule(String host, int port) { + this.host = host; + this.port = port; + } + + @Override + protected void before() throws Throwable { // <1> + try { + mongodExe = starter.prepare(new MongodConfigBuilder() + .version(Version.Main.PRODUCTION) + .net(new Net(this + .host, this.port, Network.localhostIsIPv6())).build()); + mongodProcess = mongodExe.start(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void after() { // <2> + //Stop MongoDB + if (null != mongodProcess) { + mongodProcess.stop(); + } + if (null != mongodExe) { + mongodExe.stop(); + } + } +} diff --git a/web/src/test/java/book/web/rule/RedisRule.java b/web/src/test/java/book/web/rule/RedisRule.java new file mode 100644 index 0000000..7483d8b --- /dev/null +++ b/web/src/test/java/book/web/rule/RedisRule.java @@ -0,0 +1,32 @@ +package book.web.rule; + +import org.junit.rules.ExternalResource; +import redis.embedded.RedisServer; + +public class RedisRule extends ExternalResource { + + private RedisServer redisServer; + private int port; + + public RedisRule(int port) { + this.port = port; + } + + @Override + protected void before() throws Throwable { + try { + redisServer = new RedisServer(this.port); + redisServer.start(); + } catch (final Throwable e) { + e.printStackTrace(); + } + } + + @Override + protected void after() { + //Stop Redis + if (null != redisServer) { + redisServer.stop(); + } + } +} diff --git a/web/src/test/java/book/web/rule/ResolutionStrategy.java b/web/src/test/java/book/web/rule/ResolutionStrategy.java new file mode 100644 index 0000000..353ca07 --- /dev/null +++ b/web/src/test/java/book/web/rule/ResolutionStrategy.java @@ -0,0 +1,8 @@ +package book.web.rule; + +import java.io.File; + +public interface ResolutionStrategy { + + File getJavaExecutable(); +} diff --git a/web/src/test/resources/arquillian.xml b/web/src/test/resources/arquillian.xml new file mode 100644 index 0000000..d58ca04 --- /dev/null +++ b/web/src/test/resources/arquillian.xml @@ -0,0 +1,60 @@ + + + + + + + + 8080 + -1 + -1 + plus + target/gamerweb_work + target/gamerwebservice + + + + + 8282 + -1 + -1 + plus + target/commentsservice_work + target/commentsservice + + + + + 8383 + -1 + -1 + plus + target/gameaggregatorservice_work + target/gameaggregatorservice + + com.sun.jersey.server.impl.cdi.lookupExtensionInBeanManager=true + + + + + + + + + ${browser:chrome} + + /home/andy/dev/chromedriver + + + + + + 3 + + + + diff --git a/web/src/test/resources/test-persistence.xml b/web/src/test/resources/test-persistence.xml new file mode 100644 index 0000000..584ef8b --- /dev/null +++ b/web/src/test/resources/test-persistence.xml @@ -0,0 +1,14 @@ + + + + Games Persistence Unit + + false + + + + + + + + diff --git a/web/src/test/resources/test-resources.xml b/web/src/test/resources/test-resources.xml new file mode 100644 index 0000000..a13350e --- /dev/null +++ b/web/src/test/resources/test-resources.xml @@ -0,0 +1,11 @@ + + + + address = ${mongodb.address:-localhost} + port = ${mongodb.port:-27017} + database = ${mongodb.database:-test} + + + \ No newline at end of file diff --git a/web/src/test/resources/test-web.xml b/web/src/test/resources/test-web.xml new file mode 100644 index 0000000..c388cad --- /dev/null +++ b/web/src/test/resources/test-web.xml @@ -0,0 +1,43 @@ + + + + CorsFilter + org.apache.catalina.filters.CorsFilter + true + + cors.allowed.origins + * + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + + Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + false + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* + + + \ No newline at end of file