Below is a generalized version of my Spring RestController implementation. It's purpose is to respond to HTTP requests at a specific base URI ("/rest/my/uri/to/resource") with a resource based on the URI ID input. It works fine, however due to the structure of the data it returns I have had to add an optional date param. I have the controller calculate today's date and use that in my database query when the user does not supply one in the URI.
My question to you is if I use the todaySqlDate variable for each response where the user does not supply a date as I am below, will it recalculate the date each time it responds to a request? Obviously if the user inputs a date like
/rest/my/uri/to/resource/identifier/666?date=2016-03-15
this will not be an issue. My concern is that when the date is not included, a la
/rest/my/uri/to/resource/identifier/666
it will use the default date. Will the default date stop being current if the service is left running for more than 24 hours?
#RestController
#RequestMapping(value = "/rest/my/uri/to/resource")
public class ResourceController
{
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceController.class);
#Autowired
private ResourceService resourceService;
public String todaySqlDate = getTodaySqlDate();
#RequestMapping(value = "/identifier/{id}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public Resource getResource(#PathVariable("id") String id,
#RequestParam(value="date", defaultValue="", required=false) String resourceDate)
throws InvalidParameterException, DataNotFoundException, Exception
{
LOGGER.trace("In ResourceController.getResouce() with {}", id);
try
{
if(!isValidIdentifier(id))
{
throw new InvalidParameterException("id is not valid: " + id);
}
if(resourceDate.isEmpty())
{
resourceDate = todaySqlDate;
}
else if(!isValidSqlDateString(resourceDate))
{
throw new InvalidParameterException("resourceDate is present but not valid: " + resourceDate);
}
ResourceList resourceList = resourceService.getResource(id, resourceDate);
if (resourceList == null)
{
LOGGER.trace("No resources found for given input");
throw new DataNotFoundException("ResourceList for " + id + " not found");
}
return resourceList;
}
catch (Exception e)
{
LOGGER.error(e.getMessage(), e);
throw e;
}
}
public String getTodaySqlDate()
{
java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
return sqlDate.toString();
}
}
Yes, every new request will be handled by a new separate instance (thread) and hence it will re-calculate the date every time.
You can have a look at Spring/REST Documentation for more information.
Useful Link:
How are Threads allocated to handle Servlet request?
Related
I have two method in different controller class
#RequestMapping(value="/addHari")
public String menuAddHari(HttpServletRequest request,#RequestParam(value="addchkmerah",required = false)String status, Model model) throws ParseException {
Date hariKerja = new SimpleDateFormat("yyyy-MM-dd").parse(request.getParameter("addharikerja"));
java.sql.Date workDay = new java.sql.Date(hariKerja.getTime());
String keterangan = request.getParameter("addkolomket");
Time jamMasuk = hs.getTime(request.getParameter("addjammasuk"));
Time jamKeluar = hs.getTime(request.getParameter("addjampulang"));
month = request.getParameter("addBulan");
HariKerjaModel hm = new HariKerjaModel();
hm.setKeterangan(keterangan);
hm.setTanggal(workDay);
hm.setMerahBukan(status);
hm.setJamMasuk(jamMasuk);
hm.setJamKeluar(jamKeluar);
hs.saveDate(hm);
model.addAttribute("month", month);
return "redirect:/hariKerja";
}
and
#RequestMapping(value="/hariKerja")
public String menuHariKerja(Model model,HttpServletRequest request) {
List<HariKerjaModel> hk = new ArrayList<>();
String month = request.getParameter("bulansrc");
HariKerjaController h = new HariKerjaController();
String x=h.getMonth();
if(month==null) {
String month2 = (String) model.getAttribute("month");
}
hk = hs.readHariKerja(month);
model.addAttribute("ListHariKerjaModel", hk);
model.addAttribute("valueSelected",month);
return "hariKerja";
}
how to pass value variable "month" in method "menuAddHari" to variable "month2" in method "menuHariKerja" ?
You can achieve your requirement while redirect your request with path or query or body containing entity or fields which are url-form-encoded.
return StringBuffer("redirect:/your/other/controller/method/")
.append(param1).append("/").append(param2);
And you will be able to handle the request in your other controller with :
#GetMapping(value="/your/other/controller/method/{param1}/{param2}")
public String otherMethodInOtherController(#PathVariable("paramName1") String param1, #PathVariable("paramName2") String param2) {
// your 2sd controller code here...
return "your-view-name";
}
Of course, if you have many parameters to send, it will be better to set a ResponseEntity with BodyInserters inside the body of your request before to send it to your 2sd controller.
And don't forget not to use GET methods to forward sensitive user|system datas.
I have this database data as below (ES 7.xx) version
{
"id":"1234",
"expirationDate":"17343234234",
"paths":"http:localhost:9090",
"work":"software dev",
"family":{
"baba":"jams",
"mother":"ela"
}
},
{
"id":"00021",
"expirationDate":"0123234",
"paths":"http:localhost:8080",
"work":"software engi",
"family":{
"baba":"stev",
"mother":"hela"
}
}
how can i update the entity which its expirationDate smaller than current Time? to be the current time for example:
the id 00021 is expired because its expiration date is smaller than today then it should updated to current time.
something like void updateExpiredEntity(List<ids> ids,Long currentTime) suing void bulkUpdate(List<UpdateQuery> queries, BulkOptions bulkOptions, IndexCoordinates index);
Please provide me some code implementation
is it correct like this?
public void update(UUID id,Long currentDate) {
UpdateQuery updateQuery = UpdateQuery.builder(id.toString()).withRouting("expirationDate=currentDate")
.build();
elasticsearchTemplate.bulkUpdate(List.of(updateQuery), IndexCoordinates.of("index"));
}
}
If you are using Elasticsearch 7.xx, I will assume that you have use Spring Data Elasticsearch version 4.0.x that comes with Spring boot 2.3.x. Since it's the version that support Elasticsearch 7.xx.
There're many things that have change in this Spring Data Elasticsearch version. Update document by query is one of them. Unlike before that we autowired ElasticsearchTemplate, we now have to use ElasticsearchRestTemplate and RestHighLevelClient instead.
In your case if you might want to use RestHighLevelClient to update document by query. Assume that you stored expirationDate as number mapping type in seconds unit then the code that you have asked for should look like this.
public class ElasticsearchService {
#Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
#Autowired
private RestHighLevelClient highLevelClient;
public void updateExpireDateDemo() throws IOException {
String indexName = "test";
Date currentDate = new Date();
Long seconds = (Long) (currentDate.getTime() / 1000);
UpdateByQueryRequest request = new UpdateByQueryRequest(indexName);
request.setQuery(new RangeQueryBuilder("expirationDate").lte(seconds));
Script updateScript = new Script(
ScriptType.INLINE, "painless",
"ctx._source.expirationDate=" + seconds + ";",
Collections.emptyMap());
request.setScript(updateScript);
highLevelClient.updateByQuery(request, RequestOptions.DEFAULT);
}
}
I'm not quite get why you really need to use the bulkUpdate but if that's the case then. You have to query the record that need to be update from Elasticsearch to get and id of each document first. Then you can update with list of UpdateQuery. So your code will look like this.
#Service
public class ElasticsearchService {
#Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public void updateExpireDateByBulkDemo() throws IOException {
String indexName = "test";
Date currentDate = new Date();
Long seconds = (Long) (currentDate.getTime() / 1000);
List<UpdateQuery> updateList = new ArrayList();
RangeQueryBuilder expireQuery = new RangeQueryBuilder("expirationDate").lte(seconds);
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(expireQuery).build();
SearchHits<Data> searchResult = elasticsearchRestTemplate.search(query, Data.class, IndexCoordinates.of(indexName));
for (SearchHit<Data> hit : searchResult.getSearchHits()) {
String elasticsearchDocumentId = hit.getId();
updateList.add(UpdateQuery.builder(elasticsearchDocumentId).withScript("ctx._source.expirationDate=" + seconds + ";").build());
}
if (updateList.size() > 0) {
elasticsearchRestTemplate.bulkUpdate(updateList, IndexCoordinates.of(indexName));
}
}
}
However, this only update first page of the search result. If you require to update every record matched your query then you have to use searchScroll method in oder to get every document id instead.
I´m build a REST service through Spring and Swagger with CMIS Protocol in Maven. My services works well until the moment when I´m doing simultaneous calls through Jmeter to stress the system. Context: the service get an ID from an ID creator to generate a node in Alfresco. The id cannot be repeated in the system. this id creator take the last created node and sum +1. I tried with Thread.sleep and TimeUnit. The Jmeter Answer in 10 calls in one sec that the Node was created 10 times with the same ID.
Extract of current code: Controller:
#ApiOperation(value = "Crea un Tipo de documento")
#RequestMapping(value = "/create2", method = RequestMethod.POST)
ResponseMessage createTypeDocument2(#RequestParam String description) throws InterruptedException {
return typeDocumentService.createTypeDocument(description);
}
Service:
#Transactional
public ResponseMessage createTypeDocument(String description) throws InterruptedException {
Session session = obtieneSesion();
int id = this.idCreator2();
int aux = 0;
if(searchDocuments(id)==null) {
aux=0;
}else {
aux = Integer.parseInt(searchDocuments(id).getProperty("bc:id").getValueAsString());
}
ResponseMessage rm = new ResponseMessage();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(500));
Thread.sleep(1000);
if(id != aux ) {
SimpleDateFormat sdf = new SimpleDateFormat("SSS");
Date date = new Date();
String nombre = sdf.format(date.getTime());
DocumentTypeDTO docFilter = new DocumentTypeDTO();
Folder root = (Folder) session.getObjectByPath("/DataList-BCH");
HashMap<String, Object> metadata = new HashMap<String, Object>();
metadata.put(PropertyIds.OBJECT_TYPE_ID, "***************");
metadata.put(PropertyIds.NAME, nombre);
metadata.put("bc:id", id);
metadata.put("bc:available", activo);
metadata.put("bc:description", description);
Document newDoc = root.createDocument(metadata, null, null);
docFilter.setId( Integer.parseInt(newDoc.getProperty("bc:id").getValueAsString()));
docFilter.setDescription(newDoc.getProperty("bc:description").getValueAsString());
docFilter.setUuid(newDoc.getId().replaceAll(";1.0", "").replace("workspace://SpacesStore/", ""));
rm.setMensaje("Exitoso");
rm.setCodigo(200);
rm.setObjeto(docFilter);
return rm;
}else {
rm.setCodigo(-1);
rm.setMensaje("La ID ya existe");
return rm;
}
}
SearchDocument method if the object is not found return null. I try multiple options without results, for that reason I need your help to resolve it. Thanks in Advice
RESOLVED IN STACKOVERFLOW IN SPANISH:
RESOLVED
In my REST API Controller with #PathVariable("timestamp) I have to validate that timestamp format is complaint with ISO 8601 standard: eg. 2016-12-02T18:25:43.511Z.
#RequestMapping("/v3/testMe/{timestamp}")
public class TestController {
private static final String HARDCODED_TEST_VALUE = "{\n\t\"X\": \"01\",\n\t\"Y\": \"0.2\"\n}";
#ApiOperation(nickname = "getTestMe", value = "Return TestMe value", httpMethod = "GET",
authorizations = {#Authorization(value = OAUTH2,
scopes = {#AuthorizationScope(scope = DEFAULT_SCOPE, description = SCOPE_DESCRIPTION)})})
#RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getTestMe(#PathVariable("timestamp") String timestamp) {
if (timestamp != null) {
return HARDCODED_TEST_VALUE;
}
throw new ResourceNotFoundException("wrong timestamp format");
}
}
The way of how I would like to achieve it is similiar to above if-else statement that check whether timestamp is null or not - so analogically I would like to add similiar if-else to validate format of timestamp and return body if so or 404 error code if it's not.
Any idea what I could use to do that and please give me ready example ? I've tried simple validation with regex but is not convenient and unfortunately didn't work anyway ...
You can use Java 8's DateTimeFormatter and make sure it parses the string without throwing an exception. Here's a method that that returns true if the input string is a valid ISO date:
boolean isValidIsoDateTime(String date) {
try {
DateTimeFormatter.ISO_DATE_TIME.parse(date);
return true;
} catch (DateTimeParseException e) {
return false;
}
}
To return the hardcoded test value in response body, you should use the method like this:
public String getTestMe(#PathVariable("timestamp") String timestamp) {
if (timestamp != null && isValidIsoDateTime(timestamp)) {
return HARDCODED_TEST_VALUE;
}
throw new ResourceNotFoundException("wrong timestamp format");
}
public class MyClass {
public static void main(String args[]) {
System.out.println("-------------OK--------------");
String inputTimeString = "makara_kann";
if (!inputTimeString.matches("^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$")){
System.out.println("Invalid time string: " + inputTimeString);
} else {
System.out.println("valid time string: " + inputTimeString);
}
}
}
-------------OK--------------
Invalid time string: makara_kann
I have a Java Spring based web application and I want to insert a record to a table only if the table does not contain any rows that are "similar" (according to some specific, irrelevant criteria) to the new row.
Because this is a multi-threaded environment, I cannot use a SELECT+INSERT two-step combination as it would expose me to a race condition.
The same question was first asked and answered here and here several years ago. Unfortunately, the questions have got only a little attention and the provided answer is not sufficient to my needs.
Here's the code I currently have and it's not working:
#Component("userActionsManager")
#Transactional
public class UserActionsManager implements UserActionsManagerInterface {
#PersistenceContext(unitName = "itsadDB")
private EntityManager manager;
#Resource(name = "databaseManager")
private DB db;
...
#SuppressWarnings("unchecked")
#Override
#PreAuthorize("hasRole('ROLE_USER') && #username == authentication.name")
public String giveAnswer(String username, String courseCode, String missionName, String taskCode, String answer) {
...
List<Submission> submissions = getAllCorrectSubmissions(newSubmission);
List<Result> results = getAllCorrectResults(result);
if (submissions.size() > 0
|| results.size() > 0) throw new SessionAuthenticationException("foo");
manager.persist(newSubmission);
manager.persist(result);
submissions = getAllCorrectSubmissions(newSubmission);
results = getAllCorrectResults(result);
for (Submission s : submissions) manager.lock(s, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
for (Result r : results ) manager.lock(r, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
manager.flush();
...
}
#SuppressWarnings("unchecked")
private List<Submission> getAllCorrectSubmissions(Submission newSubmission) {
Query q = manager.createQuery("SELECT s FROM Submission AS s WHERE s.missionTask = ?1 AND s.course = ?2 AND s.user = ?3 AND s.correct = true");
q.setParameter(1, newSubmission.getMissionTask());
q.setParameter(2, newSubmission.getCourse());
q.setParameter(3, newSubmission.getUser());
return (List<Submission>) q.getResultList();
}
#SuppressWarnings("unchecked")
private List<Result> getAllCorrectResults(Result result) {
Query q = manager.createQuery("SELECT r FROM Result AS r WHERE r.missionTask = ?1 AND r.course = ?2 AND r.user = ?3");
q.setParameter(1, result.getMissionTask());
q.setParameter(2, result.getCourse());
q.setParameter(3, result.getUser());
return (List<Result>) q.getResultList();
}
...
}
According to the answer provided here I am supposed to somehow use OPTIMISTIC_FORCE_INCREMENT but it's not working. I suspect that the provided answer is erroneous so I need a better one.
edit:
Added more context related code. Right now this code still has a race condition. When I make 10 simultaneous HTTP POST requests approximately 5 rows will get erroneously inserted. Other 5 requests are rejected with HTTP error code 409 (conflict). The correct code would guarantee that only 1 row would get inserted to the database no matter how many concurrent requests I make. Making the method synchronous is not a solution since the race condition still manifests for some unknown reason (I tested it).
Unfortunately after several days of research I was unable to find a short and simple solution to my problem. Since my time budget is not unlimited I had to come up with a workaround. Call it a kludge if you may.
Since the whole HTTP request is a transaction, it will be rolled back at the sight of any conflicts. I am using this for my advantage by locking a special entity within the context of the whole HTTP request. Should multiple HTTP requests be received at the same time, all but one will result in some PersistenceException.
In the beginning of the transaction I am checking whether no other correct answers have been submitted yet. During that check the lock is already effective so no race condition could happen. The lock is effective until the answer is submitted. This basically simulates a critical section as a SELECT+INSERT two step query on the application level (in pure MySQL I would have used the INSERT IF NOT EXISTS construct).
This approach has some drawbacks. Whenever two students submit an answer at the same time, one of them will be thrown an exception. This is sort of bad for performance and bandwidth because the student who received HTTP STATUS 409 has to resubmit their answer.
To compensate the latter, I am automatically retrying to submit the answer on the server side a couple of times between randomly chosen time intervals. See the according HTTP request controller code is below:
#Controller
#RequestMapping("/users")
public class UserActionsController {
#Autowired
private SessionRegistry sessionRegistry;
#Autowired
#Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
#Resource(name = "userActionsManager")
private UserActionsManagerInterface userManager;
#Resource(name = "databaseManager")
private DB db;
.
.
.
#RequestMapping(value = "/{username}/{courseCode}/missions/{missionName}/tasks/{taskCode}/submitAnswer", method = RequestMethod.POST)
public #ResponseBody
Map<String, Object> giveAnswer(#PathVariable String username,
#PathVariable String courseCode, #PathVariable String missionName,
#PathVariable String taskCode, #RequestParam("answer") String answer, HttpServletRequest request) {
init(request);
db.log("Submitting an answer to task `"+taskCode+"` of mission `"+missionName+
"` in course `"+courseCode+"` as student `"+username+"`.");
String str = null;
boolean conflict = true;
for (int i=0; i<10; i++) {
Random rand = new Random();
int ms = rand.nextInt(1000);
try {
str = userManager.giveAnswer(username, courseCode, missionName, taskCode, answer);
conflict = false;
break;
}
catch (EntityExistsException e) {throw new EntityExistsException();}
catch (PersistenceException e) {}
catch (UnexpectedRollbackException e) {}
try {
Thread.sleep(ms);
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
if (conflict) str = userManager.giveAnswer(username, courseCode, missionName, taskCode, answer);
if (str == null) db.log("Answer accepted: `"+answer+"`.");
else db.log("Answer rejected: `"+answer+"`.");
Map<String, Object> hm = new HashMap<String, Object>();
hm.put("success", str == null);
hm.put("message", str);
return hm;
}
}
If for some reason the controller is unable to commit the transaction 10 times in a row then it will try one more time but will not attempt to catch the possible exceptions. When an exception is thrown on the 11th try then it will be processed by the global exception controller and the client will receive HTTP STATUS 409. The global exception controller is defined below.
#ControllerAdvice
public class GlobalExceptionController {
#Resource(name = "staticDatabaseManager")
private StaticDB db;
#ExceptionHandler(SessionAuthenticationException.class)
#ResponseStatus(value=HttpStatus.FORBIDDEN, reason="session has expired") //403
public ModelAndView expiredException(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView("exception");
mav.addObject("name", e.getClass().getSimpleName());
mav.addObject("message", e.getMessage());
return mav;
}
#ExceptionHandler({UnexpectedRollbackException.class,
EntityExistsException.class,
OptimisticLockException.class,
PersistenceException.class})
#ResponseStatus(value=HttpStatus.CONFLICT, reason="conflicting requests") //409
public ModelAndView conflictException(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView("exception");
mav.addObject("name", e.getClass().getSimpleName());
mav.addObject("message", e.getMessage());
synchronized (db) {
db.setUserInfo(request);
db.log("Conflicting "+request.getMethod()+" request to "+request.getRequestURI()+" ("+e.getClass().getSimpleName()+").", Log.LVL_SECURITY);
}
return mav;
}
//ResponseEntity<String> customHandler(Exception ex) {
// return new ResponseEntity<String>("Conflicting requests, try again.", HttpStatus.CONFLICT);
//}
}
Finally, the giveAnswer method itself utilizes a special entity with a primary key lock_addCorrectAnswer. I lock that special entity with the OPTIMISTIC_FORCE_INCREMENT flag which makes sure that no two transactions can have overlapping execution times for the giveAnswer method. The respective code can be seen below:
#Component("userActionsManager")
#Transactional
public class UserActionsManager implements UserActionsManagerInterface {
#PersistenceContext(unitName = "itsadDB")
private EntityManager manager;
#Resource(name = "databaseManager")
private DB db;
.
.
.
#SuppressWarnings("unchecked")
#Override
#PreAuthorize("hasRole('ROLE_USER') && #username == authentication.name")
public String giveAnswer(String username, String courseCode, String missionName, String taskCode, String answer) {
.
.
.
if (!userCanGiveAnswer(user, course, missionTask)) {
error = "It is forbidden to submit an answer to this task.";
db.log(error, Log.LVL_MAJOR);
return error;
}
.
.
.
if (correctAnswer) {
.
.
.
addCorrectAnswer(newSubmission, result);
return null;
}
newSubmission = new Submission(user, course, missionTask, answer, false);
manager.persist(newSubmission);
return error;
}
private void addCorrectAnswer(Submission submission, Result result) {
String var = "lock_addCorrectAnswer";
Global global = manager.find(Global.class, var);
if (global == null) {
global = new Global(var, 0);
manager.persist(global);
manager.flush();
}
manager.lock(global, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
manager.persist(submission);
manager.persist(result);
manager.flush();
long submissions = getCorrectSubmissionCount(submission);
long results = getResultCount(result);
if (submissions > 1 || results > 1) throw new EntityExistsException();
}
private long getCorrectSubmissionCount(Submission newSubmission) {
Query q = manager.createQuery("SELECT count(s) FROM Submission AS s WHERE s.missionTask = ?1 AND s.course = ?2 AND s.user = ?3 AND s.correct = true");
q.setParameter(1, newSubmission.getMissionTask());
q.setParameter(2, newSubmission.getCourse());
q.setParameter(3, newSubmission.getUser());
return (Long) q.getSingleResult();
}
private long getResultCount(Result result) {
Query q = manager.createQuery("SELECT count(r) FROM Result AS r WHERE r.missionTask = ?1 AND r.course = ?2 AND r.user = ?3");
q.setParameter(1, result.getMissionTask());
q.setParameter(2, result.getCourse());
q.setParameter(3, result.getUser());
return (Long) q.getSingleResult();
}
}
It is important to note that the entity Global has to have a version annotation in its class for the OPTIMISTIC_FORCE_INCREMENT to work (see code below).
#Entity
#Table(name = "GLOBALS")
public class Global implements Serializable {
.
.
.
#Id
#Column(name = "NAME", length = 32)
private String key;
#Column(name = "INTVAL")
private int intVal;
#Column(name = "STRVAL", length = 4096)
private String strVal;
#Version
private Long version;
.
.
.
}
Such an approach can be optimized even further. Instead of using the same lock name lock_addCorrectAnswer for all giveAnswer calls, I could generate the lock name deterministically from the name of the submitting user. For example, if the student's username is Hyena then the primary key for the lock entity would be lock_Hyena_addCorrectAnswer. That way multiple students could submit answers at the same time without receiving any conflicts. However, if a malicious user spams the HTTP POST method for submitAnswer 10x in parallel they will be prevented by the this locking mechanism.