The overview of the project is this:
Upload file to Springboot server via an endpoint
The endpoint sends an OK response when the file is received, but continues to process the file in the background, running tests on the file.
So, since the endpoint of the controller has already returned a response, how can I send info from the backend to the frontend outside of using the Controller.
Here is what is running after the Controller returns the response:
CompletableFuture.runAsync(() -> {
int count = 0;
boolean stillProcessing = true;
while (stillProcessing) {
stillProcessing = !test.isTestComplete();
if (test.getNumberOfInstancesComplete() > count) {
count = test.getNumberOfInstancesComplete();
log.info("{}/{} instances completed so far", count, test.getInstances().size());
}
}
});
The log.info line is what I need to return to the frontend React side of things.
The end goal is to basically have a loading bar shown to users using the values printed in log.info().
You can use websockets to notify frontend without a controller. Here is the example code to send a message to the client from backend using STOMP at any time.
#Component
public class PushMessage {
#Autowired
SimpMessagingTemplate simpMessagingTemplate;
public <T> void invokeWebSocketEndpoint(String endpoint, T payload) {
this.simpMessagingTemplate.convertAndSend(endpoint, payload);
}
}
For more info on STOMP websockets, check out this link
https://spring.io/guides/gs/messaging-stomp-websocket/
If you do not want bi-directional communication between the client and the server and just want to push messages to client from the server you can also make use of Server sent events. Here's a simple example.
#GetMapping(value = "/test")
public SseEmitter test() {
SseEmitter emitter = new SseEmitter();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
try {
Process p = Runtime.getRuntime().exec("ping -c 10 www.google.com");
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
emitter.send(line);
}
emitter.complete();
}
} catch (IOException e) {
emitter.completeWithError(e);
e.printStackTrace();
}
});
executorService.shutdown();
return emitter;
}
For more information on Server sent events see this
https://www.baeldung.com/spring-server-sent-events
You can consume server sent events from your frontend using the EventSource API
https://developer.mozilla.org/en-US/docs/Web/API/EventSource
I use Retrofit2 to make REST API requests. I have my dummy server (that runs with spring boot) on my machine:
#RestController
class SecureServiceController {
private int counter = 1;
#RequestMapping(value = "/nnrf-nfm/v1/nf-instances/bee75393-2ac3-4e60-9503-854e733309d4", method = RequestMethod.PUT)
public ResponseEntity<NFProfile> nNrfNfManagementNfRegister() {
System.out.println(counter++ + ". Got NrfClient register request. " + new Date());
NFProfile nfProfile = new NFProfile();
nfProfile.setHeartBeatTimer(2);
ResponseEntity<NFProfile> responseEntity = ResponseEntity.status(201).body(nfProfile);
return responseEntity;
}
}
When client make request from the same machine it works. But when client make request from remote machine I have error response:
Response{protocol=http/1.1, code=401, message=Unauthorized, url=https://myhostname:8443/nnrf-nfm/v1/nf-instances/bee75393-2ac3-4e60-9503-854e733309d4}
Response error body: <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>Error</title></head><body><h1>Error</h1></body></html>
I've read that such error means that client don't have the rights to access and need to add access token. But my server does not ask any access token (at least explicitly) and it should not ask it.
How to solve this problem?
My apiClient:
public class ApiClient {
private Map<String, Interceptor> apiAuthorizations;
private Builder okBuilder;
private retrofit2.Retrofit.Builder adapterBuilder;
private JSON json;
//a lot setters and getters
public <S> S createService(Class<S> serviceClass) {
return this.adapterBuilder.client(this.okBuilder.build()).build().create(serviceClass);
}
public void configureFromOkclient(OkHttpClient okClient) {
this.okBuilder = okClient.newBuilder();
this.addAuthsToOkBuilder(this.okBuilder);
}
}
my interface:
public interface NfInstanceIdDocumentApi {
#Headers({"Content-Type:application/json"})
#PUT("nf-instances/{nfInstanceID}")
Call<NFProfile> registerNFInstance(#Body NFProfile body, #Path("nfInstanceID") UUID nfInstanceID, #Header("Content-Encoding") String contentEncoding, #Header("Accept-Encoding") String acceptEncoding);
}
How I do call:
OkHttpClient okHttpClient= ClientFactory.createClient();
ApiClient client = new ApiClient();
client.configureFromOkclient(okHttpClient);
NFProfile body = getNfProfile();
String baseUri = getBaseUri();
UUID uuid = getUUID();
//create call
client.getAdapterBuilder().baseUrl(baseUri);
NfInstanceIdDocumentApi service = client.createService(NfInstanceIdDocumentApi.class);
Call<NFProfile> call = service.registerNFInstance(body, uuid, null, null);
//make call
Response<NFProfile> response = call.execute();
UPD
I found the problem. Server was running on Windows machine and firewall blocked incoming requests.
I would like to subscribe and receive any events using objects instead of strings when using Spring Boot WebSockets. If my method returns a string and I use the StringMessageConverter my code successfully listens for the /topic/rooms/created event.
If I return a Room object instead and use MappingJackson2MessageConverter then my subscription no longer receives any messages.
#MessageMapping("/rooms/create/{roomName}")
#SendTo("/topic/rooms/created")
#CrossOrigin(origins = "http://localhost:3000")
public Room createRoom(#DestinationVariable final String roomName) {
return roomService.createRoom(roomService.getRooms().size(), roomName);
}
This sends the message and creates the room successfully. The response isn't picked up by the subscription.
final String roomName = "RoomName";
final StompSession.Subscription subscription = stompSession.subscribe("/topic/rooms/created", new StompFrameHandler() {
#Override
public Type getPayloadType(final StompHeaders headers) {
return Room.class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
// Not called
System.out.println("Received message");
}
});
System.out.println("Sending message");
stompSession.send("/app/rooms/create/" + roomName, null);
I've also tried creating a Room instance and using a jackson object mapper to convert to JSON with no issues.
How can I resolve this?
I m training my self on creating a restful server and a desktop java based client.
my backend is Spring Boot based, I have the following controller :
#RestController
#RequestMapping(NiveauAccessController.URL)
public class NiveauAccessController extends GenericController{
public static final String URL = "/acl";
#Autowired
private NiveauAccessRepository niveauAccessRepository;
#PostMapping
private ServerResponse createACL(
#RequestParam("aclTitle") final String aclTitle,
#RequestParam("roles") final List<String> roles
){
if(isSessionValid()){
final MNG_NIVEAU_ACCEE mng_niveau_accee = new MNG_NIVEAU_ACCEE();
mng_niveau_accee.setAclTitle(aclTitle);
List<Role> enumRoles = new ArrayList();
roles.stream().forEach(role->{
enumRoles.add(Role.valueOf(role));
});
mng_niveau_accee.setRoles(enumRoles);
niveauAccessRepository.save(mng_niveau_accee);
initSuccessResponse(mng_niveau_accee);
return serverResponse;
}
initFailLoginResponse();
return serverResponse;
}
.
.
.
}
for my java client I m using this sample code to send a post request over my server :
#FXML
private void doAdd(ActionEvent event) throws UnirestException {
if (titleACL.getText().isEmpty()) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.initModality(Modality.WINDOW_MODAL);
alert.initOwner(((Node) event.getSource()).getScene().getWindow());
alert.setContentText("Veuillez remplir le champ");
alert.showAndWait();
titleACL.requestFocus();
return;
}
String title = titleACL.getText();
Predicate<? super JFXCheckBox> selectedCheckboxes = checkbox -> {
return checkbox.isSelected();
};
List<JFXCheckBox> selectedCheckBoxesList = observableCheckBoxes.values().stream().filter(selectedCheckboxes).collect(Collectors.toList());
final List<String> roles = new ArrayList<>();
selectedCheckBoxesList.stream().forEach(checkbox -> {
roles.add(checkbox.getText());
});
HttpResponse<String> asString = Unirest.post(ACL_URL)
.header("accept", "application/json")
.field("aclTitle", title)
.field("roles", roles)
.asString();
System.out.println(asString.getStatus());
System.out.println(asString.getHeaders().values());
if (asString.getStatus() == 200) {
}
}
my output is :
302
[[0], [Thu, 10 May 2018 13:30:05 GMT], [https://localhost:8443/acl]]
I don't understand why I m getting the 302 status code which is for URL redirection.
I m trying to use this post to add data to my database.
What should I do to make my Server accept this request?
havins ssl enabled my request over 8080 got redirection to 8443 this is no issue using a web browser because it will handle redirection but in a javafx client you have to handle the redirect by your self so there is a possible solution
if (asString.getStatus() == 200) {
//your success handler code
}else if (asString.getStatus() == 302) {
// use your Rest api to do the request using the response body
}
I am trying to use Bonita Web API. I My code is below. As you can see I call the loginservice before calling any other API service. It logs in OK 200. But when I make the subsequent call to get the list of processes I get a 401 error. You get a JSESSIONID from the first call and you are suppose to pass it to the subsequent calls to authenticate you.
var baseAddress = new Uri(<base address>);
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
HttpResponseMessage result = client.PostAsync("/bonita/loginservice", new StringContent("login=<username>,password=<password>,redirect=false")).Result;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage result2 = client.GetAsync("/bonita/API/bpm/process").Result;
result2.EnsureSuccessStatusCode();
}
This works for .Net 2.0 C# but has some interesting things to check.
WebClient wc = new WebClient();
wc.Proxy = WebRequest.GetSystemWebProxy();
//wc.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate";
string strLogin = wc.DownloadString("http://localhost:8080/bonita/loginservice?username=walter.bates&password=bpm&redirect=false");
wc.Headers[HttpRequestHeader.Cookie] = wc.ResponseHeaders[HttpResponseHeader.SetCookie].ToString();
string strCookie = wc.ResponseHeaders[HttpResponseHeader.SetCookie].ToString();
string strProcesses = wc.DownloadString("http://localhost:8080/bonita/API/bpm/process?p=0");
First of all you should know how to determine that the executed operation is successful ( login, getProcesses and whatever) When you try to login you will always get the header (for example "JSESSIONID=50E509D37AC28E2D725CBD45A8112FA7; Path=/bonita; HttpOnly") and OK 200 even if your login attempt in Bonita is unsuccesful.
For the successful login on the previous example
1) You must Pass mandatory form data: username, password and redirect You must also be sure to pass redirect in lower case ."False" will not work, "false" will work. So for .Net suppose you have a property-> Boolean redirect. You must make it lowercase with redirect.ToString().ToLower() cause either way the value will be "False" and you don't want that.
Let's say you try to login only with username and password without passing redirect. the result is that you will get both OK 200 and the header but you will also get a response which is wrong (the response must be empty), so on the next request (i.e getProcesses) you'll get (401) Unauthorized. Guess the results you will have if you pass redirect=False instead of redirect=false. Exactly the same.
2)You must get: strLogin="" // the body of the response must be empty strCookie="JSESSIONID=4F67F134840A2C72DBB968D53772FB22; Path=/bonita; HttpOnly"
For the successful getProcesses on the previous example you pass the header you got from login
wc.Headers[HttpRequestHeader.Cookie] = wc.ResponseHeaders[HttpResponseHeader.SetCookie].ToString();
and then you call the process and get a string in json format for example
"[{\"id\":\"6996906669894804403\",\"icon\":\"\",\"displayDescription\":\"\",\"deploymentDate\":\"2014-11-19 17:57:40.893\",\"description\":\"\",\"activationState\":\"ENABLED\",\"name\":\"Travel request\",\"deployedBy\":\"22\",\"displayName\":\"Travel request\",\"actorinitiatorid\":\"4\",\"last_update_date\":\"2014-11-19 17:57:41.753\",\"configurationState\":\"RESOLVED\",\"version\":\"1.0\"}]"
(or [] which means an empty json)
If the cookie is not passed correctly you will get again 401 error.
Solution for .Net 4.5.1
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace BonitaRestApi
{
class BonitaApi
{
private CookieCollection collection;
string strCookietoPass;
string sessionID;
static void Main(string[] args)
{
BonitaApi obj = new BonitaApi();
Task login = new Task(obj.Login);
login.Start();
login.Wait();
Console.ReadLine();
Task GetProcesses = new Task(obj.GetProcesses);
GetProcesses.Start();
GetProcesses.Wait();
Console.ReadLine();
Task logout = new Task(obj.Logout);
logout.Start();
logout.Wait();
Console.ReadLine();
}
public async void Login()
{
const string url = "http://localhost:8080/bonita/";
var cookies = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
using (var client = new HttpClient(handler))
{
var uri = new Uri(url);
client.BaseAddress = uri;
//client.DefaultRequestHeaders.Accept.Clear();
//client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", "helen.kelly"),
new KeyValuePair<string, string>("password", "bpm"),
new KeyValuePair<string, string>("redirect", "false"),
new KeyValuePair<string, string>("redirectUrl", ""),
});
HttpResponseMessage response = await client.PostAsync("loginservice", content);
if (response.IsSuccessStatusCode)
{
var responseBodyAsText = await response.Content.ReadAsStringAsync();
if (!String.IsNullOrEmpty(responseBodyAsText))
{
Console.WriteLine("Unsuccessful Login.Bonita bundle may not have been started, or the URL is invalid.");
return;
}
collection= cookies.GetCookies(uri);
strCookietoPass = response.Headers.GetValues("Set-Cookie").FirstOrDefault();
sessionID = collection["JSESSIONID"].ToString();
Console.WriteLine(string.Format("Successful Login Retrieved session ID {0}", sessionID));
// Do useful work
}
else
{
Console.WriteLine("Login Error" + (int)response.StatusCode + "," + response.ReasonPhrase);
}
}
}
public async void Logout()
{
const string url = "http://localhost:8080/bonita/";
var cookies = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
using (var client = new HttpClient(handler))
{
var uri = new Uri(url);
client.BaseAddress = uri;
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("redirect", "false")
});
HttpResponseMessage response = await client.PostAsync("logoutservice", content);
if (response.IsSuccessStatusCode)
{
var responseBodyText = await response.Content.ReadAsStringAsync();
if (!String.IsNullOrEmpty(responseBodyText))
{
Console.WriteLine("Unsuccessful Logout.Bonita bundle may not have been started, or the URL is invalid.");
return;
}
Console.WriteLine("Successfully Logged out.");
}
else
{
Console.WriteLine("Logout Error" + (int)response.StatusCode + "," + response.ReasonPhrase);
}
}
}
public async void GetProcesses()
{
var handler = new HttpClientHandler();
Cookie ok = new Cookie("Set-Cookie:",strCookietoPass);
handler.CookieContainer.Add(collection);
using (var client = new HttpClient(handler))
{
var builder = new UriBuilder("http://localhost/bonita/API/bpm/process");
builder.Port = 8080;
var query = HttpUtility.ParseQueryString(builder.Query);
query["p"] = "0";
query["c"] = "10";
builder.Query = query.ToString();
Uri uri= new Uri(builder.ToString());
client.BaseAddress = uri;
HttpResponseMessage response = await client.GetAsync(uri.ToString());
if (response.IsSuccessStatusCode)
{
var responseBodyText = await response.Content.ReadAsStringAsync();
if (String.IsNullOrEmpty(responseBodyText))
{
Console.WriteLine("Unsuccessful GetProcesses.Bonita bundle may not have been started, or the URL is invalid.");
return;
}
Console.WriteLine("Successfully GetProcesses:" + responseBodyText);
}
else
{
Console.WriteLine("GetProcesses Error" + (int)response.StatusCode + "," + response.ReasonPhrase);
}
}
}
}
}
I had the same problem (401 errors) for every single non-GET request.
I finally got through this by looking to the CSRF documentation:
http://documentation.bonitasoft.com/7.4?page=csrf-security
(See the "Is there an impact on REST API calls?" section)
After succesfull login, you have to put a special header in your request:
key: X-Bonita-API-Token
value: the one you got after your login (check the relevant cookie)