added @Async annotation, AsyncExecutionInterceptor, AsyncAnnotationAdvisor

This commit is contained in:
Juergen Hoeller
2009-02-10 11:24:05 +00:00
parent 21a442b253
commit 777a104d48
15 changed files with 752 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,16 @@
package example.scannable;
import java.util.concurrent.Future;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.AsyncResult;
/**
* @author Mark Fisher
* @author Juergen Hoeller
*/
public class AutowiredQualifierFooService implements FooService {
@@ -44,6 +47,10 @@ public class AutowiredQualifierFooService implements FooService {
return this.fooDao.findFoo(id);
}
public Future<String> asyncFoo(int id) {
return new AsyncResult<String>(this.fooDao.findFoo(id));
}
public boolean isInitCalled() {
return this.initCalled;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
package example.scannable;
import java.util.concurrent.Future;
import org.springframework.scheduling.annotation.Async;
/**
* @author Mark Fisher
* @author Juergen Hoeller
@@ -23,7 +27,10 @@ package example.scannable;
public interface FooService {
String foo(int id);
@Async
Future<String> asyncFoo(int id);
boolean isInitCalled();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
package example.scannable;
import java.util.List;
import java.util.concurrent.Future;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.BeanFactory;
@@ -30,7 +30,9 @@ import org.springframework.context.MessageSource;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
/**
* @author Mark Fisher
@@ -73,6 +75,12 @@ public class FooServiceImpl implements FooService {
return this.fooDao.findFoo(id);
}
public Future<String> asyncFoo(int id) {
System.out.println(Thread.currentThread().getName());
Assert.state(ServiceInvocationCounter.getThreadLocalCount() != null, "Thread-local counter not exposed");
return new AsyncResult<String>(this.fooDao.findFoo(id));
}
public boolean isInitCalled() {
return this.initCalled;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,10 @@
package example.scannable;
import java.util.concurrent.Future;
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.annotation.AsyncResult;
/**
* @author Mark Fisher
@@ -29,6 +32,10 @@ public class ScopedProxyTestBean implements FooService {
return "bar";
}
public Future<String> asyncFoo(int id) {
return new AsyncResult<String>("bar");
}
public boolean isInitCalled() {
return false;
}

View File

@@ -31,16 +31,25 @@ public class ServiceInvocationCounter {
private int useCount;
private static final ThreadLocal<Integer> threadLocalCount = new ThreadLocal<Integer>();
@Pointcut("execution(* example.scannable.FooService+.*(..))")
public void serviceExecution() {}
@Before("serviceExecution()")
public void countUse() {
this.useCount++;
this.threadLocalCount.set(this.useCount);
System.out.println("");
}
public int getCount() {
return this.useCount;
}
public static Integer getThreadLocalCount() {
return threadLocalCount.get();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,13 @@
package org.springframework.context.annotation;
import static org.junit.Assert.*;
import example.scannable.CustomComponent;
import example.scannable.FooService;
import example.scannable.FooServiceImpl;
import example.scannable.NamedStubDao;
import example.scannable.StubFooDao;
import org.aspectj.lang.annotation.Aspect;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.beans.TestBean;
@@ -35,12 +39,6 @@ import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.stereotype.Component;
import example.scannable.CustomComponent;
import example.scannable.FooService;
import example.scannable.FooServiceImpl;
import example.scannable.NamedStubDao;
import example.scannable.StubFooDao;
/**
* @author Mark Fisher
* @author Juergen Hoeller
@@ -64,8 +62,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean("thoreau"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -119,8 +117,8 @@ public class ClassPathBeanDefinitionScannerTests {
}
catch (IllegalStateException ex) {
// expected
assertTrue(ex.getMessage().indexOf("stubFooDao") != -1);
assertTrue(ex.getMessage().indexOf(StubFooDao.class.getName()) != -1);
assertTrue(ex.getMessage().contains("stubFooDao"));
assertTrue(ex.getMessage().contains(StubFooDao.class.getName()));
}
}
@@ -194,8 +192,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertEquals(4, beanCount);
assertTrue(context.containsBean("messageBean"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -212,8 +210,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertFalse(context.containsBean("myNamedComponent"));
assertFalse(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -230,8 +228,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -247,8 +245,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -264,8 +262,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -282,8 +280,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertFalse(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertFalse(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertFalse(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertFalse(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -300,8 +298,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test
@@ -318,8 +316,8 @@ public class ClassPathBeanDefinitionScannerTests {
assertTrue(context.containsBean("myNamedComponent"));
assertTrue(context.containsBean("myNamedDao"));
assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,38 +16,44 @@
package org.springframework.context.annotation;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import example.scannable.FooService;
import example.scannable.ServiceInvocationCounter;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Mark Fisher
* @author Juergen Hoeller
*/
public class SimpleConfigTests {
@Test
public void testFooService() throws Exception {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass());
FooService fooService = (FooService) ctx.getBean("fooServiceImpl");
ServiceInvocationCounter serviceInvocationCounter = (ServiceInvocationCounter) ctx.getBean("serviceInvocationCounter");
FooService fooService = ctx.getBean("fooServiceImpl", FooService.class);
ServiceInvocationCounter serviceInvocationCounter = ctx.getBean("serviceInvocationCounter", ServiceInvocationCounter.class);
String value = fooService.foo(1);
assertEquals("bar", value);
assertEquals(1, serviceInvocationCounter.getCount());
fooService.foo(1);
Future future = fooService.asyncFoo(1);
assertTrue(future instanceof FutureTask);
assertEquals("bar", future.get());
assertEquals(2, serviceInvocationCounter.getCount());
fooService.foo(1);
assertEquals(3, serviceInvocationCounter.getCount());
}
public String[] getConfigLocations() {
return new String[] {"simpleConfigTests.xml"};
}
}

View File

@@ -8,13 +8,21 @@
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:annotation-config/>
<aop:aspectj-autoproxy/>
<bean class="org.springframework.scheduling.annotation.AsyncAnnotationAdvisor">
<!--
<property name="taskExecutor">
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"/>
</property>
-->
</bean>
<bean id="fooServiceImpl" class="example.scannable.FooServiceImpl"/>
<bean id="serviceInvocationCounter" class="example.scannable.ServiceInvocationCounter"/>
<bean class="example.scannable.StubFooDao"/>
</beans>

View File

@@ -0,0 +1,261 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.annotation;
import java.util.concurrent.Future;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.scheduling.annotation.AsyncResult;
/**
* @author Juergen Hoeller
*/
public class AsyncExecutionTests {
private static String originalThreadName;
private static int listenerCalled = 0;
private static int listenerConstructed = 0;
@Test
public void asyncMethods() throws Exception {
originalThreadName = Thread.currentThread().getName();
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodBean.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
AsyncMethodBean asyncTest = context.getBean("asyncTest", AsyncMethodBean.class);
asyncTest.doNothing(5);
asyncTest.doSomething(10);
Future<String> future = asyncTest.returnSomething(20);
assertEquals("20", future.get());
}
@Test
public void asyncClass() throws Exception {
originalThreadName = Thread.currentThread().getName();
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassBean.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
AsyncClassBean asyncTest = context.getBean("asyncTest", AsyncClassBean.class);
asyncTest.doSomething(10);
Future<String> future = asyncTest.returnSomething(20);
assertEquals("20", future.get());
}
@Test
public void asyncInterface() throws Exception {
originalThreadName = Thread.currentThread().getName();
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncInterfaceBean.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
AsyncInterface asyncTest = context.getBean("asyncTest", AsyncInterface.class);
asyncTest.doSomething(10);
Future<String> future = asyncTest.returnSomething(20);
assertEquals("20", future.get());
}
@Test
public void asyncMethodsInInterface() throws Exception {
originalThreadName = Thread.currentThread().getName();
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodsInterfaceBean.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
AsyncMethodsInterface asyncTest = context.getBean("asyncTest", AsyncMethodsInterface.class);
asyncTest.doNothing(5);
asyncTest.doSomething(10);
Future<String> future = asyncTest.returnSomething(20);
assertEquals("20", future.get());
}
@Test
public void asyncMethodListener() throws Exception {
originalThreadName = Thread.currentThread().getName();
listenerCalled = 0;
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodListener.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
Thread.sleep(1000);
assertEquals(1, listenerCalled);
}
@Test
public void asyncClassListener() throws Exception {
originalThreadName = Thread.currentThread().getName();
listenerCalled = 0;
listenerConstructed = 0;
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassListener.class));
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
context.close();
Thread.sleep(1000);
assertEquals(2, listenerCalled);
assertEquals(1, listenerConstructed);
}
@Test
public void asyncPrototypeClassListener() throws Exception {
originalThreadName = Thread.currentThread().getName();
listenerCalled = 0;
listenerConstructed = 0;
GenericApplicationContext context = new GenericApplicationContext();
RootBeanDefinition listenerDef = new RootBeanDefinition(AsyncClassListener.class);
listenerDef.setScope(RootBeanDefinition.SCOPE_PROTOTYPE);
context.registerBeanDefinition("asyncTest", listenerDef);
context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class));
context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class));
context.refresh();
context.close();
Thread.sleep(1000);
assertEquals(2, listenerCalled);
assertEquals(2, listenerConstructed);
}
public static class AsyncMethodBean {
public void doNothing(int i) {
assertTrue(Thread.currentThread().getName().equals(originalThreadName));
}
@Async
public void doSomething(int i) {
System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
@Async
public Future<String> returnSomething(int i) {
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
return new AsyncResult<String>(Integer.toString(i));
}
}
@Async
public static class AsyncClassBean {
public void doSomething(int i) {
System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
public Future<String> returnSomething(int i) {
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
return new AsyncResult<String>(Integer.toString(i));
}
}
@Async
public interface AsyncInterface {
void doSomething(int i);
Future<String> returnSomething(int i);
}
public static class AsyncInterfaceBean implements AsyncInterface {
public void doSomething(int i) {
System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
public Future<String> returnSomething(int i) {
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
return new AsyncResult<String>(Integer.toString(i));
}
}
public interface AsyncMethodsInterface {
void doNothing(int i);
@Async
void doSomething(int i);
@Async
Future<String> returnSomething(int i);
}
public static class AsyncMethodsInterfaceBean implements AsyncMethodsInterface {
public void doNothing(int i) {
assertTrue(Thread.currentThread().getName().equals(originalThreadName));
}
public void doSomething(int i) {
System.out.println(Thread.currentThread().getName() + ": " + i);
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
public Future<String> returnSomething(int i) {
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
return new AsyncResult<String>(Integer.toString(i));
}
}
public static class AsyncMethodListener implements ApplicationListener {
@Async
public void onApplicationEvent(ApplicationEvent event) {
listenerCalled++;
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
}
@Async
public static class AsyncClassListener implements ApplicationListener {
public AsyncClassListener() {
listenerConstructed++;
}
public void onApplicationEvent(ApplicationEvent event) {
listenerCalled++;
assertTrue(!Thread.currentThread().getName().equals(originalThreadName));
}
}
}