├── settings.gradle ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── application.properties │ │ ├── db │ │ │ └── migration │ │ │ │ └── V1__initial_schema.sql │ │ └── public │ │ │ ├── js │ │ │ ├── routers │ │ │ │ └── router.js │ │ │ ├── models │ │ │ │ └── todo.js │ │ │ ├── collections │ │ │ │ └── todos.js │ │ │ ├── app.js │ │ │ ├── views │ │ │ │ ├── app-view.js │ │ │ │ └── todo-view.js │ │ │ └── vendor │ │ │ │ ├── todomvc-common.js │ │ │ │ └── underscore.js │ │ │ ├── css │ │ │ ├── chooser.css │ │ │ └── vendor │ │ │ │ └── todomvc-common.css │ │ │ └── index.html │ └── java │ │ └── com │ │ └── atomicjar │ │ └── todos │ │ ├── Application.java │ │ ├── web │ │ ├── TodoNotFoundException.java │ │ └── TodoController.java │ │ ├── repository │ │ └── TodoRepository.java │ │ ├── config │ │ └── WebMvcConfig.java │ │ └── entity │ │ └── Todo.java └── test │ ├── java │ └── com │ │ └── atomicjar │ │ └── todos │ │ ├── TestApplication.java │ │ ├── ContainersConfig.java │ │ ├── ApplicationTests.java │ │ ├── repository │ │ └── TodoRepositoryTest.java │ │ ├── SeleniumE2ETests.java │ │ └── web │ │ └── TodoControllerTests.java │ └── resources │ └── logback-test.xml ├── azure-pipelines.yml ├── .circleci └── config.yml ├── .gitignore ├── .github └── workflows │ ├── maven.yml │ └── gradle.yml ├── LICENSE ├── gradlew.bat ├── pom.xml ├── mvnw.cmd ├── gradlew ├── mvnw └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'testcontainers-java-spring-boot-quickstart' 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testcontainers/testcontainers-java-spring-boot-quickstart/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testcontainers/testcontainers-java-spring-boot-quickstart/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #spring.datasource.url=jdbc:postgresql://localhost:5432/postgres 2 | #spring.datasource.username=postgres 3 | #spring.datasource.password=postgres 4 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__initial_schema.sql: -------------------------------------------------------------------------------- 1 | create table todos 2 | ( 3 | id varchar(100) not null, 4 | title varchar(200) not null, 5 | completed boolean default false, 6 | order_number int, 7 | primary key (id) 8 | ) -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | public class TestApplication { 6 | public static void main(String[] args) { 7 | SpringApplication 8 | .from(Application::main) 9 | .with(ContainersConfig.class) 10 | .run(args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/Application.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/web/TodoNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.web; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class TodoNotFoundException extends RuntimeException { 8 | public TodoNotFoundException(String id) { 9 | super("Todo with id: '" + id + "' not found"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pool: 5 | vmImage: ubuntu-latest 6 | 7 | steps: 8 | - task: Bash@3 9 | inputs: 10 | targetType: "inline" 11 | script: 'sh -c "$(curl -fsSL https://get.testcontainers.cloud/bash)"' 12 | env: 13 | TC_CLOUD_TOKEN: $(TC_CLOUD_TOKEN) 14 | - task: JavaToolInstaller@0 15 | inputs: 16 | versionSpec: '17' 17 | jdkArchitectureOption: 'x64' 18 | jdkSourceOption: 'PreInstalled' 19 | 20 | - script: | 21 | ./mvnw verify 22 | displayName: 'Build with Maven' 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | gradle: circleci/gradle@3.0.0 4 | maven: circleci/maven@1.4.0 5 | tcc: atomicjar/testcontainers-cloud-orb@0.1.0 6 | executors: 7 | jdk17: 8 | docker: 9 | - image: cimg/openjdk:17.0 10 | workflows: 11 | build_and_test: 12 | jobs: 13 | - maven/test: 14 | command: 'verify' 15 | executor: jdk17 16 | pre-steps: 17 | - tcc/setup 18 | - gradle/test: 19 | executor: jdk17 20 | pre-steps: 21 | - tcc/setup -------------------------------------------------------------------------------- /src/main/resources/public/js/routers/router.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | // Todo Router 8 | // ---------- 9 | app.TodoRouter = Backbone.Router.extend({ 10 | routes: { 11 | '*filter': 'setFilter' 12 | }, 13 | 14 | setFilter: function (param) { 15 | // Set the current filter to be used 16 | app.TodoFilter = param || ''; 17 | 18 | // Trigger a collection filter event, causing hiding/unhiding 19 | // of Todo view items 20 | app.todos.trigger('filter'); 21 | } 22 | }); 23 | })(); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | target/ 8 | !.mvn/wrapper/maven-wrapper.jar 9 | !**/src/main/**/target/ 10 | !**/src/test/**/target/ 11 | 12 | ### STS ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### IntelliJ IDEA ### 22 | .idea 23 | *.iws 24 | *.iml 25 | *.ipr 26 | 27 | ### NetBeans ### 28 | /nbproject/private/ 29 | /nbbuild/ 30 | /dist/ 31 | /nbdist/ 32 | /.nb-gradle/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/repository/TodoRepository.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.repository; 2 | 3 | import com.atomicjar.todos.entity.Todo; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.ListCrudRepository; 6 | import org.springframework.data.repository.ListPagingAndSortingRepository; 7 | 8 | import java.util.List; 9 | 10 | public interface TodoRepository extends ListCrudRepository, ListPagingAndSortingRepository { 11 | @Query("select t from Todo t where t.completed = false") 12 | List getPendingTodos(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | @Configuration 8 | public class WebMvcConfig implements WebMvcConfigurer { 9 | 10 | @Override 11 | public void addCorsMappings(CorsRegistry registry) { 12 | registry.addMapping("/**") 13 | .allowedOriginPatterns("*") 14 | .allowedHeaders("*") 15 | .allowedMethods("*"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Maven CI Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | jobs: 8 | build: 9 | name: Maven Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Setup Java 17 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: 17 18 | distribution: 'temurin' 19 | cache: 'maven' 20 | 21 | - name: Setup Testcontainers Cloud Client 22 | uses: atomicjar/testcontainers-cloud-setup-action@main 23 | with: 24 | token: ${{ secrets.TC_CLOUD_TOKEN }} 25 | 26 | - name: Build with Maven 27 | run: ./mvnw verify 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Gradle CI Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | jobs: 8 | build: 9 | name: Gradle Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Setup Java 17 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: 17 18 | distribution: 'temurin' 19 | cache: 'gradle' 20 | 21 | - name: Setup Testcontainers Cloud Client 22 | uses: atomicjar/testcontainers-cloud-setup-action@main 23 | with: 24 | token: ${{ secrets.TC_CLOUD_TOKEN }} 25 | 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | 29 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/ContainersConfig.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos; 2 | 3 | import org.springframework.boot.devtools.restart.RestartScope; 4 | import org.springframework.boot.test.context.TestConfiguration; 5 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 6 | import org.springframework.context.annotation.Bean; 7 | import org.testcontainers.containers.PostgreSQLContainer; 8 | 9 | @TestConfiguration(proxyBeanMethods = false) 10 | public class ContainersConfig { 11 | 12 | @Bean 13 | @ServiceConnection 14 | @RestartScope 15 | PostgreSQLContainer postgreSQLContainer(){ 16 | return new PostgreSQLContainer<>("postgres:15-alpine"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 6 | import org.testcontainers.containers.PostgreSQLContainer; 7 | import org.testcontainers.junit.jupiter.Container; 8 | import org.testcontainers.junit.jupiter.Testcontainers; 9 | 10 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 11 | @Testcontainers 12 | class ApplicationTests { 13 | 14 | @Container 15 | @ServiceConnection 16 | static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 17 | 18 | @Test 19 | void contextLoads() { 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/public/js/models/todo.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | // Todo Model 8 | // ---------- 9 | 10 | // Our basic **Todo** model has `title`, `order`, and `completed` attributes. 11 | app.Todo = Backbone.Model.extend({ 12 | // Default attributes for the todo 13 | // and ensure that each todo created has `title` and `completed` keys. 14 | defaults: { 15 | title: '', 16 | completed: false 17 | }, 18 | 19 | idAttribute: "url", 20 | 21 | url: function() { 22 | if( this.isNew() ){ 23 | return this.collection.url; 24 | }else{ 25 | return this.get('url'); 26 | } 27 | }, 28 | 29 | // Toggle the `completed` state of this todo item. 30 | toggle: function () { 31 | this.save({ 32 | completed: !this.get('completed') 33 | },{patch:true}); 34 | } 35 | }); 36 | })(); 37 | -------------------------------------------------------------------------------- /src/main/resources/public/js/collections/todos.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | 8 | // Todo Collection 9 | // --------------- 10 | 11 | app.Todos = Backbone.Collection.extend({ 12 | // Reference to this collection's model. 13 | model: app.Todo, 14 | 15 | // Filter down the list of all todo items that are finished. 16 | completed: function () { 17 | return this.where({completed: true}); 18 | }, 19 | 20 | // Filter down the list to only todo items that are still not finished. 21 | remaining: function () { 22 | return this.where({completed: false}); 23 | }, 24 | 25 | // We keep the Todos in sequential order, despite being saved by unordered 26 | // GUID in the database. This generates the next order number for new items. 27 | nextOrder: function () { 28 | return this.length ? this.last().get('order') + 1 : 1; 29 | }, 30 | 31 | // Todos are sorted by their original insertion order. 32 | comparator: 'order' 33 | }); 34 | })(); 35 | -------------------------------------------------------------------------------- /src/main/resources/public/js/app.js: -------------------------------------------------------------------------------- 1 | /*global $ */ 2 | /*jshint unused:false */ 3 | var app = app || {}; 4 | var ENTER_KEY = 13; 5 | var ESC_KEY = 27; 6 | 7 | 8 | (function(){ 9 | 10 | function loadApiRootFromInput(){ 11 | var apiRoot = $('#api-root input').val(); 12 | window.location.search = apiRoot; 13 | } 14 | 15 | $('#api-root button').on('click',loadApiRootFromInput); 16 | $('#api-root input').on('keyup',function(){ 17 | if(event.keyCode == ENTER_KEY){ 18 | loadApiRootFromInput(); 19 | } 20 | }); 21 | 22 | })(); 23 | 24 | $(function () { 25 | 'use strict'; 26 | 27 | 28 | var apiRootUrl = window.location.search.substr(1); 29 | if( !apiRootUrl ){ 30 | $("body > *").hide(); 31 | $("#api-root").show(); 32 | return; 33 | } 34 | $("#api-root").hide(); 35 | 36 | $("#target-info .target-url").text(apiRootUrl); 37 | 38 | // Create our global collection of **Todos**. 39 | app.todos = new app.Todos(); 40 | app.todos.url = apiRootUrl; 41 | 42 | app.TodoRouter = new app.TodoRouter(); 43 | Backbone.history.start(); 44 | 45 | // kick things off by creating the `App` 46 | new app.AppView(); 47 | }); 48 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/repository/TodoRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.repository; 2 | 3 | import com.atomicjar.todos.entity.Todo; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @DataJpaTest(properties = { 12 | "spring.test.database.replace=none", 13 | "spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos" 14 | }) 15 | class TodoRepositoryTest { 16 | 17 | @Autowired 18 | TodoRepository repository; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | repository.deleteAll(); 23 | repository.save(new Todo(null, "Todo Item 1", true, 1)); 24 | repository.save(new Todo(null, "Todo Item 2", false, 2)); 25 | repository.save(new Todo(null, "Todo Item 3", false, 3)); 26 | } 27 | 28 | @Test 29 | void shouldGetPendingTodos() { 30 | assertThat(repository.getPendingTodos()).hasSize(2); 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 testcontainers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/entity/Todo.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.entity; 2 | 3 | import org.hibernate.annotations.GenericGenerator; 4 | 5 | import jakarta.persistence.*; 6 | 7 | @Entity 8 | @Table(name = "todos") 9 | public class Todo { 10 | @Id 11 | @GeneratedValue(generator = "uuid") 12 | @GenericGenerator(name = "uuid", strategy = "uuid2") 13 | private String id; 14 | 15 | @Column(name = "title", nullable = false) 16 | private String title; 17 | 18 | @Column(name = "completed") 19 | private Boolean completed = false; 20 | 21 | @Column(name = "order_number") 22 | private Integer order; 23 | 24 | public Todo() { 25 | } 26 | 27 | public Todo(String id, String title, Boolean completed, Integer order) { 28 | this.id = id; 29 | this.title = title; 30 | this.completed = completed; 31 | this.order = order; 32 | } 33 | 34 | public String getId() { 35 | return id; 36 | } 37 | 38 | public void setId(String id) { 39 | this.id = id; 40 | } 41 | 42 | public String getTitle() { 43 | return title; 44 | } 45 | 46 | public void setTitle(String title) { 47 | this.title = title; 48 | } 49 | 50 | public void setCompleted(Boolean completed) { 51 | this.completed = completed; 52 | } 53 | 54 | public Boolean getCompleted() { 55 | return completed; 56 | } 57 | 58 | public Integer getOrder() { 59 | return order; 60 | } 61 | 62 | public void setOrder(Integer order) { 63 | this.order = order; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/public/css/chooser.css: -------------------------------------------------------------------------------- 1 | #api-root { 2 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-weight: 100; 4 | margin: 2em 0; 5 | position: relative; 6 | } 7 | 8 | #api-root .wrapper { 9 | border: 1px black dotted; 10 | padding: 1em 0.5em 1em 2em; 11 | position: relative; 12 | } 13 | 14 | #api-root label { 15 | font-size: 30px; 16 | font-style: italic; 17 | position: absolute; 18 | top: -25px; 19 | left: 10px; 20 | padding: 0 8px; 21 | background: #eaeaea url('../bower_components/todomvc-common/bg.png'); 22 | } 23 | 24 | #api-root input { 25 | font: inherit; 26 | width: 95%; 27 | } 28 | 29 | #api-root button { 30 | border: none; 31 | font: inherit; 32 | font-size: 40px; 33 | font-weight: 400; 34 | margin-top: 0.4em; 35 | margin-right: 0; 36 | 37 | float: right; 38 | 39 | padding: 0.2em 0.6em; 40 | background-color: rgb(48, 173, 48); 41 | color: white; 42 | 43 | -webkit-box-shadow: 2px 2px 1px rgba(50, 50, 50, 0.75); 44 | -moz-box-shadow: 2px 2px 1px rgba(50, 50, 50, 0.75); 45 | box-shadow: 2px 2px 1px rgba(50, 50, 50, 0.75); 46 | } 47 | #api-root button:hover { 48 | background-color: rgb(8, 123, 8); 49 | } 50 | 51 | #target-info { 52 | position: fixed; 53 | bottom: 0; 54 | right: 0; 55 | left: 0; 56 | opacity: 0.6; 57 | 58 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 59 | font-size: 1.2em; 60 | font-weight: 100; 61 | padding: 1em 2em; 62 | 63 | background: rgb(36, 110, 190); 64 | color: rgb(243, 243, 243); 65 | } 66 | 67 | #target-info h2 { 68 | font-size: 1.5em; 69 | margin: 0; 70 | line-height: 1.2; 71 | } 72 | 73 | #target-info a { 74 | font-size: 1.2em; 75 | font-weight: 200; 76 | color: inherit; 77 | margin-top: 0.2em; 78 | display: block; 79 | text-align: right; 80 | } 81 | 82 | #target-info .target-url { 83 | font-style: italic; 84 | font-weight: 200; 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main/java/com/atomicjar/todos/web/TodoController.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.web; 2 | 3 | import com.atomicjar.todos.entity.Todo; 4 | import com.atomicjar.todos.repository.TodoRepository; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import jakarta.validation.Valid; 10 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 11 | 12 | @RestController 13 | @RequestMapping("/todos") 14 | public class TodoController { 15 | private final TodoRepository repository; 16 | 17 | public TodoController(TodoRepository repository) { 18 | this.repository = repository; 19 | } 20 | 21 | @GetMapping 22 | public Iterable getAll() { 23 | return repository.findAll(); 24 | } 25 | 26 | @GetMapping("/{id}") 27 | public ResponseEntity getById(@PathVariable String id) { 28 | return repository.findById(id) 29 | .map(ResponseEntity::ok) 30 | .orElseThrow(() -> new TodoNotFoundException(id)); 31 | } 32 | 33 | @PostMapping 34 | public ResponseEntity save(@Valid @RequestBody Todo todo) { 35 | todo.setId(null); 36 | Todo savedTodo = repository.save(todo); 37 | String uriLocation = ServletUriComponentsBuilder.fromCurrentContextPath().toUriString() + "/todos/" + savedTodo.getId(); 38 | return ResponseEntity 39 | .status(HttpStatus.CREATED) 40 | .header("Location", uriLocation) 41 | .body(savedTodo); 42 | } 43 | 44 | @PatchMapping("/{id}") 45 | public ResponseEntity update(@PathVariable String id, @Valid @RequestBody Todo todo) { 46 | Todo existingTodo = repository.findById(id).orElseThrow(() -> new TodoNotFoundException(id)); 47 | if(todo.getCompleted() != null) { 48 | existingTodo.setCompleted(todo.getCompleted()); 49 | } 50 | if(todo.getOrder() != null) { 51 | existingTodo.setOrder(todo.getOrder()); 52 | } 53 | if(todo.getTitle() != null) { 54 | existingTodo.setTitle(todo.getTitle()); 55 | } 56 | Todo updatedTodo = repository.save(existingTodo); 57 | return ResponseEntity.ok(updatedTodo); 58 | } 59 | 60 | @DeleteMapping("/{id}") 61 | public ResponseEntity deleteById(@PathVariable String id) { 62 | Todo todo = repository.findById(id).orElseThrow(() -> new TodoNotFoundException(id)); 63 | repository.delete(todo); 64 | return ResponseEntity.ok().build(); 65 | } 66 | 67 | @DeleteMapping 68 | public ResponseEntity deleteAll() { 69 | repository.deleteAll(); 70 | return ResponseEntity.ok().build(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo-Backend client 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 |

client connected to:

21 | choose a different server to connect to 22 |
23 | 24 |
25 | 29 |
30 | 31 | 32 |
    33 |
    34 |
    35 |
    36 | 41 | 49 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.3 9 | 10 | 11 | com.atomicjar 12 | testcontainers-java-spring-boot-quickstart 13 | 0.0.1-SNAPSHOT 14 | testcontainers-java-spring-boot-quickstart 15 | Testcontainers Java and Spring Boot QuickStart 16 | 17 | 17 18 | 1.19.0 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-validation 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | org.flywaydb 35 | flyway-core 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-devtools 40 | runtime 41 | true 42 | 43 | 44 | org.postgresql 45 | postgresql 46 | runtime 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-testcontainers 56 | test 57 | 58 | 59 | org.testcontainers 60 | junit-jupiter 61 | test 62 | 63 | 64 | org.testcontainers 65 | postgresql 66 | test 67 | 68 | 69 | org.testcontainers 70 | selenium 71 | test 72 | 73 | 74 | org.seleniumhq.selenium 75 | selenium-remote-driver 76 | test 77 | 78 | 79 | org.seleniumhq.selenium 80 | selenium-chrome-driver 81 | test 82 | 83 | 84 | io.rest-assured 85 | rest-assured 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.springframework.boot 94 | spring-boot-maven-plugin 95 | 96 | 97 | 98 | org.projectlombok 99 | lombok 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/SeleniumE2ETests.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.io.TempDir; 7 | import org.openqa.selenium.By; 8 | import org.openqa.selenium.Keys; 9 | import org.openqa.selenium.WebElement; 10 | import org.openqa.selenium.chrome.ChromeOptions; 11 | import org.openqa.selenium.remote.RemoteWebDriver; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.test.web.server.LocalServerPort; 14 | import org.springframework.test.context.TestPropertySource; 15 | import org.testcontainers.containers.BrowserWebDriverContainer; 16 | import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode; 17 | import org.testcontainers.containers.DefaultRecordingFileFactory; 18 | import org.testcontainers.junit.jupiter.Container; 19 | import org.testcontainers.junit.jupiter.Testcontainers; 20 | import org.testcontainers.lifecycle.TestDescription; 21 | 22 | import java.io.File; 23 | import java.time.Duration; 24 | import java.util.Optional; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 29 | @TestPropertySource(properties = { 30 | "spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos" 31 | }) 32 | @Testcontainers 33 | public class SeleniumE2ETests { 34 | 35 | @LocalServerPort 36 | private Integer localPort; 37 | 38 | @TempDir 39 | static File tempDir; 40 | 41 | @Container 42 | static BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>("selenium/standalone-chrome:4.8.3") 43 | .withAccessToHost(true) 44 | .withRecordingMode(VncRecordingMode.RECORD_ALL, tempDir) 45 | .withRecordingFileFactory(new DefaultRecordingFileFactory()) 46 | .withCapabilities(new ChromeOptions()) 47 | ; 48 | static RemoteWebDriver driver; 49 | 50 | @BeforeAll 51 | static void beforeAll() { 52 | driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions()); 53 | driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); 54 | System.out.println("Selenium remote URL is: " + chrome.getSeleniumAddress()); 55 | System.out.println("VNC URL is: " + chrome.getVncAddress()); 56 | } 57 | 58 | @AfterAll 59 | static void afterAll() { 60 | saveVideoRecording(); 61 | driver.quit(); 62 | } 63 | 64 | @Test 65 | void testAddTodo() { 66 | org.testcontainers.Testcontainers.exposeHostPorts(localPort); 67 | String baseUrl = "http://host.testcontainers.internal:" + localPort; 68 | String apiUrl = baseUrl + "/todos"; 69 | driver.get(baseUrl + "/?" + apiUrl); 70 | 71 | assertThat(driver.getTitle()).isEqualTo("Todo-Backend client"); 72 | 73 | driver.findElement(By.id("new-todo")).sendKeys("first todo" + Keys.RETURN); 74 | WebElement element = driver.findElement(By.xpath("//*[@id=\"todo-list\"]/li[1]/div/label")); 75 | assertThat(element.getText()).isEqualTo("first todo"); 76 | } 77 | 78 | private static void saveVideoRecording() { 79 | chrome.afterTest( 80 | new TestDescription() { 81 | @Override 82 | public String getTestId() { 83 | return getFilesystemFriendlyName(); 84 | } 85 | 86 | @Override 87 | public String getFilesystemFriendlyName() { 88 | return "Todo-E2E-Tests"; 89 | } 90 | }, 91 | Optional.empty() 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/atomicjar/todos/web/TodoControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.atomicjar.todos.web; 2 | 3 | import com.atomicjar.todos.entity.Todo; 4 | import com.atomicjar.todos.repository.TodoRepository; 5 | import io.restassured.RestAssured; 6 | import io.restassured.http.ContentType; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.boot.test.web.server.LocalServerPort; 12 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 13 | import org.testcontainers.containers.PostgreSQLContainer; 14 | import org.testcontainers.junit.jupiter.Container; 15 | import org.testcontainers.junit.jupiter.Testcontainers; 16 | 17 | import java.util.List; 18 | 19 | import static io.restassured.RestAssured.given; 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.hamcrest.CoreMatchers.is; 22 | import static org.hamcrest.Matchers.hasSize; 23 | 24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 25 | @Testcontainers 26 | public class TodoControllerTests { 27 | @LocalServerPort 28 | private Integer port; 29 | 30 | @Container 31 | @ServiceConnection 32 | static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 33 | 34 | @Autowired 35 | TodoRepository todoRepository; 36 | 37 | @BeforeEach 38 | void setUp() { 39 | todoRepository.deleteAll(); 40 | RestAssured.baseURI = "http://localhost:" + port; 41 | } 42 | 43 | @Test 44 | void shouldGetAllTodos() { 45 | List todos = List.of( 46 | new Todo(null, "Todo Item 1", false, 1), 47 | new Todo(null, "Todo Item 2", false, 2) 48 | ); 49 | todoRepository.saveAll(todos); 50 | 51 | given() 52 | .contentType(ContentType.JSON) 53 | .when() 54 | .get("/todos") 55 | .then() 56 | .statusCode(200) 57 | .body(".", hasSize(2)); 58 | } 59 | 60 | @Test 61 | void shouldGetTodoById() { 62 | Todo todo = todoRepository.save(new Todo(null, "Todo Item 1", false, 1)); 63 | 64 | given() 65 | .contentType(ContentType.JSON) 66 | .when() 67 | .get("/todos/{id}", todo.getId()) 68 | .then() 69 | .statusCode(200) 70 | .body("title", is("Todo Item 1")) 71 | .body("completed", is(false)) 72 | .body("order", is(1)); 73 | } 74 | 75 | @Test 76 | void shouldCreateTodoSuccessfully() { 77 | given() 78 | .contentType(ContentType.JSON) 79 | .body( 80 | """ 81 | { 82 | "title": "Todo Item 1", 83 | "completed": false, 84 | "order": 1 85 | } 86 | """ 87 | ) 88 | .when() 89 | .post("/todos") 90 | .then() 91 | .statusCode(201) 92 | .body("title", is("Todo Item 1")) 93 | .body("completed", is(false)) 94 | .body("order", is(1)); 95 | } 96 | 97 | @Test 98 | void shouldDeleteTodoById() { 99 | Todo todo = todoRepository.save(new Todo(null, "Todo Item 1", false, 1)); 100 | 101 | assertThat(todoRepository.findById(todo.getId())).isPresent(); 102 | given() 103 | .contentType(ContentType.JSON) 104 | .when() 105 | .delete("/todos/{id}", todo.getId()) 106 | .then() 107 | .statusCode(200); 108 | 109 | assertThat(todoRepository.findById(todo.getId())).isEmpty(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/resources/public/js/views/app-view.js: -------------------------------------------------------------------------------- 1 | /*global Backbone, jQuery, _, ENTER_KEY */ 2 | var app = app || {}; 3 | 4 | (function ($) { 5 | 'use strict'; 6 | 7 | // The Application 8 | // --------------- 9 | 10 | // Our overall **AppView** is the top-level piece of UI. 11 | app.AppView = Backbone.View.extend({ 12 | 13 | // Instead of generating a new element, bind to the existing skeleton of 14 | // the App already present in the HTML. 15 | el: '#todoapp', 16 | 17 | // Our template for the line of statistics at the bottom of the app. 18 | statsTemplate: _.template($('#stats-template').html()), 19 | 20 | // Delegated events for creating new items, and clearing completed ones. 21 | events: { 22 | 'keypress #new-todo': 'createOnEnter', 23 | 'click #clear-completed': 'clearCompleted', 24 | 'click #toggle-all': 'toggleAllComplete' 25 | }, 26 | 27 | // At initialization we bind to the relevant events on the `Todos` 28 | // collection, when items are added or changed. Kick things off by 29 | // loading any preexisting todos that might be saved in *localStorage*. 30 | initialize: function () { 31 | this.allCheckbox = this.$('#toggle-all')[0]; 32 | this.$input = this.$('#new-todo'); 33 | this.$footer = this.$('#footer'); 34 | this.$main = this.$('#main'); 35 | this.$list = $('#todo-list'); 36 | 37 | this.listenTo(app.todos, 'add', this.addOne); 38 | this.listenTo(app.todos, 'reset', this.addAll); 39 | this.listenTo(app.todos, 'change:completed', this.filterOne); 40 | this.listenTo(app.todos, 'filter', this.filterAll); 41 | this.listenTo(app.todos, 'all', this.render); 42 | 43 | // Suppresses 'add' events with {reset: true} and prevents the app view 44 | // from being re-rendered for every model. Only renders when the 'reset' 45 | // event is triggered at the end of the fetch. 46 | app.todos.fetch({reset: true}); 47 | }, 48 | 49 | // Re-rendering the App just means refreshing the statistics -- the rest 50 | // of the app doesn't change. 51 | render: function () { 52 | var completed = app.todos.completed().length; 53 | var remaining = app.todos.remaining().length; 54 | 55 | if (app.todos.length) { 56 | this.$main.show(); 57 | this.$footer.show(); 58 | 59 | this.$footer.html(this.statsTemplate({ 60 | completed: completed, 61 | remaining: remaining 62 | })); 63 | 64 | this.$('#filters li a') 65 | .removeClass('selected') 66 | .filter('[href="#/' + (app.TodoFilter || '') + '"]') 67 | .addClass('selected'); 68 | } else { 69 | this.$main.hide(); 70 | this.$footer.hide(); 71 | } 72 | 73 | this.allCheckbox.checked = !remaining; 74 | }, 75 | 76 | // Add a single todo item to the list by creating a view for it, and 77 | // appending its element to the `
      `. 78 | addOne: function (todo) { 79 | var view = new app.TodoView({ model: todo }); 80 | this.$list.append(view.render().el); 81 | }, 82 | 83 | // Add all items in the **Todos** collection at once. 84 | addAll: function () { 85 | this.$list.html(''); 86 | app.todos.each(this.addOne, this); 87 | }, 88 | 89 | filterOne: function (todo) { 90 | todo.trigger('visible'); 91 | }, 92 | 93 | filterAll: function () { 94 | app.todos.each(this.filterOne, this); 95 | }, 96 | 97 | // Generate the attributes for a new Todo item. 98 | newAttributes: function () { 99 | return { 100 | title: this.$input.val().trim(), 101 | order: app.todos.nextOrder(), 102 | completed: false 103 | }; 104 | }, 105 | 106 | // If you hit return in the main input field, create new **Todo** model, 107 | // persisting it to *localStorage*. 108 | createOnEnter: function (e) { 109 | if (e.which === ENTER_KEY && this.$input.val().trim()) { 110 | app.todos.create(this.newAttributes()); 111 | this.$input.val(''); 112 | } 113 | }, 114 | 115 | // Clear all completed todo items, destroying their models. 116 | clearCompleted: function () { 117 | _.invoke(app.todos.completed(), 'destroy'); 118 | return false; 119 | }, 120 | 121 | toggleAllComplete: function () { 122 | var completed = this.allCheckbox.checked; 123 | 124 | app.todos.each(function (todo) { 125 | todo.save({ 126 | completed: completed 127 | }); 128 | }); 129 | } 130 | }); 131 | })(jQuery); 132 | -------------------------------------------------------------------------------- /src/main/resources/public/js/views/todo-view.js: -------------------------------------------------------------------------------- 1 | /*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */ 2 | var app = app || {}; 3 | 4 | (function ($) { 5 | 'use strict'; 6 | 7 | // Todo Item View 8 | // -------------- 9 | 10 | // The DOM element for a todo item... 11 | app.TodoView = Backbone.View.extend({ 12 | //... is a list tag. 13 | tagName: 'li', 14 | 15 | // Cache the template function for a single item. 16 | template: _.template($('#item-template').html()), 17 | 18 | // The DOM events specific to an item. 19 | events: { 20 | 'click .toggle': 'toggleCompleted', 21 | 'dblclick label': 'edit', 22 | 'click .destroy': 'clear', 23 | 'keypress .edit': 'updateOnEnter', 24 | 'keydown .edit': 'revertOnEscape', 25 | 'blur .edit': 'close' 26 | }, 27 | 28 | // The TodoView listens for changes to its model, re-rendering. Since 29 | // there's a one-to-one correspondence between a **Todo** and a 30 | // **TodoView** in this app, we set a direct reference on the model for 31 | // convenience. 32 | initialize: function () { 33 | this.listenTo(this.model, 'change', this.render); 34 | this.listenTo(this.model, 'destroy', this.remove); 35 | this.listenTo(this.model, 'visible', this.toggleVisible); 36 | }, 37 | 38 | // Re-render the titles of the todo item. 39 | render: function () { 40 | // Backbone LocalStorage is adding `id` attribute instantly after 41 | // creating a model. This causes our TodoView to render twice. Once 42 | // after creating a model and once on `id` change. We want to 43 | // filter out the second redundant render, which is caused by this 44 | // `id` change. It's known Backbone LocalStorage bug, therefore 45 | // we've to create a workaround. 46 | // https://github.com/tastejs/todomvc/issues/469 47 | if (this.model.changed.id !== undefined) { 48 | return; 49 | } 50 | 51 | this.$el.html(this.template(this.model.toJSON())); 52 | this.$el.toggleClass('completed', this.model.get('completed')); 53 | this.toggleVisible(); 54 | this.$input = this.$('.edit'); 55 | return this; 56 | }, 57 | 58 | toggleVisible: function () { 59 | this.$el.toggleClass('hidden', this.isHidden()); 60 | }, 61 | 62 | isHidden: function () { 63 | var isCompleted = this.model.get('completed'); 64 | return (// hidden cases only 65 | (!isCompleted && app.TodoFilter === 'completed') || 66 | (isCompleted && app.TodoFilter === 'active') 67 | ); 68 | }, 69 | 70 | // Toggle the `"completed"` state of the model. 71 | toggleCompleted: function () { 72 | this.model.toggle(); 73 | }, 74 | 75 | // Switch this view into `"editing"` mode, displaying the input field. 76 | edit: function () { 77 | this.$el.addClass('editing'); 78 | this.$input.focus(); 79 | }, 80 | 81 | // Close the `"editing"` mode, saving changes to the todo. 82 | close: function () { 83 | var value = this.$input.val(); 84 | var trimmedValue = value.trim(); 85 | 86 | // We don't want to handle blur events from an item that is no 87 | // longer being edited. Relying on the CSS class here has the 88 | // benefit of us not having to maintain state in the DOM and the 89 | // JavaScript logic. 90 | if (!this.$el.hasClass('editing')) { 91 | return; 92 | } 93 | 94 | if (trimmedValue) { 95 | this.model.save({ title: trimmedValue },{patch:true}); 96 | 97 | if (value !== trimmedValue) { 98 | // Model values changes consisting of whitespaces only are 99 | // not causing change to be triggered Therefore we've to 100 | // compare untrimmed version with a trimmed one to check 101 | // whether anything changed 102 | // And if yes, we've to trigger change event ourselves 103 | this.model.trigger('change'); 104 | } 105 | } else { 106 | this.clear(); 107 | } 108 | 109 | this.$el.removeClass('editing'); 110 | }, 111 | 112 | // If you hit `enter`, we're through editing the item. 113 | updateOnEnter: function (e) { 114 | if (e.which === ENTER_KEY) { 115 | this.close(); 116 | } 117 | }, 118 | 119 | // If you're pressing `escape` we revert your change by simply leaving 120 | // the `editing` state. 121 | revertOnEscape: function (e) { 122 | if (e.which === ESC_KEY) { 123 | this.$el.removeClass('editing'); 124 | // Also reset the hidden input back to the original value. 125 | this.$input.val(this.model.get('title')); 126 | } 127 | }, 128 | 129 | // Remove the item, destroy the model from *localStorage* and delete its view. 130 | clear: function () { 131 | this.model.destroy(); 132 | } 133 | }); 134 | })(jQuery); 135 | -------------------------------------------------------------------------------- /src/main/resources/public/js/vendor/todomvc-common.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // Underscore's Template Module 5 | // Courtesy of underscorejs.org 6 | var _ = (function (_) { 7 | _.defaults = function (object) { 8 | if (!object) { 9 | return object; 10 | } 11 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { 12 | var iterable = arguments[argsIndex]; 13 | if (iterable) { 14 | for (var key in iterable) { 15 | if (object[key] == null) { 16 | object[key] = iterable[key]; 17 | } 18 | } 19 | } 20 | } 21 | return object; 22 | } 23 | 24 | // By default, Underscore uses ERB-style template delimiters, change the 25 | // following template settings to use alternative delimiters. 26 | _.templateSettings = { 27 | evaluate : /<%([\s\S]+?)%>/g, 28 | interpolate : /<%=([\s\S]+?)%>/g, 29 | escape : /<%-([\s\S]+?)%>/g 30 | }; 31 | 32 | // When customizing `templateSettings`, if you don't want to define an 33 | // interpolation, evaluation or escaping regex, we need one that is 34 | // guaranteed not to match. 35 | var noMatch = /(.)^/; 36 | 37 | // Certain characters need to be escaped so that they can be put into a 38 | // string literal. 39 | var escapes = { 40 | "'": "'", 41 | '\\': '\\', 42 | '\r': 'r', 43 | '\n': 'n', 44 | '\t': 't', 45 | '\u2028': 'u2028', 46 | '\u2029': 'u2029' 47 | }; 48 | 49 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 50 | 51 | // JavaScript micro-templating, similar to John Resig's implementation. 52 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 53 | // and correctly escapes quotes within interpolated code. 54 | _.template = function(text, data, settings) { 55 | var render; 56 | settings = _.defaults({}, settings, _.templateSettings); 57 | 58 | // Combine delimiters into one regular expression via alternation. 59 | var matcher = new RegExp([ 60 | (settings.escape || noMatch).source, 61 | (settings.interpolate || noMatch).source, 62 | (settings.evaluate || noMatch).source 63 | ].join('|') + '|$', 'g'); 64 | 65 | // Compile the template source, escaping string literals appropriately. 66 | var index = 0; 67 | var source = "__p+='"; 68 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 69 | source += text.slice(index, offset) 70 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 71 | 72 | if (escape) { 73 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 74 | } 75 | if (interpolate) { 76 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 77 | } 78 | if (evaluate) { 79 | source += "';\n" + evaluate + "\n__p+='"; 80 | } 81 | index = offset + match.length; 82 | return match; 83 | }); 84 | source += "';\n"; 85 | 86 | // If a variable is not specified, place data values in local scope. 87 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 88 | 89 | source = "var __t,__p='',__j=Array.prototype.join," + 90 | "print=function(){__p+=__j.call(arguments,'');};\n" + 91 | source + "return __p;\n"; 92 | 93 | try { 94 | render = new Function(settings.variable || 'obj', '_', source); 95 | } catch (e) { 96 | e.source = source; 97 | throw e; 98 | } 99 | 100 | if (data) return render(data, _); 101 | var template = function(data) { 102 | return render.call(this, data, _); 103 | }; 104 | 105 | // Provide the compiled function source as a convenience for precompilation. 106 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 107 | 108 | return template; 109 | }; 110 | 111 | return _; 112 | })({}); 113 | 114 | if (location.hostname === 'todomvc.com') { 115 | window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); 116 | } 117 | 118 | function redirect() { 119 | if (location.hostname === 'tastejs.github.io') { 120 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 121 | } 122 | } 123 | 124 | function findRoot() { 125 | var base = location.href.indexOf('examples/'); 126 | return location.href.substr(0, base); 127 | } 128 | 129 | function getFile(file, callback) { 130 | if (!location.host) { 131 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); 132 | } 133 | 134 | var xhr = new XMLHttpRequest(); 135 | 136 | xhr.open('GET', findRoot() + file, true); 137 | xhr.send(); 138 | 139 | xhr.onload = function () { 140 | if (xhr.status === 200 && callback) { 141 | callback(xhr.responseText); 142 | } 143 | }; 144 | } 145 | 146 | function Learn(learnJSON, config) { 147 | if (!(this instanceof Learn)) { 148 | return new Learn(learnJSON, config); 149 | } 150 | 151 | var template, framework; 152 | 153 | if (typeof learnJSON !== 'object') { 154 | try { 155 | learnJSON = JSON.parse(learnJSON); 156 | } catch (e) { 157 | return; 158 | } 159 | } 160 | 161 | if (config) { 162 | template = config.template; 163 | framework = config.framework; 164 | } 165 | 166 | if (!template && learnJSON.templates) { 167 | template = learnJSON.templates.todomvc; 168 | } 169 | 170 | if (!framework && document.querySelector('[data-framework]')) { 171 | framework = document.querySelector('[data-framework]').dataset.framework; 172 | } 173 | 174 | 175 | if (template && learnJSON[framework]) { 176 | this.frameworkJSON = learnJSON[framework]; 177 | this.template = template; 178 | 179 | this.append(); 180 | } 181 | } 182 | 183 | Learn.prototype.append = function () { 184 | var aside = document.createElement('aside'); 185 | aside.innerHTML = _.template(this.template, this.frameworkJSON); 186 | aside.className = 'learn'; 187 | 188 | // Localize demo links 189 | var demoLinks = aside.querySelectorAll('.demo-link'); 190 | Array.prototype.forEach.call(demoLinks, function (demoLink) { 191 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); 192 | }); 193 | 194 | document.body.className = (document.body.className + ' learn-bar').trim(); 195 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); 196 | }; 197 | 198 | redirect(); 199 | getFile('learn.json', Learn); 200 | })(); 201 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /src/main/resources/public/css/vendor/todomvc-common.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('bg.png'); 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | #todoapp { 42 | background: #fff; 43 | background: rgba(255, 255, 255, 0.9); 44 | margin: 130px 0 40px 0; 45 | border: 1px solid #ccc; 46 | position: relative; 47 | border-top-left-radius: 2px; 48 | border-top-right-radius: 2px; 49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 50 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 51 | } 52 | 53 | #todoapp:before { 54 | content: ''; 55 | border-left: 1px solid #f5d6d6; 56 | border-right: 1px solid #f5d6d6; 57 | width: 2px; 58 | position: absolute; 59 | top: 0; 60 | left: 40px; 61 | height: 100%; 62 | } 63 | 64 | #todoapp input::-webkit-input-placeholder { 65 | font-style: italic; 66 | } 67 | 68 | #todoapp input::-moz-placeholder { 69 | font-style: italic; 70 | color: #a9a9a9; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -120px; 76 | width: 100%; 77 | font-size: 70px; 78 | font-weight: bold; 79 | text-align: center; 80 | color: #b3b3b3; 81 | color: rgba(255, 255, 255, 0.3); 82 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 83 | -webkit-text-rendering: optimizeLegibility; 84 | -moz-text-rendering: optimizeLegibility; 85 | -ms-text-rendering: optimizeLegibility; 86 | -o-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | #header { 91 | padding-top: 15px; 92 | border-radius: inherit; 93 | } 94 | 95 | #header:before { 96 | content: ''; 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | left: 0; 101 | height: 15px; 102 | z-index: 2; 103 | border-bottom: 1px solid #6c615c; 104 | background: #8d7d77; 105 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 106 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 107 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 108 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 109 | border-top-left-radius: 1px; 110 | border-top-right-radius: 1px; 111 | } 112 | 113 | #new-todo, 114 | .edit { 115 | position: relative; 116 | margin: 0; 117 | width: 100%; 118 | font-size: 24px; 119 | font-family: inherit; 120 | line-height: 1.4em; 121 | border: 0; 122 | outline: none; 123 | color: inherit; 124 | padding: 6px; 125 | border: 1px solid #999; 126 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 127 | -moz-box-sizing: border-box; 128 | -ms-box-sizing: border-box; 129 | -o-box-sizing: border-box; 130 | box-sizing: border-box; 131 | -webkit-font-smoothing: antialiased; 132 | -moz-font-smoothing: antialiased; 133 | -ms-font-smoothing: antialiased; 134 | -o-font-smoothing: antialiased; 135 | font-smoothing: antialiased; 136 | } 137 | 138 | #new-todo { 139 | padding: 16px 16px 16px 60px; 140 | border: none; 141 | background: rgba(0, 0, 0, 0.02); 142 | z-index: 2; 143 | box-shadow: none; 144 | } 145 | 146 | #main { 147 | position: relative; 148 | z-index: 2; 149 | border-top: 1px dotted #adadad; 150 | } 151 | 152 | label[for='toggle-all'] { 153 | display: none; 154 | } 155 | 156 | #toggle-all { 157 | position: absolute; 158 | top: -42px; 159 | left: -4px; 160 | width: 40px; 161 | text-align: center; 162 | /* Mobile Safari */ 163 | border: none; 164 | } 165 | 166 | #toggle-all:before { 167 | content: '»'; 168 | font-size: 28px; 169 | color: #d9d9d9; 170 | padding: 0 25px 7px; 171 | } 172 | 173 | #toggle-all:checked:before { 174 | color: #737373; 175 | } 176 | 177 | #todo-list { 178 | margin: 0; 179 | padding: 0; 180 | list-style: none; 181 | } 182 | 183 | #todo-list li { 184 | position: relative; 185 | font-size: 24px; 186 | border-bottom: 1px dotted #ccc; 187 | } 188 | 189 | #todo-list li:last-child { 190 | border-bottom: none; 191 | } 192 | 193 | #todo-list li.editing { 194 | border-bottom: none; 195 | padding: 0; 196 | } 197 | 198 | #todo-list li.editing .edit { 199 | display: block; 200 | width: 506px; 201 | padding: 13px 17px 12px 17px; 202 | margin: 0 0 0 43px; 203 | } 204 | 205 | #todo-list li.editing .view { 206 | display: none; 207 | } 208 | 209 | #todo-list li .toggle { 210 | text-align: center; 211 | width: 40px; 212 | /* auto, since non-WebKit browsers doesn't support input styling */ 213 | height: auto; 214 | position: absolute; 215 | top: 0; 216 | bottom: 0; 217 | margin: auto 0; 218 | /* Mobile Safari */ 219 | border: none; 220 | -webkit-appearance: none; 221 | -ms-appearance: none; 222 | -o-appearance: none; 223 | appearance: none; 224 | } 225 | 226 | #todo-list li .toggle:after { 227 | content: '✔'; 228 | /* 40 + a couple of pixels visual adjustment */ 229 | line-height: 43px; 230 | font-size: 20px; 231 | color: #d9d9d9; 232 | text-shadow: 0 -1px 0 #bfbfbf; 233 | } 234 | 235 | #todo-list li .toggle:checked:after { 236 | color: #85ada7; 237 | text-shadow: 0 1px 0 #669991; 238 | bottom: 1px; 239 | position: relative; 240 | } 241 | 242 | #todo-list li label { 243 | white-space: pre; 244 | word-break: break-word; 245 | padding: 15px 60px 15px 15px; 246 | margin-left: 45px; 247 | display: block; 248 | line-height: 1.2; 249 | -webkit-transition: color 0.4s; 250 | transition: color 0.4s; 251 | } 252 | 253 | #todo-list li.completed label { 254 | color: #a9a9a9; 255 | text-decoration: line-through; 256 | } 257 | 258 | #todo-list li .destroy { 259 | display: none; 260 | position: absolute; 261 | top: 0; 262 | right: 10px; 263 | bottom: 0; 264 | width: 40px; 265 | height: 40px; 266 | margin: auto 0; 267 | font-size: 22px; 268 | color: #a88a8a; 269 | -webkit-transition: all 0.2s; 270 | transition: all 0.2s; 271 | } 272 | 273 | #todo-list li .destroy:hover { 274 | text-shadow: 0 0 1px #000, 275 | 0 0 10px rgba(199, 107, 107, 0.8); 276 | -webkit-transform: scale(1.3); 277 | transform: scale(1.3); 278 | } 279 | 280 | #todo-list li .destroy:after { 281 | content: '✖'; 282 | } 283 | 284 | #todo-list li:hover .destroy { 285 | display: block; 286 | } 287 | 288 | #todo-list li .edit { 289 | display: none; 290 | } 291 | 292 | #todo-list li.editing:last-child { 293 | margin-bottom: -1px; 294 | } 295 | 296 | #footer { 297 | color: #777; 298 | padding: 0 15px; 299 | position: absolute; 300 | right: 0; 301 | bottom: -31px; 302 | left: 0; 303 | height: 20px; 304 | z-index: 1; 305 | text-align: center; 306 | } 307 | 308 | #footer:before { 309 | content: ''; 310 | position: absolute; 311 | right: 0; 312 | bottom: 31px; 313 | left: 0; 314 | height: 50px; 315 | z-index: -1; 316 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 317 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 318 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 319 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 320 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 321 | } 322 | 323 | #todo-count { 324 | float: left; 325 | text-align: left; 326 | } 327 | 328 | #filters { 329 | margin: 0; 330 | padding: 0; 331 | list-style: none; 332 | position: absolute; 333 | right: 0; 334 | left: 0; 335 | } 336 | 337 | #filters li { 338 | display: inline; 339 | } 340 | 341 | #filters li a { 342 | color: #83756f; 343 | margin: 2px; 344 | text-decoration: none; 345 | } 346 | 347 | #filters li a.selected { 348 | font-weight: bold; 349 | } 350 | 351 | #clear-completed { 352 | float: right; 353 | position: relative; 354 | line-height: 20px; 355 | text-decoration: none; 356 | background: rgba(0, 0, 0, 0.1); 357 | font-size: 11px; 358 | padding: 0 10px; 359 | border-radius: 3px; 360 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 361 | } 362 | 363 | #clear-completed:hover { 364 | background: rgba(0, 0, 0, 0.15); 365 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 366 | } 367 | 368 | #info { 369 | margin: 65px auto 0; 370 | color: #a6a6a6; 371 | font-size: 12px; 372 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 373 | text-align: center; 374 | } 375 | 376 | #info a { 377 | color: inherit; 378 | } 379 | 380 | /* 381 | Hack to remove background from Mobile Safari. 382 | Can't use it globally since it destroys checkboxes in Firefox and Opera 383 | */ 384 | 385 | @media screen and (-webkit-min-device-pixel-ratio:0) { 386 | #toggle-all, 387 | #todo-list li .toggle { 388 | background: none; 389 | } 390 | 391 | #todo-list li .toggle { 392 | height: 40px; 393 | } 394 | 395 | #toggle-all { 396 | top: -56px; 397 | left: -15px; 398 | width: 65px; 399 | height: 41px; 400 | -webkit-transform: rotate(90deg); 401 | transform: rotate(90deg); 402 | -webkit-appearance: none; 403 | appearance: none; 404 | } 405 | } 406 | 407 | .hidden { 408 | display: none; 409 | } 410 | 411 | hr { 412 | margin: 20px 0; 413 | border: 0; 414 | border-top: 1px dashed #C5C5C5; 415 | border-bottom: 1px dashed #F7F7F7; 416 | } 417 | 418 | .learn a { 419 | font-weight: normal; 420 | text-decoration: none; 421 | color: #b83f45; 422 | } 423 | 424 | .learn a:hover { 425 | text-decoration: underline; 426 | color: #787e7e; 427 | } 428 | 429 | .learn h3, 430 | .learn h4, 431 | .learn h5 { 432 | margin: 10px 0; 433 | font-weight: 500; 434 | line-height: 1.2; 435 | color: #000; 436 | } 437 | 438 | .learn h3 { 439 | font-size: 24px; 440 | } 441 | 442 | .learn h4 { 443 | font-size: 18px; 444 | } 445 | 446 | .learn h5 { 447 | margin-bottom: 0; 448 | font-size: 14px; 449 | } 450 | 451 | .learn ul { 452 | padding: 0; 453 | margin: 0 0 30px 25px; 454 | } 455 | 456 | .learn li { 457 | line-height: 20px; 458 | } 459 | 460 | .learn p { 461 | font-size: 15px; 462 | font-weight: 300; 463 | line-height: 1.3; 464 | margin-top: 0; 465 | margin-bottom: 0; 466 | } 467 | 468 | .quote { 469 | border: none; 470 | margin: 20px 0 60px 0; 471 | } 472 | 473 | .quote p { 474 | font-style: italic; 475 | } 476 | 477 | .quote p:before { 478 | content: '“'; 479 | font-size: 50px; 480 | opacity: .15; 481 | position: absolute; 482 | top: -20px; 483 | left: 3px; 484 | } 485 | 486 | .quote p:after { 487 | content: '”'; 488 | font-size: 50px; 489 | opacity: .15; 490 | position: absolute; 491 | bottom: -42px; 492 | right: 3px; 493 | } 494 | 495 | .quote footer { 496 | position: absolute; 497 | bottom: -40px; 498 | right: 0; 499 | } 500 | 501 | .quote footer img { 502 | border-radius: 3px; 503 | } 504 | 505 | .quote footer a { 506 | margin-left: 5px; 507 | vertical-align: middle; 508 | } 509 | 510 | .speech-bubble { 511 | position: relative; 512 | padding: 10px; 513 | background: rgba(0, 0, 0, .04); 514 | border-radius: 5px; 515 | } 516 | 517 | .speech-bubble:after { 518 | content: ''; 519 | position: absolute; 520 | top: 100%; 521 | right: 30px; 522 | border: 13px solid transparent; 523 | border-top-color: rgba(0, 0, 0, .04); 524 | } 525 | 526 | .learn-bar > .learn { 527 | position: absolute; 528 | width: 272px; 529 | top: 8px; 530 | left: -300px; 531 | padding: 10px; 532 | border-radius: 5px; 533 | background-color: rgba(255, 255, 255, .6); 534 | -webkit-transition-property: left; 535 | transition-property: left; 536 | -webkit-transition-duration: 500ms; 537 | transition-duration: 500ms; 538 | } 539 | 540 | @media (min-width: 899px) { 541 | .learn-bar { 542 | width: auto; 543 | margin: 0 0 0 300px; 544 | } 545 | 546 | .learn-bar > .learn { 547 | left: 8px; 548 | } 549 | 550 | .learn-bar #todoapp { 551 | width: 550px; 552 | margin: 130px auto 40px auto; 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testcontainers SpringBoot Quickstart 2 | This quick starter will guide you to configure and use Testcontainers in a SpringBoot project. 3 | 4 | In this guide, we'll look at a sample Spring Boot application that uses Testcontainers for running unit tests with real dependencies. 5 | The initial implementation uses a relational database for storing data. 6 | We'll look at the necessary parts of the code that integrates Testcontainers into the app. 7 | Then we'll switch the relation database for MongoDB, and guide you through using Testcontainers for testing the app against a real instance of MongoDB running in a container. 8 | 9 | After the quick start, you'll have a working Spring Boot app with Testcontainers-based tests, and will be ready to explore integrations with other databases and other technologies via Testcontainers. 10 | 11 | ## 1. Setup Environment 12 | Make sure you have Java 8+ and a [compatible Docker environment](https://www.testcontainers.org/supported_docker_environment/) installed. 13 | If you are going to use Maven build tool then make sure Java 17+ is installed. 14 | 15 | For example: 16 | ```shell 17 | $ java -version 18 | openjdk version "17.0.4" 2022-07-19 19 | OpenJDK Runtime Environment Temurin-17.0.4+8 (build 17.0.4+8) 20 | OpenJDK 64-Bit Server VM Temurin-17.0.4+8 (build 17.0.4+8, mixed mode, sharing) 21 | 22 | $ docker version 23 | ... 24 | Server: Docker Desktop 4.12.0 (85629) 25 | Engine: 26 | Version: 20.10.17 27 | API version: 1.41 (minimum version 1.12) 28 | Go version: go1.17.11 29 | ... 30 | ``` 31 | ## 2. Setup Project 32 | * Clone the repository `git clone https://github.com/testcontainers/testcontainers-java-spring-boot-quickstart.git && cd testcontainers-java-spring-boot-quickstart` 33 | * Open the **testcontainers-java-spring-boot-quickstart** project in your favorite IDE. 34 | 35 | ## 3. Run Tests 36 | The sample project uses JUnit tests and Testcontainers to run them against actual databases running in containers. 37 | 38 | Run the command to run the tests. 39 | ```shell 40 | $ ./gradlew test //for Gradle 41 | $ ./mvnw verify //for Maven 42 | ``` 43 | 44 | The tests should pass. 45 | 46 | ## 4. Let's explore the code 47 | The **testcontainers-java-spring-boot-quickstart** project is a SpringBoot REST API using Java 17, Spring Data JPA, PostgreSQL, and Gradle/Maven. 48 | We are using [JUnit 5](https://junit.org/junit5/), [Testcontainers](https://testcontainers.org) and [RestAssured](https://rest-assured.io/) for testing. 49 | 50 | ### 4.1. Test Dependencies 51 | Following are the Testcontainers and RestAssured dependencies: 52 | 53 | **build.gradle** 54 | ```groovy 55 | ext { 56 | set('testcontainersVersion', "1.19.0") 57 | } 58 | 59 | dependencies { 60 | ... 61 | ... 62 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 63 | testImplementation 'org.springframework.boot:spring-boot-testcontainers' 64 | testImplementation 'org.testcontainers:junit-jupiter' 65 | testImplementation 'org.testcontainers:postgresql' 66 | testImplementation 'io.rest-assured:rest-assured' 67 | } 68 | 69 | dependencyManagement { 70 | imports { 71 | mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}" 72 | } 73 | } 74 | ``` 75 | 76 | For Maven build the Testcontainers and RestAssured dependencies are configured in **pom.xml** as follows: 77 | 78 | ```xml 79 | 83 | 84 | ... 85 | ... 86 | 1.19.0 87 | 88 | 89 | ... 90 | ... 91 | 92 | org.springframework.boot 93 | spring-boot-starter-test 94 | test 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-testcontainers 99 | test 100 | 101 | 102 | org.testcontainers 103 | junit-jupiter 104 | test 105 | 106 | 107 | org.testcontainers 108 | postgresql 109 | test 110 | 111 | 112 | io.rest-assured 113 | rest-assured 114 | test 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.testcontainers 122 | testcontainers-bom 123 | ${testcontainers.version} 124 | pom 125 | import 126 | 127 | 128 | 129 | 130 | ``` 131 | 132 | ### 4.2. How to use Testcontainers? 133 | Testcontainers library can be used to spin up desired services as docker containers and run tests against those services. 134 | We can use our testing library lifecycle hooks to start/stop containers using Testcontainers API. 135 | 136 | ```java 137 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 138 | public class TodoControllerTests { 139 | @LocalServerPort 140 | private Integer port; 141 | 142 | static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 143 | 144 | @BeforeAll 145 | static void beforeAll() { 146 | postgres.start(); 147 | } 148 | 149 | @AfterAll 150 | static void afterAll() { 151 | postgres.stop(); 152 | } 153 | 154 | @DynamicPropertySource 155 | static void configureProperties(DynamicPropertyRegistry registry) { 156 | registry.add("spring.datasource.url", postgres::getJdbcUrl); 157 | registry.add("spring.datasource.username", postgres::getUsername); 158 | registry.add("spring.datasource.password", postgres::getPassword); 159 | } 160 | 161 | @Autowired 162 | TodoRepository todoRepository; 163 | 164 | @BeforeEach 165 | void setUp() { 166 | todoRepository.deleteAll(); 167 | RestAssured.baseURI = "http://localhost:" + port; 168 | } 169 | 170 | @Test 171 | void shouldGetAllTodos() { 172 | List todos = List.of( 173 | new Todo(null, "Todo Item 1", false, 1), 174 | new Todo(null, "Todo Item 2", false, 2) 175 | ); 176 | todoRepository.saveAll(todos); 177 | 178 | given() 179 | .contentType(ContentType.JSON) 180 | .when() 181 | .get("/todos") 182 | .then() 183 | .statusCode(200) 184 | .body(".", hasSize(2)); 185 | } 186 | } 187 | ``` 188 | 189 | Here we have defined a `PostgreSQLContainer` instance, started the container before executing tests and stopped it after executing all the tests using JUnit 5 test lifecycle hook methods. 190 | 191 | > **Note** 192 | > 193 | > If you are using any different Testing library like TestNG or Spock then you can use similar lifecycle callback methods provided by that testing library. 194 | 195 | The Postgresql container port (5432) will be mapped to a random available port on the host. 196 | This helps to avoid port conflicts and allows running tests in parallel. 197 | Then we are using SpringBoot's dynamic property registration support to add/override the `datasource` properties obtained from the Postgres container. 198 | 199 | ```java 200 | @DynamicPropertySource 201 | static void configureProperties(DynamicPropertyRegistry registry) { 202 | registry.add("spring.datasource.url", postgres::getJdbcUrl); 203 | registry.add("spring.datasource.username", postgres::getUsername); 204 | registry.add("spring.datasource.password", postgres::getPassword); 205 | } 206 | ``` 207 | 208 | In `shouldGetAllTodos()` test we are saving two Todo entities into the database using `TodoRepository` and testing `GET /todos` API endpoint to fetch todos using RestAssured. 209 | 210 | You can run the tests directly from IDE or using the command `./gradlew test` from the terminal. 211 | 212 | ### 4.3. Using Testcontainers JUnit 5 Extension 213 | Instead of implementing JUnit 5 lifecycle callback methods to start and stop the Postgres container, 214 | we can use [Testcontainers JUnit 5 Extension annotations](https://www.testcontainers.org/quickstart/junit_5_quickstart/) to manage the container lifecycle as follows: 215 | 216 | ```java 217 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 218 | @Testcontainers 219 | public class TodoControllerTests { 220 | @Container 221 | static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 222 | 223 | @DynamicPropertySource 224 | static void configureProperties(DynamicPropertyRegistry registry) { 225 | registry.add("spring.datasource.url", postgres::getJdbcUrl); 226 | registry.add("spring.datasource.username", postgres::getUsername); 227 | registry.add("spring.datasource.password", postgres::getPassword); 228 | } 229 | } 230 | ``` 231 | 232 | > **Note** 233 | > 234 | > The Testcontainers JUnit 5 Extension will take care of starting the container before tests and stopping it after tests. 235 | If the container is a `static` field then it will be started once before all the tests and stopped after all the tests. 236 | If it is a non-static field then the container will be started before each test and stopped after each test. 237 | > 238 | > Even if you don't stop the containers explicitly, Testcontainers will take care of removing the containers, using `ryuk` container behind the scenes, once all the tests are done. 239 | > But it is recommended to clean up the containers as soon as possible. 240 | 241 | 242 | ### 4.5. Using magical Testcontainers JDBC URL 243 | Testcontainers provides the [**special jdbc url** support](https://www.testcontainers.org/modules/databases/jdbc/) which automatically spins up the configured database as a container. 244 | 245 | ```java 246 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 247 | @TestPropertySource(properties = { 248 | "spring.datasource.url=jdbc:tc:postgresql:15-alpine:///todos" 249 | }) 250 | class ApplicationTests { 251 | 252 | @Test 253 | void contextLoads() { 254 | } 255 | } 256 | ``` 257 | 258 | By setting the datasource url to `jdbc:tc:postgresql:15-alpine:///todos` (notice the special `:tc` prefix), 259 | Testcontainers automatically spin up the Postgres database using `postgresql:15-alpine` docker image. 260 | 261 | For more information on Testcontainers JDBC Support refer https://www.testcontainers.org/modules/databases/jdbc/ 262 | 263 | ### 4.6. Using Spring Boot 3.1.0 @ServiceConnection 264 | Spring Boot 3.1.0 introduced better support for Testcontainers that simplifies test configuration greatly. 265 | Instead of registering the postgres database connection properties using `@DynamicPropertySource`, 266 | we can use `@ServiceConnection` to register the Database connection as follows: 267 | 268 | ```java 269 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 270 | @Testcontainers 271 | public class TodoControllerTests { 272 | @Container 273 | @ServiceConnection 274 | static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); 275 | 276 | @Test 277 | void test() { 278 | ... 279 | } 280 | } 281 | ``` 282 | 283 | 284 | ## 5. Local Development using Testcontainers 285 | Spring Boot 3.1.0 introduced support for using Testcontainers at development time. 286 | You can configure your Spring Boot application to automatically start the required docker containers. 287 | 288 | First, create a configuration class to define the required containers as follows: 289 | 290 | ```java 291 | @TestConfiguration(proxyBeanMethods = false) 292 | public class ContainersConfig { 293 | 294 | @Bean 295 | @ServiceConnection 296 | @RestartScope 297 | PostgreSQLContainer postgreSQLContainer(){ 298 | return new PostgreSQLContainer<>("postgres:15-alpine"); 299 | } 300 | } 301 | ``` 302 | 303 | Next, create a `TestApplication` class under `src/test/java` as follows: 304 | 305 | ```java 306 | public class TestApplication { 307 | public static void main(String[] args) { 308 | SpringApplication 309 | .from(Application::main) 310 | .with(ContainersConfig.class) 311 | .run(args); 312 | } 313 | } 314 | ``` 315 | 316 | Now you can either run `TestApplication` from your IDE or use your build tool to start the application as follows: 317 | 318 | ```shell 319 | $ ./gradlew bootTestRun //for Gradle 320 | $ ./mvnw spring-boot:test-run //for Maven 321 | ``` 322 | 323 | You can access the application UI at http://localhost:8080 and enter http://localhost:8080/todos as API URL. 324 | 325 | ### 5.1 Using DevTools with Testcontainers at Development Time 326 | During development, you can use Spring Boot DevTools to reload the code changes without having to completely restart the application. 327 | You can also configure your containers to reuse the existing containers by adding `@RestartScope`. 328 | 329 | First, Add `spring-boot-devtools` dependency. 330 | 331 | **Gradle** 332 | 333 | ```groovy 334 | testImplementation 'org.springframework.boot:spring-boot-devtools' 335 | ``` 336 | 337 | **Maven** 338 | 339 | ```xml 340 | 341 | org.springframework.boot 342 | spring-boot-devtools 343 | runtime 344 | true 345 | 346 | ``` 347 | 348 | Next, add `@RestartScope` annotation on container bean definition as follows: 349 | 350 | ```java 351 | @TestConfiguration(proxyBeanMethods = false) 352 | public class ContainersConfig { 353 | 354 | @Bean 355 | @ServiceConnection 356 | @RestartScope 357 | PostgreSQLContainer postgreSQLContainer(){ 358 | return new PostgreSQLContainer<>("postgres:15-alpine"); 359 | } 360 | 361 | } 362 | ``` 363 | 364 | Now when devtools reloads your application, the same containers will be reused instead of re-creating them. 365 | 366 | ## 6. Switch to MongoDB 367 | Let's explore how Testcontainers allow using other technologies in your unit tests. 368 | In this chapter, we'll switch the application to use MongoDB as its data store, and will adapt the tests accordingly. 369 | 370 | The application has several tests in the `TodoControllerTests` class for testing various API endpoints. 371 | These high-level tests enable the developers to enhance or refactor the code without breaking the API contracts. 372 | 373 | Let us see how we can switch to MongoDB and use Testcontainers `MongoDBContainer` to ensure API endpoints are not broken and are working as expected. 374 | 375 | ### 6.1. Switch to MongoDB and Spring Data Mongo 376 | Following are the changes to use MongoDB instead of Postgres. 377 | 378 | #### 6.1.1. Update dependencies in `build.gradle` 379 | * Remove `spring-boot-starter-data-jpa`, `flyway-core`, `postgresql`, `org.testcontainers:postgresql` dependencies. 380 | * Add the following dependencies: 381 | 382 | * If you are using Gradle 383 | ```groovy 384 | dependencies { 385 | implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' 386 | testImplementation 'org.testcontainers:mongodb' 387 | } 388 | ``` 389 | 390 | * If you are using Maven 391 | ```xml 392 | 393 | 394 | org.springframework.boot 395 | spring-boot-starter-data-mongodb 396 | 397 | 398 | org.testcontainers 399 | mongodb 400 | test 401 | 402 | 403 | ``` 404 | #### 6.1.2. Delete flyway migrations 405 | Delete flyway migrations under `src/main/resources/db/migration` folder. 406 | 407 | #### 6.1.3. Update `Todo.java` 408 | Update `Todo.java` which is currently a JPA entity to represent a Mongo Document using Spring Data Mongo as follows: 409 | 410 | ```java 411 | import org.springframework.data.annotation.Id; 412 | import org.springframework.data.mongodb.core.mapping.Document; 413 | 414 | @Document(collection = "todos") 415 | public class Todo { 416 | @Id 417 | private String id; 418 | private String title; 419 | private Boolean completed; 420 | private Integer order; 421 | //setter & getters 422 | ... 423 | } 424 | ``` 425 | #### 6.1.4. Update `TodoControllerTests.java` 426 | Update `TodoControllerTests.java` to use `MongoDBContainer` instead of `PostgreSQLContainer`. 427 | 428 | ```java 429 | 430 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 431 | @Testcontainers 432 | class TodoControllerTest { 433 | 434 | @Container 435 | @ServiceConnection 436 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0.5"); 437 | 438 | // tests 439 | } 440 | ``` 441 | 442 | #### 6.1.5. Update `ApplicationTests.java` 443 | Update `ApplicationTests.java` to run MongoDB container using JUnit5 Extension. 444 | 445 | ```java 446 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 447 | @Testcontainers 448 | class ApplicationTests { 449 | @Container 450 | @ServiceConnection 451 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0.5"); 452 | 453 | @Test 454 | void contextLoads() { 455 | } 456 | 457 | } 458 | ``` 459 | 460 | We have made all the changes to migrate from Postgres to MongoDB. Let us verify it by running tests. 461 | 462 | ```shell 463 | $ ./gradlew test 464 | $ ./mvnw verify 465 | ``` 466 | 467 | All tests should PASS. 468 | 469 | ## Conclusion 470 | Testcontainers enable using the real dependency services like SQL databases, NoSQL datastores, message brokers 471 | or any containerized services for that matter. This approach allows you to create reliable test suites improving confidence in your code. 472 | -------------------------------------------------------------------------------- /src/main/resources/public/js/vendor/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.6.0 2 | // http://underscorejs.org 3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var 25 | push = ArrayProto.push, 26 | slice = ArrayProto.slice, 27 | concat = ArrayProto.concat, 28 | toString = ObjProto.toString, 29 | hasOwnProperty = ObjProto.hasOwnProperty; 30 | 31 | // All **ECMAScript 5** native function implementations that we hope to use 32 | // are declared here. 33 | var 34 | nativeForEach = ArrayProto.forEach, 35 | nativeMap = ArrayProto.map, 36 | nativeReduce = ArrayProto.reduce, 37 | nativeReduceRight = ArrayProto.reduceRight, 38 | nativeFilter = ArrayProto.filter, 39 | nativeEvery = ArrayProto.every, 40 | nativeSome = ArrayProto.some, 41 | nativeIndexOf = ArrayProto.indexOf, 42 | nativeLastIndexOf = ArrayProto.lastIndexOf, 43 | nativeIsArray = Array.isArray, 44 | nativeKeys = Object.keys, 45 | nativeBind = FuncProto.bind; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { 49 | if (obj instanceof _) return obj; 50 | if (!(this instanceof _)) return new _(obj); 51 | this._wrapped = obj; 52 | }; 53 | 54 | // Export the Underscore object for **Node.js**, with 55 | // backwards-compatibility for the old `require()` API. If we're in 56 | // the browser, add `_` as a global object via a string identifier, 57 | // for Closure Compiler "advanced" mode. 58 | if (typeof exports !== 'undefined') { 59 | if (typeof module !== 'undefined' && module.exports) { 60 | exports = module.exports = _; 61 | } 62 | exports._ = _; 63 | } else { 64 | root._ = _; 65 | } 66 | 67 | // Current version. 68 | _.VERSION = '1.6.0'; 69 | 70 | // Collection Functions 71 | // -------------------- 72 | 73 | // The cornerstone, an `each` implementation, aka `forEach`. 74 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 75 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 76 | var each = _.each = _.forEach = function(obj, iterator, context) { 77 | if (obj == null) return obj; 78 | if (nativeForEach && obj.forEach === nativeForEach) { 79 | obj.forEach(iterator, context); 80 | } else if (obj.length === +obj.length) { 81 | for (var i = 0, length = obj.length; i < length; i++) { 82 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 83 | } 84 | } else { 85 | var keys = _.keys(obj); 86 | for (var i = 0, length = keys.length; i < length; i++) { 87 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; 88 | } 89 | } 90 | return obj; 91 | }; 92 | 93 | // Return the results of applying the iterator to each element. 94 | // Delegates to **ECMAScript 5**'s native `map` if available. 95 | _.map = _.collect = function(obj, iterator, context) { 96 | var results = []; 97 | if (obj == null) return results; 98 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 99 | each(obj, function(value, index, list) { 100 | results.push(iterator.call(context, value, index, list)); 101 | }); 102 | return results; 103 | }; 104 | 105 | var reduceError = 'Reduce of empty array with no initial value'; 106 | 107 | // **Reduce** builds up a single result from a list of values, aka `inject`, 108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 110 | var initial = arguments.length > 2; 111 | if (obj == null) obj = []; 112 | if (nativeReduce && obj.reduce === nativeReduce) { 113 | if (context) iterator = _.bind(iterator, context); 114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 115 | } 116 | each(obj, function(value, index, list) { 117 | if (!initial) { 118 | memo = value; 119 | initial = true; 120 | } else { 121 | memo = iterator.call(context, memo, value, index, list); 122 | } 123 | }); 124 | if (!initial) throw new TypeError(reduceError); 125 | return memo; 126 | }; 127 | 128 | // The right-associative version of reduce, also known as `foldr`. 129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 131 | var initial = arguments.length > 2; 132 | if (obj == null) obj = []; 133 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 134 | if (context) iterator = _.bind(iterator, context); 135 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 136 | } 137 | var length = obj.length; 138 | if (length !== +length) { 139 | var keys = _.keys(obj); 140 | length = keys.length; 141 | } 142 | each(obj, function(value, index, list) { 143 | index = keys ? keys[--length] : --length; 144 | if (!initial) { 145 | memo = obj[index]; 146 | initial = true; 147 | } else { 148 | memo = iterator.call(context, memo, obj[index], index, list); 149 | } 150 | }); 151 | if (!initial) throw new TypeError(reduceError); 152 | return memo; 153 | }; 154 | 155 | // Return the first value which passes a truth test. Aliased as `detect`. 156 | _.find = _.detect = function(obj, predicate, context) { 157 | var result; 158 | any(obj, function(value, index, list) { 159 | if (predicate.call(context, value, index, list)) { 160 | result = value; 161 | return true; 162 | } 163 | }); 164 | return result; 165 | }; 166 | 167 | // Return all the elements that pass a truth test. 168 | // Delegates to **ECMAScript 5**'s native `filter` if available. 169 | // Aliased as `select`. 170 | _.filter = _.select = function(obj, predicate, context) { 171 | var results = []; 172 | if (obj == null) return results; 173 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); 174 | each(obj, function(value, index, list) { 175 | if (predicate.call(context, value, index, list)) results.push(value); 176 | }); 177 | return results; 178 | }; 179 | 180 | // Return all the elements for which a truth test fails. 181 | _.reject = function(obj, predicate, context) { 182 | return _.filter(obj, function(value, index, list) { 183 | return !predicate.call(context, value, index, list); 184 | }, context); 185 | }; 186 | 187 | // Determine whether all of the elements match a truth test. 188 | // Delegates to **ECMAScript 5**'s native `every` if available. 189 | // Aliased as `all`. 190 | _.every = _.all = function(obj, predicate, context) { 191 | predicate || (predicate = _.identity); 192 | var result = true; 193 | if (obj == null) return result; 194 | if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); 195 | each(obj, function(value, index, list) { 196 | if (!(result = result && predicate.call(context, value, index, list))) return breaker; 197 | }); 198 | return !!result; 199 | }; 200 | 201 | // Determine if at least one element in the object matches a truth test. 202 | // Delegates to **ECMAScript 5**'s native `some` if available. 203 | // Aliased as `any`. 204 | var any = _.some = _.any = function(obj, predicate, context) { 205 | predicate || (predicate = _.identity); 206 | var result = false; 207 | if (obj == null) return result; 208 | if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); 209 | each(obj, function(value, index, list) { 210 | if (result || (result = predicate.call(context, value, index, list))) return breaker; 211 | }); 212 | return !!result; 213 | }; 214 | 215 | // Determine if the array or object contains a given value (using `===`). 216 | // Aliased as `include`. 217 | _.contains = _.include = function(obj, target) { 218 | if (obj == null) return false; 219 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 220 | return any(obj, function(value) { 221 | return value === target; 222 | }); 223 | }; 224 | 225 | // Invoke a method (with arguments) on every item in a collection. 226 | _.invoke = function(obj, method) { 227 | var args = slice.call(arguments, 2); 228 | var isFunc = _.isFunction(method); 229 | return _.map(obj, function(value) { 230 | return (isFunc ? method : value[method]).apply(value, args); 231 | }); 232 | }; 233 | 234 | // Convenience version of a common use case of `map`: fetching a property. 235 | _.pluck = function(obj, key) { 236 | return _.map(obj, _.property(key)); 237 | }; 238 | 239 | // Convenience version of a common use case of `filter`: selecting only objects 240 | // containing specific `key:value` pairs. 241 | _.where = function(obj, attrs) { 242 | return _.filter(obj, _.matches(attrs)); 243 | }; 244 | 245 | // Convenience version of a common use case of `find`: getting the first object 246 | // containing specific `key:value` pairs. 247 | _.findWhere = function(obj, attrs) { 248 | return _.find(obj, _.matches(attrs)); 249 | }; 250 | 251 | // Return the maximum element or (element-based computation). 252 | // Can't optimize arrays of integers longer than 65,535 elements. 253 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) 254 | _.max = function(obj, iterator, context) { 255 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 256 | return Math.max.apply(Math, obj); 257 | } 258 | var result = -Infinity, lastComputed = -Infinity; 259 | each(obj, function(value, index, list) { 260 | var computed = iterator ? iterator.call(context, value, index, list) : value; 261 | if (computed > lastComputed) { 262 | result = value; 263 | lastComputed = computed; 264 | } 265 | }); 266 | return result; 267 | }; 268 | 269 | // Return the minimum element (or element-based computation). 270 | _.min = function(obj, iterator, context) { 271 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 272 | return Math.min.apply(Math, obj); 273 | } 274 | var result = Infinity, lastComputed = Infinity; 275 | each(obj, function(value, index, list) { 276 | var computed = iterator ? iterator.call(context, value, index, list) : value; 277 | if (computed < lastComputed) { 278 | result = value; 279 | lastComputed = computed; 280 | } 281 | }); 282 | return result; 283 | }; 284 | 285 | // Shuffle an array, using the modern version of the 286 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 287 | _.shuffle = function(obj) { 288 | var rand; 289 | var index = 0; 290 | var shuffled = []; 291 | each(obj, function(value) { 292 | rand = _.random(index++); 293 | shuffled[index - 1] = shuffled[rand]; 294 | shuffled[rand] = value; 295 | }); 296 | return shuffled; 297 | }; 298 | 299 | // Sample **n** random values from a collection. 300 | // If **n** is not specified, returns a single random element. 301 | // The internal `guard` argument allows it to work with `map`. 302 | _.sample = function(obj, n, guard) { 303 | if (n == null || guard) { 304 | if (obj.length !== +obj.length) obj = _.values(obj); 305 | return obj[_.random(obj.length - 1)]; 306 | } 307 | return _.shuffle(obj).slice(0, Math.max(0, n)); 308 | }; 309 | 310 | // An internal function to generate lookup iterators. 311 | var lookupIterator = function(value) { 312 | if (value == null) return _.identity; 313 | if (_.isFunction(value)) return value; 314 | return _.property(value); 315 | }; 316 | 317 | // Sort the object's values by a criterion produced by an iterator. 318 | _.sortBy = function(obj, iterator, context) { 319 | iterator = lookupIterator(iterator); 320 | return _.pluck(_.map(obj, function(value, index, list) { 321 | return { 322 | value: value, 323 | index: index, 324 | criteria: iterator.call(context, value, index, list) 325 | }; 326 | }).sort(function(left, right) { 327 | var a = left.criteria; 328 | var b = right.criteria; 329 | if (a !== b) { 330 | if (a > b || a === void 0) return 1; 331 | if (a < b || b === void 0) return -1; 332 | } 333 | return left.index - right.index; 334 | }), 'value'); 335 | }; 336 | 337 | // An internal function used for aggregate "group by" operations. 338 | var group = function(behavior) { 339 | return function(obj, iterator, context) { 340 | var result = {}; 341 | iterator = lookupIterator(iterator); 342 | each(obj, function(value, index) { 343 | var key = iterator.call(context, value, index, obj); 344 | behavior(result, key, value); 345 | }); 346 | return result; 347 | }; 348 | }; 349 | 350 | // Groups the object's values by a criterion. Pass either a string attribute 351 | // to group by, or a function that returns the criterion. 352 | _.groupBy = group(function(result, key, value) { 353 | _.has(result, key) ? result[key].push(value) : result[key] = [value]; 354 | }); 355 | 356 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 357 | // when you know that your index values will be unique. 358 | _.indexBy = group(function(result, key, value) { 359 | result[key] = value; 360 | }); 361 | 362 | // Counts instances of an object that group by a certain criterion. Pass 363 | // either a string attribute to count by, or a function that returns the 364 | // criterion. 365 | _.countBy = group(function(result, key) { 366 | _.has(result, key) ? result[key]++ : result[key] = 1; 367 | }); 368 | 369 | // Use a comparator function to figure out the smallest index at which 370 | // an object should be inserted so as to maintain order. Uses binary search. 371 | _.sortedIndex = function(array, obj, iterator, context) { 372 | iterator = lookupIterator(iterator); 373 | var value = iterator.call(context, obj); 374 | var low = 0, high = array.length; 375 | while (low < high) { 376 | var mid = (low + high) >>> 1; 377 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 378 | } 379 | return low; 380 | }; 381 | 382 | // Safely create a real, live array from anything iterable. 383 | _.toArray = function(obj) { 384 | if (!obj) return []; 385 | if (_.isArray(obj)) return slice.call(obj); 386 | if (obj.length === +obj.length) return _.map(obj, _.identity); 387 | return _.values(obj); 388 | }; 389 | 390 | // Return the number of elements in an object. 391 | _.size = function(obj) { 392 | if (obj == null) return 0; 393 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 394 | }; 395 | 396 | // Array Functions 397 | // --------------- 398 | 399 | // Get the first element of an array. Passing **n** will return the first N 400 | // values in the array. Aliased as `head` and `take`. The **guard** check 401 | // allows it to work with `_.map`. 402 | _.first = _.head = _.take = function(array, n, guard) { 403 | if (array == null) return void 0; 404 | if ((n == null) || guard) return array[0]; 405 | if (n < 0) return []; 406 | return slice.call(array, 0, n); 407 | }; 408 | 409 | // Returns everything but the last entry of the array. Especially useful on 410 | // the arguments object. Passing **n** will return all the values in 411 | // the array, excluding the last N. The **guard** check allows it to work with 412 | // `_.map`. 413 | _.initial = function(array, n, guard) { 414 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 415 | }; 416 | 417 | // Get the last element of an array. Passing **n** will return the last N 418 | // values in the array. The **guard** check allows it to work with `_.map`. 419 | _.last = function(array, n, guard) { 420 | if (array == null) return void 0; 421 | if ((n == null) || guard) return array[array.length - 1]; 422 | return slice.call(array, Math.max(array.length - n, 0)); 423 | }; 424 | 425 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 426 | // Especially useful on the arguments object. Passing an **n** will return 427 | // the rest N values in the array. The **guard** 428 | // check allows it to work with `_.map`. 429 | _.rest = _.tail = _.drop = function(array, n, guard) { 430 | return slice.call(array, (n == null) || guard ? 1 : n); 431 | }; 432 | 433 | // Trim out all falsy values from an array. 434 | _.compact = function(array) { 435 | return _.filter(array, _.identity); 436 | }; 437 | 438 | // Internal implementation of a recursive `flatten` function. 439 | var flatten = function(input, shallow, output) { 440 | if (shallow && _.every(input, _.isArray)) { 441 | return concat.apply(output, input); 442 | } 443 | each(input, function(value) { 444 | if (_.isArray(value) || _.isArguments(value)) { 445 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 446 | } else { 447 | output.push(value); 448 | } 449 | }); 450 | return output; 451 | }; 452 | 453 | // Flatten out an array, either recursively (by default), or just one level. 454 | _.flatten = function(array, shallow) { 455 | return flatten(array, shallow, []); 456 | }; 457 | 458 | // Return a version of the array that does not contain the specified value(s). 459 | _.without = function(array) { 460 | return _.difference(array, slice.call(arguments, 1)); 461 | }; 462 | 463 | // Split an array into two arrays: one whose elements all satisfy the given 464 | // predicate, and one whose elements all do not satisfy the predicate. 465 | _.partition = function(array, predicate) { 466 | var pass = [], fail = []; 467 | each(array, function(elem) { 468 | (predicate(elem) ? pass : fail).push(elem); 469 | }); 470 | return [pass, fail]; 471 | }; 472 | 473 | // Produce a duplicate-free version of the array. If the array has already 474 | // been sorted, you have the option of using a faster algorithm. 475 | // Aliased as `unique`. 476 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 477 | if (_.isFunction(isSorted)) { 478 | context = iterator; 479 | iterator = isSorted; 480 | isSorted = false; 481 | } 482 | var initial = iterator ? _.map(array, iterator, context) : array; 483 | var results = []; 484 | var seen = []; 485 | each(initial, function(value, index) { 486 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 487 | seen.push(value); 488 | results.push(array[index]); 489 | } 490 | }); 491 | return results; 492 | }; 493 | 494 | // Produce an array that contains the union: each distinct element from all of 495 | // the passed-in arrays. 496 | _.union = function() { 497 | return _.uniq(_.flatten(arguments, true)); 498 | }; 499 | 500 | // Produce an array that contains every item shared between all the 501 | // passed-in arrays. 502 | _.intersection = function(array) { 503 | var rest = slice.call(arguments, 1); 504 | return _.filter(_.uniq(array), function(item) { 505 | return _.every(rest, function(other) { 506 | return _.contains(other, item); 507 | }); 508 | }); 509 | }; 510 | 511 | // Take the difference between one array and a number of other arrays. 512 | // Only the elements present in just the first array will remain. 513 | _.difference = function(array) { 514 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 515 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 516 | }; 517 | 518 | // Zip together multiple lists into a single array -- elements that share 519 | // an index go together. 520 | _.zip = function() { 521 | var length = _.max(_.pluck(arguments, 'length').concat(0)); 522 | var results = new Array(length); 523 | for (var i = 0; i < length; i++) { 524 | results[i] = _.pluck(arguments, '' + i); 525 | } 526 | return results; 527 | }; 528 | 529 | // Converts lists into objects. Pass either a single array of `[key, value]` 530 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 531 | // the corresponding values. 532 | _.object = function(list, values) { 533 | if (list == null) return {}; 534 | var result = {}; 535 | for (var i = 0, length = list.length; i < length; i++) { 536 | if (values) { 537 | result[list[i]] = values[i]; 538 | } else { 539 | result[list[i][0]] = list[i][1]; 540 | } 541 | } 542 | return result; 543 | }; 544 | 545 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 546 | // we need this function. Return the position of the first occurrence of an 547 | // item in an array, or -1 if the item is not included in the array. 548 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 549 | // If the array is large and already in sort order, pass `true` 550 | // for **isSorted** to use binary search. 551 | _.indexOf = function(array, item, isSorted) { 552 | if (array == null) return -1; 553 | var i = 0, length = array.length; 554 | if (isSorted) { 555 | if (typeof isSorted == 'number') { 556 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); 557 | } else { 558 | i = _.sortedIndex(array, item); 559 | return array[i] === item ? i : -1; 560 | } 561 | } 562 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 563 | for (; i < length; i++) if (array[i] === item) return i; 564 | return -1; 565 | }; 566 | 567 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 568 | _.lastIndexOf = function(array, item, from) { 569 | if (array == null) return -1; 570 | var hasIndex = from != null; 571 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 572 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 573 | } 574 | var i = (hasIndex ? from : array.length); 575 | while (i--) if (array[i] === item) return i; 576 | return -1; 577 | }; 578 | 579 | // Generate an integer Array containing an arithmetic progression. A port of 580 | // the native Python `range()` function. See 581 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 582 | _.range = function(start, stop, step) { 583 | if (arguments.length <= 1) { 584 | stop = start || 0; 585 | start = 0; 586 | } 587 | step = arguments[2] || 1; 588 | 589 | var length = Math.max(Math.ceil((stop - start) / step), 0); 590 | var idx = 0; 591 | var range = new Array(length); 592 | 593 | while(idx < length) { 594 | range[idx++] = start; 595 | start += step; 596 | } 597 | 598 | return range; 599 | }; 600 | 601 | // Function (ahem) Functions 602 | // ------------------ 603 | 604 | // Reusable constructor function for prototype setting. 605 | var ctor = function(){}; 606 | 607 | // Create a function bound to a given object (assigning `this`, and arguments, 608 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 609 | // available. 610 | _.bind = function(func, context) { 611 | var args, bound; 612 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 613 | if (!_.isFunction(func)) throw new TypeError; 614 | args = slice.call(arguments, 2); 615 | return bound = function() { 616 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 617 | ctor.prototype = func.prototype; 618 | var self = new ctor; 619 | ctor.prototype = null; 620 | var result = func.apply(self, args.concat(slice.call(arguments))); 621 | if (Object(result) === result) return result; 622 | return self; 623 | }; 624 | }; 625 | 626 | // Partially apply a function by creating a version that has had some of its 627 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 628 | // as a placeholder, allowing any combination of arguments to be pre-filled. 629 | _.partial = function(func) { 630 | var boundArgs = slice.call(arguments, 1); 631 | return function() { 632 | var position = 0; 633 | var args = boundArgs.slice(); 634 | for (var i = 0, length = args.length; i < length; i++) { 635 | if (args[i] === _) args[i] = arguments[position++]; 636 | } 637 | while (position < arguments.length) args.push(arguments[position++]); 638 | return func.apply(this, args); 639 | }; 640 | }; 641 | 642 | // Bind a number of an object's methods to that object. Remaining arguments 643 | // are the method names to be bound. Useful for ensuring that all callbacks 644 | // defined on an object belong to it. 645 | _.bindAll = function(obj) { 646 | var funcs = slice.call(arguments, 1); 647 | if (funcs.length === 0) throw new Error('bindAll must be passed function names'); 648 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 649 | return obj; 650 | }; 651 | 652 | // Memoize an expensive function by storing its results. 653 | _.memoize = function(func, hasher) { 654 | var memo = {}; 655 | hasher || (hasher = _.identity); 656 | return function() { 657 | var key = hasher.apply(this, arguments); 658 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 659 | }; 660 | }; 661 | 662 | // Delays a function for the given number of milliseconds, and then calls 663 | // it with the arguments supplied. 664 | _.delay = function(func, wait) { 665 | var args = slice.call(arguments, 2); 666 | return setTimeout(function(){ return func.apply(null, args); }, wait); 667 | }; 668 | 669 | // Defers a function, scheduling it to run after the current call stack has 670 | // cleared. 671 | _.defer = function(func) { 672 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 673 | }; 674 | 675 | // Returns a function, that, when invoked, will only be triggered at most once 676 | // during a given window of time. Normally, the throttled function will run 677 | // as much as it can, without ever going more than once per `wait` duration; 678 | // but if you'd like to disable the execution on the leading edge, pass 679 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 680 | _.throttle = function(func, wait, options) { 681 | var context, args, result; 682 | var timeout = null; 683 | var previous = 0; 684 | options || (options = {}); 685 | var later = function() { 686 | previous = options.leading === false ? 0 : _.now(); 687 | timeout = null; 688 | result = func.apply(context, args); 689 | context = args = null; 690 | }; 691 | return function() { 692 | var now = _.now(); 693 | if (!previous && options.leading === false) previous = now; 694 | var remaining = wait - (now - previous); 695 | context = this; 696 | args = arguments; 697 | if (remaining <= 0) { 698 | clearTimeout(timeout); 699 | timeout = null; 700 | previous = now; 701 | result = func.apply(context, args); 702 | context = args = null; 703 | } else if (!timeout && options.trailing !== false) { 704 | timeout = setTimeout(later, remaining); 705 | } 706 | return result; 707 | }; 708 | }; 709 | 710 | // Returns a function, that, as long as it continues to be invoked, will not 711 | // be triggered. The function will be called after it stops being called for 712 | // N milliseconds. If `immediate` is passed, trigger the function on the 713 | // leading edge, instead of the trailing. 714 | _.debounce = function(func, wait, immediate) { 715 | var timeout, args, context, timestamp, result; 716 | 717 | var later = function() { 718 | var last = _.now() - timestamp; 719 | if (last < wait) { 720 | timeout = setTimeout(later, wait - last); 721 | } else { 722 | timeout = null; 723 | if (!immediate) { 724 | result = func.apply(context, args); 725 | context = args = null; 726 | } 727 | } 728 | }; 729 | 730 | return function() { 731 | context = this; 732 | args = arguments; 733 | timestamp = _.now(); 734 | var callNow = immediate && !timeout; 735 | if (!timeout) { 736 | timeout = setTimeout(later, wait); 737 | } 738 | if (callNow) { 739 | result = func.apply(context, args); 740 | context = args = null; 741 | } 742 | 743 | return result; 744 | }; 745 | }; 746 | 747 | // Returns a function that will be executed at most one time, no matter how 748 | // often you call it. Useful for lazy initialization. 749 | _.once = function(func) { 750 | var ran = false, memo; 751 | return function() { 752 | if (ran) return memo; 753 | ran = true; 754 | memo = func.apply(this, arguments); 755 | func = null; 756 | return memo; 757 | }; 758 | }; 759 | 760 | // Returns the first function passed as an argument to the second, 761 | // allowing you to adjust arguments, run code before and after, and 762 | // conditionally execute the original function. 763 | _.wrap = function(func, wrapper) { 764 | return _.partial(wrapper, func); 765 | }; 766 | 767 | // Returns a function that is the composition of a list of functions, each 768 | // consuming the return value of the function that follows. 769 | _.compose = function() { 770 | var funcs = arguments; 771 | return function() { 772 | var args = arguments; 773 | for (var i = funcs.length - 1; i >= 0; i--) { 774 | args = [funcs[i].apply(this, args)]; 775 | } 776 | return args[0]; 777 | }; 778 | }; 779 | 780 | // Returns a function that will only be executed after being called N times. 781 | _.after = function(times, func) { 782 | return function() { 783 | if (--times < 1) { 784 | return func.apply(this, arguments); 785 | } 786 | }; 787 | }; 788 | 789 | // Object Functions 790 | // ---------------- 791 | 792 | // Retrieve the names of an object's properties. 793 | // Delegates to **ECMAScript 5**'s native `Object.keys` 794 | _.keys = function(obj) { 795 | if (!_.isObject(obj)) return []; 796 | if (nativeKeys) return nativeKeys(obj); 797 | var keys = []; 798 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 799 | return keys; 800 | }; 801 | 802 | // Retrieve the values of an object's properties. 803 | _.values = function(obj) { 804 | var keys = _.keys(obj); 805 | var length = keys.length; 806 | var values = new Array(length); 807 | for (var i = 0; i < length; i++) { 808 | values[i] = obj[keys[i]]; 809 | } 810 | return values; 811 | }; 812 | 813 | // Convert an object into a list of `[key, value]` pairs. 814 | _.pairs = function(obj) { 815 | var keys = _.keys(obj); 816 | var length = keys.length; 817 | var pairs = new Array(length); 818 | for (var i = 0; i < length; i++) { 819 | pairs[i] = [keys[i], obj[keys[i]]]; 820 | } 821 | return pairs; 822 | }; 823 | 824 | // Invert the keys and values of an object. The values must be serializable. 825 | _.invert = function(obj) { 826 | var result = {}; 827 | var keys = _.keys(obj); 828 | for (var i = 0, length = keys.length; i < length; i++) { 829 | result[obj[keys[i]]] = keys[i]; 830 | } 831 | return result; 832 | }; 833 | 834 | // Return a sorted list of the function names available on the object. 835 | // Aliased as `methods` 836 | _.functions = _.methods = function(obj) { 837 | var names = []; 838 | for (var key in obj) { 839 | if (_.isFunction(obj[key])) names.push(key); 840 | } 841 | return names.sort(); 842 | }; 843 | 844 | // Extend a given object with all the properties in passed-in object(s). 845 | _.extend = function(obj) { 846 | each(slice.call(arguments, 1), function(source) { 847 | if (source) { 848 | for (var prop in source) { 849 | obj[prop] = source[prop]; 850 | } 851 | } 852 | }); 853 | return obj; 854 | }; 855 | 856 | // Return a copy of the object only containing the whitelisted properties. 857 | _.pick = function(obj) { 858 | var copy = {}; 859 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 860 | each(keys, function(key) { 861 | if (key in obj) copy[key] = obj[key]; 862 | }); 863 | return copy; 864 | }; 865 | 866 | // Return a copy of the object without the blacklisted properties. 867 | _.omit = function(obj) { 868 | var copy = {}; 869 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 870 | for (var key in obj) { 871 | if (!_.contains(keys, key)) copy[key] = obj[key]; 872 | } 873 | return copy; 874 | }; 875 | 876 | // Fill in a given object with default properties. 877 | _.defaults = function(obj) { 878 | each(slice.call(arguments, 1), function(source) { 879 | if (source) { 880 | for (var prop in source) { 881 | if (obj[prop] === void 0) obj[prop] = source[prop]; 882 | } 883 | } 884 | }); 885 | return obj; 886 | }; 887 | 888 | // Create a (shallow-cloned) duplicate of an object. 889 | _.clone = function(obj) { 890 | if (!_.isObject(obj)) return obj; 891 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 892 | }; 893 | 894 | // Invokes interceptor with the obj, and then returns obj. 895 | // The primary purpose of this method is to "tap into" a method chain, in 896 | // order to perform operations on intermediate results within the chain. 897 | _.tap = function(obj, interceptor) { 898 | interceptor(obj); 899 | return obj; 900 | }; 901 | 902 | // Internal recursive comparison function for `isEqual`. 903 | var eq = function(a, b, aStack, bStack) { 904 | // Identical objects are equal. `0 === -0`, but they aren't identical. 905 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 906 | if (a === b) return a !== 0 || 1 / a == 1 / b; 907 | // A strict comparison is necessary because `null == undefined`. 908 | if (a == null || b == null) return a === b; 909 | // Unwrap any wrapped objects. 910 | if (a instanceof _) a = a._wrapped; 911 | if (b instanceof _) b = b._wrapped; 912 | // Compare `[[Class]]` names. 913 | var className = toString.call(a); 914 | if (className != toString.call(b)) return false; 915 | switch (className) { 916 | // Strings, numbers, dates, and booleans are compared by value. 917 | case '[object String]': 918 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 919 | // equivalent to `new String("5")`. 920 | return a == String(b); 921 | case '[object Number]': 922 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 923 | // other numeric values. 924 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 925 | case '[object Date]': 926 | case '[object Boolean]': 927 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 928 | // millisecond representations. Note that invalid dates with millisecond representations 929 | // of `NaN` are not equivalent. 930 | return +a == +b; 931 | // RegExps are compared by their source patterns and flags. 932 | case '[object RegExp]': 933 | return a.source == b.source && 934 | a.global == b.global && 935 | a.multiline == b.multiline && 936 | a.ignoreCase == b.ignoreCase; 937 | } 938 | if (typeof a != 'object' || typeof b != 'object') return false; 939 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 940 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 941 | var length = aStack.length; 942 | while (length--) { 943 | // Linear search. Performance is inversely proportional to the number of 944 | // unique nested structures. 945 | if (aStack[length] == a) return bStack[length] == b; 946 | } 947 | // Objects with different constructors are not equivalent, but `Object`s 948 | // from different frames are. 949 | var aCtor = a.constructor, bCtor = b.constructor; 950 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 951 | _.isFunction(bCtor) && (bCtor instanceof bCtor)) 952 | && ('constructor' in a && 'constructor' in b)) { 953 | return false; 954 | } 955 | // Add the first object to the stack of traversed objects. 956 | aStack.push(a); 957 | bStack.push(b); 958 | var size = 0, result = true; 959 | // Recursively compare objects and arrays. 960 | if (className == '[object Array]') { 961 | // Compare array lengths to determine if a deep comparison is necessary. 962 | size = a.length; 963 | result = size == b.length; 964 | if (result) { 965 | // Deep compare the contents, ignoring non-numeric properties. 966 | while (size--) { 967 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 968 | } 969 | } 970 | } else { 971 | // Deep compare objects. 972 | for (var key in a) { 973 | if (_.has(a, key)) { 974 | // Count the expected number of properties. 975 | size++; 976 | // Deep compare each member. 977 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 978 | } 979 | } 980 | // Ensure that both objects contain the same number of properties. 981 | if (result) { 982 | for (key in b) { 983 | if (_.has(b, key) && !(size--)) break; 984 | } 985 | result = !size; 986 | } 987 | } 988 | // Remove the first object from the stack of traversed objects. 989 | aStack.pop(); 990 | bStack.pop(); 991 | return result; 992 | }; 993 | 994 | // Perform a deep comparison to check if two objects are equal. 995 | _.isEqual = function(a, b) { 996 | return eq(a, b, [], []); 997 | }; 998 | 999 | // Is a given array, string, or object empty? 1000 | // An "empty" object has no enumerable own-properties. 1001 | _.isEmpty = function(obj) { 1002 | if (obj == null) return true; 1003 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 1004 | for (var key in obj) if (_.has(obj, key)) return false; 1005 | return true; 1006 | }; 1007 | 1008 | // Is a given value a DOM element? 1009 | _.isElement = function(obj) { 1010 | return !!(obj && obj.nodeType === 1); 1011 | }; 1012 | 1013 | // Is a given value an array? 1014 | // Delegates to ECMA5's native Array.isArray 1015 | _.isArray = nativeIsArray || function(obj) { 1016 | return toString.call(obj) == '[object Array]'; 1017 | }; 1018 | 1019 | // Is a given variable an object? 1020 | _.isObject = function(obj) { 1021 | return obj === Object(obj); 1022 | }; 1023 | 1024 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 1025 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 1026 | _['is' + name] = function(obj) { 1027 | return toString.call(obj) == '[object ' + name + ']'; 1028 | }; 1029 | }); 1030 | 1031 | // Define a fallback version of the method in browsers (ahem, IE), where 1032 | // there isn't any inspectable "Arguments" type. 1033 | if (!_.isArguments(arguments)) { 1034 | _.isArguments = function(obj) { 1035 | return !!(obj && _.has(obj, 'callee')); 1036 | }; 1037 | } 1038 | 1039 | // Optimize `isFunction` if appropriate. 1040 | if (typeof (/./) !== 'function') { 1041 | _.isFunction = function(obj) { 1042 | return typeof obj === 'function'; 1043 | }; 1044 | } 1045 | 1046 | // Is a given object a finite number? 1047 | _.isFinite = function(obj) { 1048 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1049 | }; 1050 | 1051 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1052 | _.isNaN = function(obj) { 1053 | return _.isNumber(obj) && obj != +obj; 1054 | }; 1055 | 1056 | // Is a given value a boolean? 1057 | _.isBoolean = function(obj) { 1058 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 1059 | }; 1060 | 1061 | // Is a given value equal to null? 1062 | _.isNull = function(obj) { 1063 | return obj === null; 1064 | }; 1065 | 1066 | // Is a given variable undefined? 1067 | _.isUndefined = function(obj) { 1068 | return obj === void 0; 1069 | }; 1070 | 1071 | // Shortcut function for checking if an object has a given property directly 1072 | // on itself (in other words, not on a prototype). 1073 | _.has = function(obj, key) { 1074 | return hasOwnProperty.call(obj, key); 1075 | }; 1076 | 1077 | // Utility Functions 1078 | // ----------------- 1079 | 1080 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1081 | // previous owner. Returns a reference to the Underscore object. 1082 | _.noConflict = function() { 1083 | root._ = previousUnderscore; 1084 | return this; 1085 | }; 1086 | 1087 | // Keep the identity function around for default iterators. 1088 | _.identity = function(value) { 1089 | return value; 1090 | }; 1091 | 1092 | _.constant = function(value) { 1093 | return function () { 1094 | return value; 1095 | }; 1096 | }; 1097 | 1098 | _.property = function(key) { 1099 | return function(obj) { 1100 | return obj[key]; 1101 | }; 1102 | }; 1103 | 1104 | // Returns a predicate for checking whether an object has a given set of `key:value` pairs. 1105 | _.matches = function(attrs) { 1106 | return function(obj) { 1107 | if (obj === attrs) return true; //avoid comparing an object to itself. 1108 | for (var key in attrs) { 1109 | if (attrs[key] !== obj[key]) 1110 | return false; 1111 | } 1112 | return true; 1113 | } 1114 | }; 1115 | 1116 | // Run a function **n** times. 1117 | _.times = function(n, iterator, context) { 1118 | var accum = Array(Math.max(0, n)); 1119 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1120 | return accum; 1121 | }; 1122 | 1123 | // Return a random integer between min and max (inclusive). 1124 | _.random = function(min, max) { 1125 | if (max == null) { 1126 | max = min; 1127 | min = 0; 1128 | } 1129 | return min + Math.floor(Math.random() * (max - min + 1)); 1130 | }; 1131 | 1132 | // A (possibly faster) way to get the current timestamp as an integer. 1133 | _.now = Date.now || function() { return new Date().getTime(); }; 1134 | 1135 | // List of HTML entities for escaping. 1136 | var entityMap = { 1137 | escape: { 1138 | '&': '&', 1139 | '<': '<', 1140 | '>': '>', 1141 | '"': '"', 1142 | "'": ''' 1143 | } 1144 | }; 1145 | entityMap.unescape = _.invert(entityMap.escape); 1146 | 1147 | // Regexes containing the keys and values listed immediately above. 1148 | var entityRegexes = { 1149 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1150 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1151 | }; 1152 | 1153 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1154 | _.each(['escape', 'unescape'], function(method) { 1155 | _[method] = function(string) { 1156 | if (string == null) return ''; 1157 | return ('' + string).replace(entityRegexes[method], function(match) { 1158 | return entityMap[method][match]; 1159 | }); 1160 | }; 1161 | }); 1162 | 1163 | // If the value of the named `property` is a function then invoke it with the 1164 | // `object` as context; otherwise, return it. 1165 | _.result = function(object, property) { 1166 | if (object == null) return void 0; 1167 | var value = object[property]; 1168 | return _.isFunction(value) ? value.call(object) : value; 1169 | }; 1170 | 1171 | // Add your own custom functions to the Underscore object. 1172 | _.mixin = function(obj) { 1173 | each(_.functions(obj), function(name) { 1174 | var func = _[name] = obj[name]; 1175 | _.prototype[name] = function() { 1176 | var args = [this._wrapped]; 1177 | push.apply(args, arguments); 1178 | return result.call(this, func.apply(_, args)); 1179 | }; 1180 | }); 1181 | }; 1182 | 1183 | // Generate a unique integer id (unique within the entire client session). 1184 | // Useful for temporary DOM ids. 1185 | var idCounter = 0; 1186 | _.uniqueId = function(prefix) { 1187 | var id = ++idCounter + ''; 1188 | return prefix ? prefix + id : id; 1189 | }; 1190 | 1191 | // By default, Underscore uses ERB-style template delimiters, change the 1192 | // following template settings to use alternative delimiters. 1193 | _.templateSettings = { 1194 | evaluate : /<%([\s\S]+?)%>/g, 1195 | interpolate : /<%=([\s\S]+?)%>/g, 1196 | escape : /<%-([\s\S]+?)%>/g 1197 | }; 1198 | 1199 | // When customizing `templateSettings`, if you don't want to define an 1200 | // interpolation, evaluation or escaping regex, we need one that is 1201 | // guaranteed not to match. 1202 | var noMatch = /(.)^/; 1203 | 1204 | // Certain characters need to be escaped so that they can be put into a 1205 | // string literal. 1206 | var escapes = { 1207 | "'": "'", 1208 | '\\': '\\', 1209 | '\r': 'r', 1210 | '\n': 'n', 1211 | '\t': 't', 1212 | '\u2028': 'u2028', 1213 | '\u2029': 'u2029' 1214 | }; 1215 | 1216 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1217 | 1218 | // JavaScript micro-templating, similar to John Resig's implementation. 1219 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1220 | // and correctly escapes quotes within interpolated code. 1221 | _.template = function(text, data, settings) { 1222 | var render; 1223 | settings = _.defaults({}, settings, _.templateSettings); 1224 | 1225 | // Combine delimiters into one regular expression via alternation. 1226 | var matcher = new RegExp([ 1227 | (settings.escape || noMatch).source, 1228 | (settings.interpolate || noMatch).source, 1229 | (settings.evaluate || noMatch).source 1230 | ].join('|') + '|$', 'g'); 1231 | 1232 | // Compile the template source, escaping string literals appropriately. 1233 | var index = 0; 1234 | var source = "__p+='"; 1235 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1236 | source += text.slice(index, offset) 1237 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1238 | 1239 | if (escape) { 1240 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1241 | } 1242 | if (interpolate) { 1243 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1244 | } 1245 | if (evaluate) { 1246 | source += "';\n" + evaluate + "\n__p+='"; 1247 | } 1248 | index = offset + match.length; 1249 | return match; 1250 | }); 1251 | source += "';\n"; 1252 | 1253 | // If a variable is not specified, place data values in local scope. 1254 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1255 | 1256 | source = "var __t,__p='',__j=Array.prototype.join," + 1257 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1258 | source + "return __p;\n"; 1259 | 1260 | try { 1261 | render = new Function(settings.variable || 'obj', '_', source); 1262 | } catch (e) { 1263 | e.source = source; 1264 | throw e; 1265 | } 1266 | 1267 | if (data) return render(data, _); 1268 | var template = function(data) { 1269 | return render.call(this, data, _); 1270 | }; 1271 | 1272 | // Provide the compiled function source as a convenience for precompilation. 1273 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1274 | 1275 | return template; 1276 | }; 1277 | 1278 | // Add a "chain" function, which will delegate to the wrapper. 1279 | _.chain = function(obj) { 1280 | return _(obj).chain(); 1281 | }; 1282 | 1283 | // OOP 1284 | // --------------- 1285 | // If Underscore is called as a function, it returns a wrapped object that 1286 | // can be used OO-style. This wrapper holds altered versions of all the 1287 | // underscore functions. Wrapped objects may be chained. 1288 | 1289 | // Helper function to continue chaining intermediate results. 1290 | var result = function(obj) { 1291 | return this._chain ? _(obj).chain() : obj; 1292 | }; 1293 | 1294 | // Add all of the Underscore functions to the wrapper object. 1295 | _.mixin(_); 1296 | 1297 | // Add all mutator Array functions to the wrapper. 1298 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1299 | var method = ArrayProto[name]; 1300 | _.prototype[name] = function() { 1301 | var obj = this._wrapped; 1302 | method.apply(obj, arguments); 1303 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1304 | return result.call(this, obj); 1305 | }; 1306 | }); 1307 | 1308 | // Add all accessor Array functions to the wrapper. 1309 | each(['concat', 'join', 'slice'], function(name) { 1310 | var method = ArrayProto[name]; 1311 | _.prototype[name] = function() { 1312 | return result.call(this, method.apply(this._wrapped, arguments)); 1313 | }; 1314 | }); 1315 | 1316 | _.extend(_.prototype, { 1317 | 1318 | // Start chaining a wrapped Underscore object. 1319 | chain: function() { 1320 | this._chain = true; 1321 | return this; 1322 | }, 1323 | 1324 | // Extracts the result from a wrapped and chained object. 1325 | value: function() { 1326 | return this._wrapped; 1327 | } 1328 | 1329 | }); 1330 | 1331 | // AMD registration happens at the end for compatibility with AMD loaders 1332 | // that may not enforce next-turn semantics on modules. Even though general 1333 | // practice for AMD registration is to be anonymous, underscore registers 1334 | // as a named module because, like jQuery, it is a base library that is 1335 | // popular enough to be bundled in a third party lib, but not be part of 1336 | // an AMD load request. Those cases could generate an error when an 1337 | // anonymous define() is called outside of a loader request. 1338 | if (typeof define === 'function' && define.amd) { 1339 | define('underscore', [], function() { 1340 | return _; 1341 | }); 1342 | } 1343 | }).call(this); 1344 | --------------------------------------------------------------------------------