├── .gitignore ├── src ├── main │ ├── resources │ │ └── logback.xml │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── samples │ │ │ └── async │ │ │ ├── chat │ │ │ ├── ChatRepository.java │ │ │ ├── InMemoryChatRepository.java │ │ │ └── ChatController.java │ │ │ └── config │ │ │ ├── DispatcherServletInitializer.java │ │ │ └── WebMvcConfig.java │ └── webapp │ │ ├── WEB-INF │ │ └── templates │ │ │ └── chat.html │ │ └── resources │ │ └── js │ │ ├── chat.js │ │ ├── knockout-2.0.0.js │ │ └── jquery-1.7.2.min.js └── test │ ├── resources │ └── log4j.xml │ └── java │ └── org │ └── springframework │ └── samples │ └── async │ └── chat │ └── ChatControllerTests.java ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .project 3 | .classpath 4 | .settings 5 | *.iml 6 | /.idea/ 7 | bin 8 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Overview 3 | 4 | A chat sample using Spring MVC 3.2, Servlet-based, async request processing. Also see the [redis](https://github.com/rstoyanchev/spring-mvc-chat/tree/redis) branch for a distributed chat. 5 | 6 | ### Note 7 | 8 | There is a bug in Tomcat that affects this sample. Please, use Tomcat 7.0.32 or higher. 9 | 10 | ### Instructions 11 | 12 | Eclipse users run `mvn eclipse:eclipse` and then import the project. Or just import the code as a Maven project into IntelliJ, NetBeans, or Eclipse. 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/samples/async/chat/ChatRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.samples.async.chat; 18 | 19 | import java.util.List; 20 | 21 | public interface ChatRepository { 22 | 23 | List getMessages(int messageIndex); 24 | 25 | void addMessage(String message); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/samples/async/config/DispatcherServletInitializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.samples.async.config; 18 | 19 | import javax.servlet.ServletRegistration.Dynamic; 20 | 21 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 22 | 23 | public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 24 | 25 | @Override 26 | protected Class[] getRootConfigClasses() { 27 | return null; 28 | } 29 | 30 | @Override 31 | protected Class[] getServletConfigClasses() { 32 | return new Class[] { WebMvcConfig.class }; 33 | } 34 | 35 | @Override 36 | protected String[] getServletMappings() { 37 | return new String[] { "/" }; 38 | } 39 | 40 | @Override 41 | protected void customizeRegistration(Dynamic registration) { 42 | registration.setAsyncSupported(true); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/samples/async/chat/InMemoryChatRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.samples.async.chat; 18 | 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.concurrent.CopyOnWriteArrayList; 22 | 23 | import org.springframework.stereotype.Repository; 24 | import org.springframework.util.Assert; 25 | 26 | @Repository 27 | public class InMemoryChatRepository implements ChatRepository { 28 | 29 | private final List messages = new CopyOnWriteArrayList(); 30 | 31 | public List getMessages(int index) { 32 | if (this.messages.isEmpty()) { 33 | return Collections. emptyList(); 34 | } 35 | Assert.isTrue((index >= 0) && (index <= this.messages.size()), "Invalid message index"); 36 | return this.messages.subList(index, this.messages.size()); 37 | } 38 | 39 | public void addMessage(String message) { 40 | this.messages.add(message); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat 5 | 6 | 7 | 8 |

Chat

9 | 10 |
11 |

12 | 13 | 14 | 15 | 16 |

17 |
18 | 19 |
20 |

21 | You're chatting as 22 | 23 |

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

32 | 33 | 34 |

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/webapp/resources/js/chat.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | function ChatViewModel() { 4 | 5 | var that = this; 6 | 7 | that.userName = ko.observable(''); 8 | that.chatContent = ko.observable(''); 9 | that.message = ko.observable(''); 10 | that.messageIndex = ko.observable(0); 11 | that.activePollingXhr = ko.observable(null); 12 | 13 | var keepPolling = false; 14 | 15 | that.joinChat = function() { 16 | if (that.userName().trim() != '') { 17 | keepPolling = true; 18 | pollForMessages(); 19 | } 20 | } 21 | 22 | function pollForMessages() { 23 | if (!keepPolling) { 24 | return; 25 | } 26 | var form = $("#joinChatForm"); 27 | that.activePollingXhr($.ajax({url : form.attr("action"), type : "GET", data : form.serialize(),cache: false, 28 | success : function(messages) { 29 | for ( var i = 0; i < messages.length; i++) { 30 | that.chatContent(that.chatContent() + messages[i] + "\n"); 31 | that.messageIndex(that.messageIndex() + 1); 32 | } 33 | }, 34 | error : function(xhr) { 35 | if (xhr.statusText != "abort" && xhr.status != 503) { 36 | resetUI(); 37 | console.error("Unable to retrieve chat messages. Chat ended."); 38 | } 39 | }, 40 | complete : pollForMessages 41 | })); 42 | $('#message').focus(); 43 | } 44 | 45 | that.postMessage = function() { 46 | if (that.message().trim() != '') { 47 | var form = $("#postMessageForm"); 48 | $.ajax({url : form.attr("action"), type : "POST", 49 | data : "message=[" + that.userName() + "] " + $("#postMessageForm input[name=message]").val(), 50 | error : function(xhr) { 51 | console.error("Error posting chat message: status=" + xhr.status + ", statusText=" + xhr.statusText); 52 | } 53 | }); 54 | that.message(''); 55 | } 56 | } 57 | 58 | that.leaveChat = function() { 59 | that.activePollingXhr(null); 60 | resetUI(); 61 | this.userName(''); 62 | } 63 | 64 | function resetUI() { 65 | keepPolling = false; 66 | that.activePollingXhr(null); 67 | that.message(''); 68 | that.messageIndex(0); 69 | that.chatContent(''); 70 | } 71 | 72 | } 73 | 74 | //Activate knockout.js 75 | ko.applyBindings(new ChatViewModel()); 76 | 77 | }); 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/samples/async/chat/ChatController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.async.chat; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import org.springframework.web.context.request.async.DeferredResult; 16 | 17 | @RestController 18 | @RequestMapping("/mvc/chat") 19 | public class ChatController { 20 | 21 | private final ChatRepository chatRepository; 22 | 23 | private final Map>, Integer> chatRequests = 24 | new ConcurrentHashMap>, Integer>(); 25 | 26 | 27 | @Autowired 28 | public ChatController(ChatRepository chatRepository) { 29 | this.chatRepository = chatRepository; 30 | } 31 | 32 | @GetMapping 33 | public DeferredResult> getMessages(@RequestParam int messageIndex) { 34 | 35 | final DeferredResult> deferredResult = new DeferredResult>(null, Collections.emptyList()); 36 | this.chatRequests.put(deferredResult, messageIndex); 37 | 38 | deferredResult.onCompletion(new Runnable() { 39 | @Override 40 | public void run() { 41 | chatRequests.remove(deferredResult); 42 | } 43 | }); 44 | 45 | List messages = this.chatRepository.getMessages(messageIndex); 46 | if (!messages.isEmpty()) { 47 | deferredResult.setResult(messages); 48 | } 49 | 50 | return deferredResult; 51 | } 52 | 53 | @PostMapping 54 | public void postMessage(@RequestParam String message) { 55 | 56 | this.chatRepository.addMessage(message); 57 | 58 | // Update all chat requests as part of the POST request 59 | // See Redis branch for a more sophisticated, non-blocking approach 60 | 61 | for (Entry>, Integer> entry : this.chatRequests.entrySet()) { 62 | List messages = this.chatRepository.getMessages(entry.getValue()); 63 | entry.getKey().setResult(messages); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/samples/async/chat/ChatControllerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.async.chat; 17 | 18 | import static org.easymock.EasyMock.expect; 19 | import static org.easymock.EasyMock.replay; 20 | import static org.easymock.EasyMock.verify; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; 25 | 26 | import java.util.Arrays; 27 | import java.util.List; 28 | 29 | import org.easymock.EasyMock; 30 | import org.junit.Before; 31 | import org.junit.Test; 32 | import org.springframework.test.web.servlet.MockMvc; 33 | 34 | public class ChatControllerTests { 35 | 36 | private MockMvc mockMvc; 37 | 38 | private ChatRepository chatRepository; 39 | 40 | @Before 41 | public void setup() { 42 | this.chatRepository = EasyMock.createMock(ChatRepository.class); 43 | this.mockMvc = standaloneSetup(new ChatController(this.chatRepository)).build(); 44 | } 45 | 46 | @Test 47 | public void getMessages() throws Exception { 48 | List messages = Arrays.asList("a", "b", "c"); 49 | expect(this.chatRepository.getMessages(9)).andReturn(messages); 50 | replay(this.chatRepository); 51 | 52 | this.mockMvc.perform(get("/mvc/chat").param("messageIndex", "9")) 53 | .andExpect(status().isOk()) 54 | .andExpect(request().asyncStarted()) 55 | .andExpect(request().asyncResult(messages)); 56 | 57 | verify(this.chatRepository); 58 | } 59 | 60 | @Test 61 | public void getMessagesStartAsync() throws Exception { 62 | expect(this.chatRepository.getMessages(9)).andReturn(Arrays.asList()); 63 | replay(this.chatRepository); 64 | 65 | this.mockMvc.perform(get("/mvc/chat").param("messageIndex", "9")) 66 | .andExpect(request().asyncStarted()); 67 | 68 | verify(this.chatRepository); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/samples/async/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.async.config; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.converter.HttpMessageConverter; 9 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 10 | import org.springframework.web.servlet.ViewResolver; 11 | import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; 12 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 13 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 14 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 15 | import org.thymeleaf.spring5.SpringTemplateEngine; 16 | import org.thymeleaf.spring5.view.ThymeleafViewResolver; 17 | import org.thymeleaf.templateresolver.ServletContextTemplateResolver; 18 | import org.thymeleaf.templateresolver.ITemplateResolver; 19 | 20 | @Configuration 21 | @ComponentScan(basePackages = { "org.springframework.samples.async" }) 22 | public class WebMvcConfig extends WebMvcConfigurationSupport { 23 | 24 | @Override 25 | public void configureAsyncSupport(AsyncSupportConfigurer configurer) { 26 | configurer.setDefaultTimeout(30*1000L); 27 | } 28 | 29 | @Override 30 | protected void configureMessageConverters(List> converters) { 31 | converters.add(new MappingJackson2HttpMessageConverter()); 32 | } 33 | 34 | public void addViewControllers(ViewControllerRegistry registry) { 35 | registry.addViewController("/").setViewName("chat"); 36 | } 37 | 38 | @Override 39 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 40 | registry.addResourceHandler("/resources/**").addResourceLocations("resources/"); 41 | } 42 | 43 | @Bean 44 | public ViewResolver viewResolver() { 45 | ThymeleafViewResolver resolver = new ThymeleafViewResolver(); 46 | resolver.setTemplateEngine(templateEngine()); 47 | return resolver; 48 | } 49 | 50 | @Bean 51 | public SpringTemplateEngine templateEngine() { 52 | SpringTemplateEngine engine = new SpringTemplateEngine(); 53 | engine.setTemplateResolver(templateResolver()); 54 | return engine; 55 | } 56 | 57 | @Bean 58 | public ITemplateResolver templateResolver() { 59 | ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(getServletContext()); 60 | resolver.setPrefix("/WEB-INF/templates/"); 61 | resolver.setSuffix(".html"); 62 | resolver.setTemplateMode("HTML5"); 63 | resolver.setCacheable(false); 64 | return resolver; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.springframework.samples 5 | spring-mvc-chat 6 | war 7 | 1.0.0-SNAPSHOT 8 | Chat sample using the Spring MVC Servlet-based async support 9 | 10 | 11 | 5.3.5 12 | 13 | 14 | 15 | 16 | org.springframework 17 | spring-context 18 | ${org.springframework-version} 19 | 20 | 21 | org.springframework 22 | spring-web 23 | ${org.springframework-version} 24 | 25 | 26 | org.springframework 27 | spring-webmvc 28 | ${org.springframework-version} 29 | 30 | 31 | org.springframework 32 | spring-context-support 33 | ${org.springframework-version} 34 | 35 | 36 | org.springframework 37 | spring-tx 38 | ${org.springframework-version} 39 | 40 | 41 | javax.servlet 42 | javax.servlet-api 43 | 4.0.1 44 | provided 45 | 46 | 47 | javax.servlet.jsp 48 | jsp-api 49 | 2.2 50 | provided 51 | 52 | 53 | javax.servlet 54 | jstl 55 | 1.2 56 | 57 | 58 | org.thymeleaf 59 | thymeleaf-spring5 60 | 3.0.12.RELEASE 61 | 62 | 63 | org.slf4j 64 | slf4j-api 65 | 66 | 67 | 68 | 69 | com.fasterxml.jackson.core 70 | jackson-databind 71 | 2.12.0 72 | 73 | 74 | ch.qos.logback 75 | logback-classic 76 | 1.2.3 77 | runtime 78 | 79 | 80 | org.slf4j 81 | jcl-over-slf4j 82 | 1.7.30 83 | runtime 84 | 85 | 86 | org.springframework 87 | spring-test 88 | ${org.springframework-version} 89 | 90 | 91 | junit 92 | junit 93 | 4.13.2 94 | test 95 | 96 | 97 | org.easymock 98 | easymock 99 | 3.6 100 | test 101 | 102 | 103 | org.hamcrest 104 | hamcrest-library 105 | 2.1 106 | test 107 | 108 | 109 | 110 | 111 | 112 | SpringSource repository 113 | https://repo.springsource.org/milestone 114 | 115 | false 116 | 117 | 118 | true 119 | 120 | 121 | 122 | SpringSource snapshot repository 123 | https://repo.springsource.org/snapshot 124 | true 125 | false 126 | 127 | 128 | 129 | 130 | 131 | apache.snapshots 132 | https://repository.apache.org/content/groups/snapshots-group/ 133 | 134 | 135 | 136 | 137 | ${project.artifactId} 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-compiler-plugin 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-war-plugin 146 | 147 | false 148 | 149 | 150 | 151 | org.apache.maven.plugins 152 | maven-resources-plugin 153 | 154 | UTF-8 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-eclipse-plugin 160 | 161 | true 162 | false 163 | 2.0 164 | 165 | 166 | 167 | org.eclipse.jetty 168 | jetty-maven-plugin 169 | 9.4.35.v20201120 170 | 171 | 172 | /${project.artifactId} 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/webapp/resources/js/knockout-2.0.0.js: -------------------------------------------------------------------------------- 1 | // Knockout JavaScript library v2.0.0 2 | // (c) Steven Sanderson - http://knockoutjs.com/ 3 | // License: MIT (http://www.opensource.org/licenses/mit-license.php) 4 | 5 | (function(window,undefined){ 6 | function c(a){throw a;}var l=void 0,m=!0,o=null,p=!1,r=window.ko={};r.b=function(a,b){for(var d=a.split("."),e=window,f=0;f",b[0];);return 4r.a.k(e,a[b])&&e.push(a[b]);return e},ba:function(a,e){for(var a=a||[],b=[],f=0,d=a.length;fa.length?p:a.substring(0,e.length)===e},hb:function(a){for(var e=Array.prototype.slice.call(arguments,1),b="return ("+a+")",f=0;f",""]||!d.indexOf("",""]||(!d.indexOf("",""]||[0,"",""];a="ignored
"+ 25 | d[1]+a+d[2]+"
";for("function"==typeof window.innerShiv?b.appendChild(window.innerShiv(a)):b.innerHTML=a;d[0]--;)b=b.lastChild;b=r.a.X(b.lastChild.childNodes)}return b};r.a.Z=function(a,b){r.a.U(a);if(b!==o&&b!==l)if("string"!=typeof b&&(b=b.toString()),"undefined"!=typeof jQuery)jQuery(a).html(b);else for(var d=r.a.ma(b),e=0;e"},Ra:function(a,b){var h=d[a];h===l&&c(Error("Couldn't find any memo with ID "+ 27 | a+". Perhaps it's already been unmemoized."));try{return h.apply(o,b||[]),m}finally{delete d[a]}},Sa:function(a,f){var d=[];b(a,d);for(var g=0,i=d.length;gb;b++)a=a();return a})};r.toJSON=function(a){a=r.Pa(a);return r.a.qa(a)}})();r.b("ko.toJS",r.Pa);r.b("ko.toJSON",r.toJSON); 43 | r.h={q:function(a){return"OPTION"==a.tagName?a.__ko__hasDomDataOptionValue__===m?r.a.e.get(a,r.c.options.la):a.getAttribute("value"):"SELECT"==a.tagName?0<=a.selectedIndex?r.h.q(a.options[a.selectedIndex]):l:a.value},S:function(a,b){if("OPTION"==a.tagName)switch(typeof b){case "string":r.a.e.set(a,r.c.options.la,l);"__ko__hasDomDataOptionValue__"in a&&delete a.__ko__hasDomDataOptionValue__;a.value=b;break;default:r.a.e.set(a,r.c.options.la,b),a.__ko__hasDomDataOptionValue__=m,a.value="number"===typeof b? 44 | b:""}else if("SELECT"==a.tagName)for(var d=a.options.length-1;0<=d;d--){if(r.h.q(a.options[d])==b){a.selectedIndex=d;break}}else{if(b===o||b===l)b="";a.value=b}}};r.b("ko.selectExtensions",r.h);r.b("ko.selectExtensions.readValue",r.h.q);r.b("ko.selectExtensions.writeValue",r.h.S); 45 | r.j=function(){function a(a,e){for(var d=o;a!=d;)d=a,a=a.replace(b,function(a,b){return e[b]});return a}var b=/\@ko_token_(\d+)\@/g,d=/^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i,e=["true","false"];return{D:[],Y:function(b){var e=r.a.z(b);if(3>e.length)return[];"{"===e.charAt(0)&&(e=e.substring(1,e.length-1));for(var b=[],d=o,i,j=0;j$/: 50 | /^\s*ko\s+(.*\:.*)\s*$/,g=f?/^<\!--\s*\/ko\s*--\>$/:/^\s*\/ko\s*$/,i={ul:m,ol:m};r.f={C:{},childNodes:function(b){return a(b)?d(b):b.childNodes},ha:function(b){if(a(b))for(var b=r.f.childNodes(b),e=0,d=b.length;e"),p)}};r.c.uniqueName.Za=0; 70 | r.c.checked={init:function(a,b,d){r.a.s(a,"click",function(){var e;if("checkbox"==a.type)e=a.checked;else if("radio"==a.type&&a.checked)e=a.value;else return;var f=b();"checkbox"==a.type&&r.a.d(f)instanceof Array?(e=r.a.k(r.a.d(f),a.value),a.checked&&0>e?f.push(a.value):!a.checked&&0<=e&&f.splice(e,1)):r.P(f)?f()!==e&&f(e):(f=d(),f._ko_property_writers&&f._ko_property_writers.checked&&f._ko_property_writers.checked(e))});"radio"==a.type&&!a.name&&r.c.uniqueName.init(a,function(){return m})},update:function(a, 71 | b){var d=r.a.d(b());if("checkbox"==a.type)a.checked=d instanceof Array?0<=r.a.k(d,a.value):d;else if("radio"==a.type)a.checked=a.value==d}};r.c.attr={update:function(a,b){var d=r.a.d(b())||{},e;for(e in d)if("string"==typeof e){var f=r.a.d(d[e]);f===p||f===o||f===l?a.removeAttribute(e):a.setAttribute(e,f.toString())}}}; 72 | r.c.hasfocus={init:function(a,b,d){function e(a){var e=b();a!=r.a.d(e)&&(r.P(e)?e(a):(e=d(),e._ko_property_writers&&e._ko_property_writers.hasfocus&&e._ko_property_writers.hasfocus(a)))}r.a.s(a,"focus",function(){e(m)});r.a.s(a,"focusin",function(){e(m)});r.a.s(a,"blur",function(){e(p)});r.a.s(a,"focusout",function(){e(p)})},update:function(a,b){var d=r.a.d(b());d?a.focus():a.blur();r.a.sa(a,d?"focusin":"focusout")}}; 73 | r.c["with"]={o:function(a){return function(){var b=a();return{"if":b,data:b,templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c["with"].o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c["with"].o(b),d,e,f)}};r.j.D["with"]=p;r.f.C["with"]=m;r.c["if"]={o:function(a){return function(){return{"if":a(),templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c["if"].o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c["if"].o(b),d,e,f)}}; 74 | r.j.D["if"]=p;r.f.C["if"]=m;r.c.ifnot={o:function(a){return function(){return{ifnot:a(),templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c.ifnot.o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c.ifnot.o(b),d,e,f)}};r.j.D.ifnot=p;r.f.C.ifnot=m; 75 | r.c.foreach={o:function(a){return function(){var b=r.a.d(a());return!b||"number"==typeof b.length?{foreach:b,templateEngine:r.p.M}:{foreach:b.data,includeDestroyed:b.includeDestroyed,afterAdd:b.afterAdd,beforeRemove:b.beforeRemove,afterRender:b.afterRender,templateEngine:r.p.M}}},init:function(a,b){return r.c.template.init(a,r.c.foreach.o(b))},update:function(a,b,d,e,f){return r.c.template.update(a,r.c.foreach.o(b),d,e,f)}};r.j.D.foreach=p;r.f.C.foreach=m;r.b("ko.allowedVirtualElementBindings",r.f.C); 76 | r.t=function(){};r.t.prototype.renderTemplateSource=function(){c("Override renderTemplateSource")};r.t.prototype.createJavaScriptEvaluatorBlock=function(){c("Override createJavaScriptEvaluatorBlock")};r.t.prototype.makeTemplateSource=function(a){if("string"==typeof a){var b=document.getElementById(a);b||c(Error("Cannot find template with ID "+a));return new r.m.g(b)}if(1==a.nodeType||8==a.nodeType)return new r.m.I(a);c(Error("Unknown template type: "+a))}; 77 | r.t.prototype.renderTemplate=function(a,b,d){return this.renderTemplateSource(this.makeTemplateSource(a),b,d)};r.t.prototype.isTemplateRewritten=function(a){return this.allowTemplateRewriting===p?m:this.W&&this.W[a]?m:this.makeTemplateSource(a).data("isRewritten")};r.t.prototype.rewriteTemplate=function(a,b){var d=this.makeTemplateSource(a),e=b(d.text());d.text(e);d.data("isRewritten",m);if("string"==typeof a)this.W=this.W||{},this.W[a]=m};r.b("ko.templateEngine",r.t); 78 | r.$=function(){function a(a,b,d){for(var a=r.j.Y(a),g=r.j.D,i=0;i/g;return{gb:function(a,b){b.isTemplateRewritten(a)||b.rewriteTemplate(a,function(a){return r.$.ub(a,b)})},ub:function(e,f){return e.replace(b,function(b,e,d,j,k,n,t){return a(t,e,f)}).replace(d,function(b,e){return a(e,"<\!-- ko --\>",f)})},Ua:function(a){return r.r.ka(function(b,d){b.nextSibling&&r.xa(b.nextSibling,a,d)})}}}();r.b("ko.templateRewriting",r.$);r.b("ko.templateRewriting.applyMemoizedBindingsToNextSibling",r.$.Ua);r.m={};r.m.g=function(a){this.g=a}; 80 | r.m.g.prototype.text=function(){if(0==arguments.length)return"script"==this.g.tagName.toLowerCase()?this.g.text:this.g.innerHTML;var a=arguments[0];"script"==this.g.tagName.toLowerCase()?this.g.text=a:r.a.Z(this.g,a)};r.m.g.prototype.data=function(a){if(1===arguments.length)return r.a.e.get(this.g,"templateSourceData_"+a);r.a.e.set(this.g,"templateSourceData_"+a,arguments[1])};r.m.I=function(a){this.g=a};r.m.I.prototype=new r.m.g; 81 | r.m.I.prototype.text=function(){if(0==arguments.length)return r.a.e.get(this.g,"__ko_anon_template__");r.a.e.set(this.g,"__ko_anon_template__",arguments[0])};r.b("ko.templateSources",r.m);r.b("ko.templateSources.domElement",r.m.g);r.b("ko.templateSources.anonymousTemplate",r.m.I); 82 | (function(){function a(a,b,d){for(var g=0;node=a[g];g++)node.parentNode===b&&(1===node.nodeType||8===node.nodeType)&&d(node)}function b(a,b,h,g,i){var i=i||{},j=i.templateEngine||d;r.$.gb(h,j);h=j.renderTemplate(h,g,i);("number"!=typeof h.length||0a&&c(Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later."));var h=d.data("precompiled");h||(h=d.text()||"",h=jQuery.template(o,"{{ko_with $item.koBindingContext}}"+h+"{{/ko_with}}"),d.data("precompiled",h)); 95 | d=[e.$data];e=jQuery.extend({koBindingContext:e},f.templateOptions);e=jQuery.tmpl(h,d,e);e.appendTo(document.createElement("div"));jQuery.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+a+" })()) }}"};this.addTemplate=function(a,b){document.write("