RateLimiter With Spring MVC - java

Here my requirement is,
I've a REST API which takes request parameter, from this request we get appId, userid, and IP address(of the request coming from) and i need to rate limit 5 requests per second(for the given key combination).
I have this sample code which restricts the user to allow maximum 5 requests/second. it works fine when there is only one user sending more than 5 requests then, it allows only 5 request/second and for remaining request it throws error "Too many request". but when there is more than one user(say 5) and each user is sending more than 5 requests/second then it is not able to limit the rate for the users(here all the 5 users should be able to send 5 requests/second) ie, total 25 requests should be success and remaining request should throw error like "too many request".
please suggest me where I am missing.
import java.util.concurrent.ConcurrentMap;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
#Controller
public class HomeController {
private int CACHESIZE = 1000;
private int rateRequests = 5;
private ConcurrentMap<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(CACHESIZE).build().asMap();
private RateLimiter rateLimiter;
HomeController() {
rateLimiter = RateLimiter.create(rateRequests);
}
#RequestMapping(value = "/getData", method = RequestMethod.GET)
public String getData(HttpServletRequest request) {
if (!preCheck(request)) {
return null;
} else {
return "get userdata from data base";
}
}
private boolean preCheck(HttpServletRequest request) {
String key = request.getParameter("userId") + request.getParameter("applicationId") + request.getRemoteAddr();
RateLimiter rateLimiter = getRateLimiter();
if (cache.containsKey(key)) {
rateLimiter = (RateLimiter) cache.get(key);
} else {
cache.put(key, rateLimiter);
}
boolean allow = rateLimiter.tryAcquire();
if (!allow) {
System.out.println("Too many request");
}
return allow;
}
public RateLimiter getRateLimiter() {
return rateLimiter;
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class RateLimitTest {
public static void main(String[] args) {
RateLimitTest limitTest = new RateLimitTest();
limitTest.scheduleJob();
}
static ExecutorService executorService = Executors.newFixedThreadPool(10);
private void scheduleJob() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
#Override
public void run() {
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
#Override
public void run() {
// getUserData("user1");
getUserData(randomGen(5));
}
});
}
}
}, 0, 20, TimeUnit.SECONDS);
}
private String randomGen(int count) {
String value = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder builder = new StringBuilder();
while (count-- != 0) {
int charIndex = (int) (Math.random() * value.length());
builder.append(value.charAt(charIndex));
}
return builder.toString();
}
private String getUserData(String user) {
return "call rest api( /getData ) to get the user data";
}
}

I think it is because all requests share one rate limiter, you should create a rate limiter per key. try this :
private boolean preCheck(HttpServletRequest request) {
String key = request.getParameter("userId") + request.getParameter("applicationId") + request.getRemoteAddr();
RateLimiter rl = cache.get(key);
if(rl == null){
rl = RateLimiter.create(rateRequests);
RateLimiter old = cache.putIfAbsent(key , rl);
if(old !=null){
rl = old;
}
}
boolean allow = rl.tryAcquire();
if (!allow) {
System.out.println("Too many request");
}
return allow;
}
do not forget to remove the definition of property rateLimiter

Related

How to set continuation for CompletableFuture from it's callback conditionally?

Given the code below - how the readAllValuesFuture() method should be implemented?
Make a call and depending on the result of its Future:
either make the next call and pass the result of the previous one
or return an aggregated result of all previous calls.
package com.example.demo;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class TestFuture
{
#Test
public void testReadAllValues() throws Exception
{
counter = 0;
var list = readAllValues();
assert list.size() == 10;
assert counter == 11;
}
#Test
public void testReadAllValuesFuture() throws Exception
{
counter = 0;
var list = readAllValuesFuture().get();
assert list.size() == 10;
assert counter == 11;
}
// Synchronous logic - plain and simple
private List<Integer> readAllValues()
{
List<Integer> list = new ArrayList<>();
Integer res = makeFirstRequest();
while (res != null)
{
list.add(res);
res = makeNextRequest(res);
}
return list;
}
// Futures - how to implement it?
private CompletableFuture<List<Integer>> readAllValuesFuture()
{
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
private int counter = 0;
// Assuming this is external code which cannot be changed
private Integer makeFirstRequest()
{
counter++;
return 0;
}
// Assuming this is external code which cannot be changed
private Integer makeNextRequest(Integer prevValue)
{
counter++;
if (++prevValue < 10)
{
return prevValue;
}
return null;
}
// Assuming this is external code which cannot be changed
private CompletableFuture<Integer> makeFirstRequestFuture()
{
counter++;
return CompletableFuture.completedFuture(0);
}
// Assuming this is external code which cannot be changed
private CompletableFuture<Integer> makeNextRequestFuture(Integer prevValue)
{
counter++;
if (++prevValue < 10)
{
return CompletableFuture.completedFuture(prevValue);
}
return CompletableFuture.completedFuture(null);
}
}
Real-life use-case:
Implement listAllTables() from the example below using DynamoDbAsyncClient (DynamoDbAsyncClient.listTables() returns CompletableFuture<ListTablesResponse> instead of ListTablesResponse as DynamoDbClient.listTables() does):
https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-dynamodb-tables.html
One way to achieve this is via Stream.iterate:
var values = Stream.iterate(
makeFirstRequestFuture().get(),
Objects::nonNull,
previous -> {
try {
return makeNextRequestFuture(previous).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
).toList();

AWS Java SDK - launching an EC2 Spot instance with a public IP

The Java SDK docs don't cover launching a spot instance into a VPC with a Public IP: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/tutorial-spot-adv-java.html.
How to do that?
Here's a SSSCE using aws-java-sdk-ec2-1.11.487:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2ClientBuilder;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.InstanceNetworkInterfaceSpecification;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.LaunchSpecification;
import com.amazonaws.services.ec2.model.RequestSpotInstancesRequest;
import com.amazonaws.services.ec2.model.SpotInstanceRequest;
import com.amazonaws.services.ec2.model.Tag;
public class SpotLauncher
{
private static final int kInstances = 25;
private static final String kMaxPrice = "0.007";
private static final InstanceType kInstanceType = InstanceType.M3Medium;
private static final String kSubnet = "subnet-xxxx";
private static final String kAmi = "ami-xxxx";
private static final String kName = "spot";
private static final String kSecurityGroup2 = "sg-xxxx";
private static final String kSecurityGroup1 = "sg-yyyy";
public static void main(String[] args)
{
AmazonEC2 ec2 = AmazonEC2ClientBuilder.defaultClient();
RequestSpotInstancesRequest request = new RequestSpotInstancesRequest();
request.setSpotPrice(kMaxPrice); // max price we're willing to pay
request.setInstanceCount(kInstances);
LaunchSpecification launchSpecification = new LaunchSpecification();
launchSpecification.setImageId(kAmi);
launchSpecification.setInstanceType(kInstanceType);
launchSpecification.setKeyName("aws");
// security group IDs - don't add them, they're already added to the network spec
// launchSpecification.withAllSecurityGroups(new GroupIdentifier().withGroupId("sg-xxxx"), new GroupIdentifier().withGroupId("sg-yyyy"));
List<String> securityGroups = new ArrayList<String>();
securityGroups.add(kSecurityGroup1);
securityGroups.add(kSecurityGroup2);
InstanceNetworkInterfaceSpecification networkSpec = new InstanceNetworkInterfaceSpecification();
networkSpec.setDeviceIndex(0);
networkSpec.setSubnetId(kSubnet);
networkSpec.setGroups(securityGroups);
networkSpec.setAssociatePublicIpAddress(true);
List<InstanceNetworkInterfaceSpecification> nicWrapper = new ArrayList<InstanceNetworkInterfaceSpecification>();
nicWrapper.add(networkSpec);
// launchSpecification.setSubnetId("subnet-ccde4ce1"); // don't add this, it's already added to the network interface spec
launchSpecification.setNetworkInterfaces(nicWrapper);
// add the launch specifications to the request
request.setLaunchSpecification(launchSpecification);
// call the RequestSpotInstance API
ec2.requestSpotInstances(request);
while (!SetEc2Names(ec2))
{
Sleep(2000);
}
System.out.println("\nDONE.");
}
private static void Sleep(long aMillis)
{
try
{
Thread.sleep(aMillis);
}
catch (InterruptedException aEx)
{
aEx.printStackTrace();
}
}
private static boolean SetEc2Names(AmazonEC2 aEc2Client)
{
List<SpotInstanceRequest> requests = aEc2Client.describeSpotInstanceRequests().getSpotInstanceRequests();
Collections.sort(requests, GetCreatedDescComparator());
for (int i = 0; i < kInstances; i++)
{
SpotInstanceRequest request = requests.get(i);
if (request.getLaunchSpecification().getImageId().equals(kAmi))
{
System.out.println("request: " + request);
String instanceId = request.getInstanceId();
if (instanceId == null)
{
System.out.println("instance not launched yet, we don't have an id");
return false;
}
System.out.println("setting name for newly launched spot instance, id: " + instanceId);
AssignName(aEc2Client, request);
}
}
return true;
}
private static void AssignName(AmazonEC2 aEc2Client, SpotInstanceRequest aRequest)
{
String instanceId = aRequest.getInstanceId();
Tag tag = new Tag("Name", kName);
CreateTagsRequest tagRequest = new CreateTagsRequest();
List<String> instanceIds = new ArrayList<String>();
instanceIds.add(instanceId);
tagRequest.withResources(instanceIds);
List<Tag> tags = new ArrayList<Tag>();
tags.add(tag);
tagRequest.setTags(tags);
aEc2Client.createTags(tagRequest);
}
private static Comparator<SpotInstanceRequest> GetCreatedDescComparator()
{
return new Comparator<SpotInstanceRequest>()
{
#Override
public int compare(SpotInstanceRequest o1, SpotInstanceRequest o2)
{
return -1 * o1.getCreateTime().compareTo(o2.getCreateTime());
}
};
}
}

Global variables in Controller class are overridden with latest session opened

I have developed an application using spring-MVC and hibernate, which is having signup page . When user tries to signup , application sends an OTP to user mail and I have maintained this OTP sent by the application in controller class as global variable. So, here the problem is when two users are accessing at a time latest requested user otp is overriding the old one and because of this first user is unable to signup.
1 > Does spring maintain separate session for each user accessing the application? If no ?how to solve this problem?.
Please find below code.
controller class:
package com.uday;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
#Controller
public class ControllerSignUp_Login {
private final Login_DetailsDao dao;
private Login_Details ld = new Login_Details();
private String OtpMailed = "";
private MailSendTest mailSender;
private int chances = 4;
private String emailAdd;
public ControllerSignUp_Login(Login_DetailsDao login_DetailsDao, MailSendTest mailSender) {
this.dao = login_DetailsDao;
this.mailSender = mailSender;
}
#RequestMapping("/hello")
#Transactional
public String diaplay(#RequestParam("name") String name, #RequestParam("pass") String pass, Model m) {
if (dao.isLogoinSuccessfull(name, pass)) {
m.addAttribute("message", "Hello " + name + " You are successfully logged in");
return "Success";
} else {
m.addAttribute("message", "Cannot validate given details.Please try again");
return "login";
}
}
#RequestMapping("/SignUp")
public String redirect() {
System.out.println("ControllerSignUp_Login.display()");
chances = 4;
return "signup";
}
#RequestMapping("/login")
public String display() {
System.out.println("ControllerSignUp_Login.display()");
return "login";
}
#RequestMapping("/updateDetails")
#Transactional
public String display(HttpServletRequest req, Model M) {
String firstName = req.getParameter("firstName");
String lastName = req.getParameter("lastName");
String mobileNo = req.getParameter("mobileNo");
String address = req.getParameter("address");
String email = req.getParameter("email");
String password = req.getParameter("password");
if (checkLength(firstName) && checkLength(lastName) && checkLength(mobileNo) && checkLength(address)
&& checkLength(email) && checkLength(password)) {
ld.setFirstName(firstName);
ld.setLastName(lastName);
ld.setEmail(email);
ld.setAddress(address);
ld.setMobileNo(mobileNo);
ld.setPassword(password);
if (dao.validateMobileAndEmail(mobileNo, email)) {
doSendEmail(email);
M.addAttribute("cMessage", false);
return "ValidationPage";
} else {
M.addAttribute("message", "MobileNo/Email is already registered");
return "signup";
}
} else {
M.addAttribute("message", "SignUp Failed !! All details are mandatory.");
return "signup";
}
}
#RequestMapping("/Home")
public String displayy() {
return "Home";
}
#RequestMapping("/")
public String display1() {
return "login";
}
public boolean checkLength(String s) {
if (s != null && s.length() > 0) {
return true;
}
return false;
}
#Transactional
#RequestMapping("/submitToDB")
public String submitToDataBase(HttpServletRequest req, Model M) {
String otp = req.getParameter("otp");
System.out.println("ControllerSignUp_Login.submitToDataBase()" + otp);
if (OtpMailed.equals(otp)) {
dao.saveEmployee(ld);
chances = 4;
M.addAttribute("message", "SignUp Successfull !! Thank You");
M.addAttribute("displayLogin", true);
return "Success";
} else {
if (chances != 1) {
chances = chances - 1;
M.addAttribute("message", chances + " Chances Left");
return "ValidationPage";
} else {
chances = 4;
M.addAttribute("message", "Authorization failed");
return "signup";
}
}
}
#RequestMapping("/validate")
public String validateOtp() {
return "Success";
}
public String generateOtp() {
String otp = "";
for (int i = 0; i < 4; i++) {
Double d = Math.ceil(Math.random() * 10);
int value = d.intValue();
if (value == 10) {
otp = otp + 1;
} else {
otp = otp + value;
}
}
return otp;
}
public void doSendEmail(String mail) {
try {
this.emailAdd = mail;
String recipientAddress = mail;
String subject = "One Time Verification <Uday>";
String otpGenerated = generateOtp();
this.OtpMailed = otpGenerated;
String message = "Please use this OTP " + otpGenerated + " to signup. ";
mailSender.Send("xxxxxxxxx#gmail.com", "lxrxnxnhmyclvzxs", recipientAddress, subject, message);
} catch (AddressException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
#RequestMapping("/resend")
public String resend(Model m) {
doSendEmail(this.emailAdd);
m.addAttribute("message", chances + " Chances Left");
return "ValidationPage";
}
}
Spring REST Controllers are always scoped as singletons (#Controller annotation implies that). You're NOT supposed to re-use private class level variables/fields on method invocation.
If you have global concerns you need to manage/visit outside the scope of a single request, please be sure to separate them to different classes.
Otherwise, the entire modification scope that takes place inside #RequestMapping annotated methods should be method/function-local.

Unexpected query result inside each executorservice thread

I am pulling data in batches (chunkSize=1000) for that I have implemented executor Service. In the below loop I am calculating the firstResult and sending to executorService to fetch data and insert into mongodb
for(int i = 1; i <= numOfBatches; i++){
int firstResult = (i -1) * chunkSize;
explicitAudienceCreationExecutorService.fetchFromMartAndInsertIntoMongo(firstResult,chunkSize,query,promotion,
filterKeywords,audienceFilterName,programId,counttoReturn.get(0).intValue());
}
This is my runnable task which is giving unexpected result while executing query
For ex: when I am executing the code without any loop and directly pass firstResult as 302000 it prints in log
firstResult 302000 queryResultSize 1000
But when I do this in loop I saw this in logs. This happens for several values.
firstResult 302000 queryResultSize 899
package com.loylty.campaign.com.loylty.campaign.service;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import static com.loylty.campaign.com.loylty.campaign.config.MongoTenantTemplate.tenantTemplates;
#Service
public class ExplicitAudienceCreationExecutorService {
static int classCount = 0;
#Autowired
CampaignExecutorService campaignExecutorService;
#Autowired
MongoTemplate mongoTemplate;
#Autowired
AudienceWithCriteria audienceWithCriteria;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public void fetchFromMartAndInsertIntoMongo(int fr, int cs, TypedQuery<Object[]> qr, Promotion promotion,
FilterKeywords filterKeywords, String audienceFilterName, String programId, int queryrernCont) {
final int firstResult = fr;
final int chunkSize = cs;
final TypedQuery<Object[]> query = qr;
campaignExecutorService.dotask(new Runnable() {
#Override
public void run() {
mongoTemplate = tenantTemplates.get(programId);
final List<Object[]> toReturn = query.setFirstResult(firstResult).setMaxResults(chunkSize).getResultList();
classCount++;
System.out.println("classCount "+ classCount);
logger.info("firstResult "+ firstResult + " queryResultSize " + toReturn.size() );
if (toReturn != null || toReturn.size() > 0) {
List<TGAudience> tgAudienceList = new ArrayList<>();
for (Object[] tuple : toReturn) {
HashMap<String, Object> queryResponseTuple = new HashMap<>();
int index = 0;
for (RequiredKeys selectProperty : promotion.getRequiredKeys()) {
queryResponseTuple.put(filterKeywords.matcher(selectProperty.getKeyName()).iterator().next(), tuple[index++]);
}
if (null != promotion.getAggregation() && promotion.getAggregation().size() > 0) {
for (Aggregation aggregations : promotion.getAggregation()) {
queryResponseTuple.put(filterKeywords.matcher(aggregations.getAggregateOn()).iterator().next() + "_" + aggregations.getAggregateStrategy().name(), tuple[index++]);
}
}
TGAudience tgAudience1 = new TGAudience();
String stringToConvert = String.valueOf(queryResponseTuple.get("CUSTOMER_MOBILE"));
tgAudience1.setMobile(stringToConvert);
tgAudience1.setCustomerId(String.valueOf(queryResponseTuple.get("CUSTOMER_CUSTOMER_ID")));
tgAudienceList.add(tgAudience1);
}
System.out.println("tgAudienceList "+ tgAudienceList.size());
mongoTemplate.insert(tgAudienceList, audienceFilterName);
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
}
}
CampaignExecutorService
package com.loylty.campaign.com.loylty.campaign.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.*;
#Service
public class CampaignExecutorService {
private ExecutorService executorService = Executors.newFixedThreadPool(100);
public void dotask(Runnable runnable){
executorService.submit(runnable);
}
}

Reader-Writer and priority

I'm working on a system Reader-Writer with the Java threads, and it must be prioritized : the reader has the priority over the writer.
I wrote a source-code, which compiles and can be executed without any problem. But I would want to be really sure it's correct.
Can you tell me if you see some errors ?
Well, first I have to explain you the aim of my little program. At regular intervals, a message is displayed to the user. The latter can modify it, and change its display delay (the "interval of time"). A message is identified by an ID.
So if the user type : 0 \n Hello \n 2, it means the message n°0 is now "Hello" and will be displayed every 2 seconds.
Each message is taken care by a thread. I have to use semaphores.
SOURCE-CODES.
The reader :
public class Lecteur extends Thread {
private Message<String> message;
public Lecteur(Message<String> message) {
this.message = message;
}
public void run() {
try {
while(true) {
System.out.println(message.getContent());
int time = message.getRefresh_time()*1000;
Thread.sleep(time);
}
} catch(InterruptedException e) {
System.out.println(e);
}
}
}
The writer :
import java.util.HashMap;
import java.util.Scanner;
public class GestionnaireSaisie extends Thread {
private HashMap<Integer, Message<String>> messages;
public GestionnaireSaisie(HashMap<Integer, Message<String>> messages) {
this.messages = messages;
}
public void run() {
Scanner scanner = new Scanner(System.in);
int id;
String content;
int time_refresh;
while (true) {
id = scanner.nextInt();
content = scanner.next();
time_refresh = scanner.nextInt();
Message<String> found_msg = messages.get(id);
found_msg.setContent(content);
found_msg.setRefreshTime(time_refresh);
}
}
}
And the most interesting class, the shared object which contains shared data :
import java.util.concurrent.Semaphore;
public class Message<T> {
private static int maxid;
private int id;
private T content;
private int refresh_time;
public Semaphore mutex_content, mutex_refresh_time, semNbl;
public static int nbL = 0;
public int getId() {
return id;
}
public Message(T content, int refresh_time, Semaphore mutex_content, Semaphore mutex_refresh_time, Semaphore semNbl) {
id = maxid;
Message.maxid++;
this.content = content;
this.refresh_time = refresh_time;
this.mutex_content = mutex_content;
this.mutex_refresh_time = mutex_refresh_time;
this.semNbl = semNbl;
}
// <!-- CONTENT
public void setContent(T content) {
try {
mutex_content.acquire();
this.content = content;
mutex_content.release();
} catch(InterruptedException e) {
System.out.println(e);
}
}
public T getContent() {
T ret = null;
try {
semNbl.acquire();
Message.nbL++;
if(Message.nbL == 1) {
mutex_content.acquire();
}
semNbl.release();
ret = content;
semNbl.acquire();
Message.nbL--;
if(Message.nbL == 0) {
mutex_content.release();
}
semNbl.release();
} catch(InterruptedException e) {
System.out.println(e);
}
return ret;
}
// CONTENT -->
// <!-- REFRESH TIME
public void setRefreshTime(int refresh_time) {
try {
mutex_refresh_time.acquire();
this.refresh_time = refresh_time;
mutex_refresh_time.release();
} catch(InterruptedException e) {
System.out.println(e);
}
}
public int getRefresh_time() {
int ret = 0;
try {
semNbl.acquire();
Message.nbL++;
if(Message.nbL == 1) {
mutex_refresh_time.acquire();
}
semNbl.release();
ret = refresh_time;
semNbl.acquire();
Message.nbL--;
if(Message.nbL == 0) {
mutex_refresh_time.release();
}
semNbl.release();
} catch(InterruptedException e) {
System.out.println(e);
}
return ret;
}
// REFRESH TIME -->
}
Here some code to test it :
Semaphore mutex_content = new Semaphore(1);
Semaphore mutex_refresh_time = new Semaphore(1);
Semaphore semNbl = new Semaphore(1);
Message<String> message1 = new Message<String>("Bonjour le monde !", 5, mutex_content, mutex_refresh_time, semNbl);
new Lecteur(message1).start();
HashMap<Integer, Message<String>> messages = new HashMap<Integer, Message<String>>();
messages.put(message1.getId(), message1);
GestionnaireSaisie gs = new GestionnaireSaisie(messages);
gs.start();

Categories

Resources