Is there any way integrate ajax in spring mvc and apache tile. For me, when sent the request to controller and after that send back to view, the data can not pass through tile config.
Help me please. Thanks
I do this all the time. What i do is, instead of sending back a "View" string or "ModelAndView" object, return an object of type ResponseEntity.
For instance, in your controller class, you can have a method like:
#RequestMapping(value="/cache/clear", method = RequestMethod.GET)
public ResponseEntity<String> clearAllCaches() {
Map<String, Object> results = new HashMap<String, Object>();
long startTimestamp = System.currentTimeMillis();
for (CachingService cachingService : cachingServices) {
LOGGER.info("Clearing caches in " + cachingService.getClass().getSimpleName());
cachingService.clearCache();
}
long finishTimestamp = System.currentTimeMillis();
long executionTime = finishTimestamp - startTimestamp;
LOGGER.warn("Finished clearing caches in " + cachingServices.size() + " services in " + executionTime + " ms.");
results.put("executionTime", executionTime);
results.put("serviceCount", cachingServices.size());
results.put(ServiceConstants.RETURN_KEY_SUCCESS, Boolean.TRUE);
return createResponse(results, HttpStatus.CREATED);
}
protected ResponseEntity<String> createResponse(Map <String, Object> results, HttpStatus successCode) {
JSONSerializer serializer = new JSONSerializer();
String responseBody = serializer.deepSerialize(results);
HttpStatus status = successCode;
boolean success = results.containsKey(ServiceConstants.RETURN_KEY_SUCCESS)
? (Boolean)results.get(ServiceConstants.RETURN_KEY_SUCCESS)
: false;
if (!success) {
status = HttpStatus.BAD_REQUEST;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setContentLength(responseBody.length());
headers.add("Access-Control-Allow-Origin", "*");
return new ResponseEntity<String>(responseBody, headers, status);
}
Note that I'm using FlexJSON as I'm using Spring Roo. You could also manually invoke Jackson.
i solve my problem by annotation #ResponseBody to send string back to ajax page.
public #ResponseBody
String test() {
List<User> users = userDetailsService.test();
for (User user : users) {
System.out.println(user);
}
return "1";
}
Related
Ok, so I am trying to achieve facebook authentication in my application but after success user is not redirected to homepage even though location header is set.
This is my current scenario.
Front-End call:
#GetMapping(value = "/login/facebook")
public String loginFacebook() {
String facebookLoginUrl = String.valueOf(this.restTemplate.getForEntity(this.serverApi + "/login/facebook",String.class).getHeaders().get("Location"));
facebookLoginUrl = facebookLoginUrl.substring(1,facebookLoginUrl.length()-1);
System.out.println(facebookLoginUrl);
return "redirect:" + facebookLoginUrl;
}
Back-End:
#RequestMapping(value = "/login/facebook", method = RequestMethod.GET)
public String startFacebookProcess() {
this.facebookConnectionFactory = new FacebookConnectionFactory(appId,appSecret);
OAuth2Operations operations = this.facebookConnectionFactory.getOAuthOperations();
OAuth2Parameters parameters = new OAuth2Parameters();
// promeni url-a za front-enda
parameters.setRedirectUri("http://localhost:8080/login/returnFromFacebook");
parameters.setScope(this.scope);
System.out.println("In startFacebookProcess");
String url = operations.buildAuthorizeUrl(parameters);
System.out.println(url);
return "redirect:" + url;
}
After that, the user is being redirected to the official facebook login page where he allows his details to be used.
Then he performs a GET request to my Front-End with his authorization code:
#GetMapping(value = "/login/returnFromFacebook")
public ResponseEntity getFacebookData(#RequestParam("code") String authorizationCode){
System.out.println(authorizationCode);
ResponseEntity response = this.restTemplate.getForEntity(this.serverApi +
"/login/returnFromFacebook?code=" + authorizationCode, ResponseEntity.class);
if(response.getStatusCode().is2xxSuccessful()){
return response;
}
return new ResponseEntity(HttpStatus.CONFLICT);
}
My Back-End is being called and I contact facebook to fetch the user data using his authorization code. Then I return a ResponseEntity which inherited all the HttpServletResponse headers and cookies.
#RequestMapping(value = "/login/returnFromFacebook", method = RequestMethod.GET)
public ResponseEntity getDataFromFacebook(#RequestParam("code") String authorizationCode,
HttpServletResponse response) throws IOException {
// leg 2
OAuth2Operations operations = this.facebookConnectionFactory.getOAuthOperations();
AccessGrant accessGrant = operations.exchangeForAccess(authorizationCode,
"http://localhost:8080/login/returnFromFacebook",null);
// leg 3
System.out.println("code: " + authorizationCode);
Connection<Facebook> connection = this.facebookConnectionFactory.createConnection(accessGrant);
User currentUser = connection.getApi().fetchObject("me", User.class,"email,first_name,last_name");
System.out.println("Email: " + currentUser.getEmail());
if(this.appUserDAO.findUserAccountByEmail(currentUser.getEmail()) == null){
Map<String, String> facebookDetailsMap = new LinkedHashMap<>();
facebookDetailsMap.put("email",currentUser.getEmail());
facebookDetailsMap.put("name",currentUser.getFirstName() + " " + currentUser.getLastName());
this.appUserDAO.saveFacebookAccount(facebookDetailsMap);
}
// Create JWT Token
String token = JWT.create()
.withSubject(currentUser.getEmail())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.sign(HMAC512(JwtProperties.SECRET.getBytes()));
JwtAuthenticationFilter.setJwtCookie(response,token);
System.out.println(response.getStatus() + " " + response.getHeader("set-cookie"));
response.setHeader("Location", "http://localhost:8080/");
// HttpHeaders headers = new HttpHeaders();
// headers.add(index");
return new ResponseEntity<>(HttpStatus.CREATED);
}
This is the received response header in the browser. The cookies are set and everything but user is not being redirected to Location header. :
I tried to return a String "redirect:/" from GetFacebookData(Front-End) but the response cookies and headers are not being applied. I don't know why the Location header is not working.
I fixed it by returning the 'index' template if the back-end call was sucessful but also apply the cookie header into the original response (which was to the front-end) using HttpServletResponse (this is not possible with ResponseEntity btw). With HttpServletResponse you dont have to return it to the browser, it applies everything by default. With ResponseEntity you have to return ResponseEntity Object in order to apply headers. This is how the function looks like now:
#GetMapping(value = "/login/returnFromFacebook")
public String getFacebookData(#RequestParam("code") String authorizationCode, HttpServletResponse httpServletResponse) throws IOException {
System.out.println(authorizationCode);
ResponseEntity response = this.restTemplate.getForEntity(this.serverApi +
"/login/returnFromFacebook?code=" + authorizationCode, ResponseEntity.class);
System.out.println(response.getStatusCodeValue() + " " + response.getHeaders().get("Location"));
//set cookie for original request
String jwtCookieHeader = String.valueOf(response.getHeaders().get("set-cookie"));
jwtCookieHeader = jwtCookieHeader.substring(1,jwtCookieHeader.length()-1);
httpServletResponse.setHeader("set-cookie", jwtCookieHeader);
if(response.getStatusCode().is2xxSuccessful()){
return "redirect:/";
}
return String.valueOf(new ResponseEntity(HttpStatus.CONFLICT));
}
I'm currently developing my first java program who'll make a call to a rest api(jira rest api, to be more especific).
So, if i go to my browser and type the url =
"http://my-jira-domain/rest/api/latest/search?jql=assignee=currentuser()&fields=worklog"
I get a response(json) with all the worklogs of the current user.
But my problem is, how i do my java program to do this ?
Like,connect to this url, get the response and store it in a object ?
I use spring, with someone know how to this with it.
Thx in advance guys.
Im adding, my code here:
RestTemplate restTemplate = new RestTemplate();
String url;
url = http://my-jira-domain/rest/api/latest/search/jql=assignee=currentuser()&fields=worklog
jiraResponse = restTemplate.getForObject(url,JiraWorklogResponse.class);
JiraWorkLogResponse is a simple class with some attributes only.
Edit,
My entire class:
#Controller
#RequestMapping("/jira/worklogs")
public class JiraWorkLog {
private static final Logger logger = Logger.getLogger(JiraWorkLog.class.getName() );
#RequestMapping(path = "/get", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity getWorkLog() {
RestTemplate restTemplate = new RestTemplate();
String url;
JiraProperties jiraProperties = null;
url = "http://my-jira-domain/rest/api/latest/search?jql=assignee=currentuser()&fields=worklog";
ResponseEntity<JiraWorklogResponse> jiraResponse;
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders = this.createHeaders();
try {
jiraResponse = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<Object>(httpHeaders),JiraWorklogResponse.class);
}catch (Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
return ResponseEntity.status(HttpStatus.OK).body(jiraResponse);
}
private HttpHeaders createHeaders(){
HttpHeaders headers = new HttpHeaders(){
{
set("Authorization", "Basic something");
}
};
return headers;
}
This code is returning :
org.springframework.http.converter.HttpMessageNotWritableException
Anyone knows why ?
All you need is http client. It could be for example RestTemplate (related to spring, easy client) or more advanced and a little more readable for me Retrofit (or your favorite client).
With this client you can execute requests like this to obtain JSON:
RestTemplate coolRestTemplate = new RestTemplate();
String url = "http://host/user/";
ResponseEntity<String> response
= restTemplate.getForEntity(userResourceUrl + "/userId", String.class);
Generally recommened way to map beetwen JSON and objects/collections in Java is Jackson/Gson libraries. Instead them for quickly check you can:
Define POJO object:
public class User implements Serializable {
private String name;
private String surname;
// standard getters and setters
}
Use getForObject() method of RestTemplate.
User user = restTemplate.getForObject(userResourceUrl + "/userId", User.class);
To get basic knowledge about working with RestTemplate and Jackson , I recommend you, really great articles from baeldung:
http://www.baeldung.com/rest-template
http://www.baeldung.com/jackson-object-mapper-tutorial
Since you are using Spring you can take a look at RestTemplate of spring-web project.
A simple rest call using the RestTemplate can be:
RestTemplate restTemplate = new RestTemplate();
String fooResourceUrl = "http://localhost:8080/spring-rest/foos";
ResponseEntity<String> response = restTemplate.getForEntity(fooResourceUrl + "/1", String.class);
assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
The issue could be because of the serialization. Define a proper Model with fields coming to the response. That should solve your problem.
May not be a better option for a newbie, but I felt spring-cloud-feign has helped me to keep the code clean.
Basically, you will be having an interface for invoking the JIRA api.
#FeignClient("http://my-jira-domain/")
public interface JiraClient {
#RequestMapping(value = "rest/api/latest/search?jql=assignee=currentuser()&fields=", method = GET)
JiraWorklogResponse search();
}
And in your controller, you just have to inject the JiraClient and invoke the method
jiraClient.search();
And it also provides easy way to pass the headers.
i'm back and with a solution (:
#Controller
#RequestMapping("/jira/worklogs")
public class JiraWorkLog {
private static final Logger logger = Logger.getLogger(JiraWorkLog.class.getName() );
#RequestMapping(path = "/get", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<JiraWorklogIssue> getWorkLog(#RequestParam(name = "username") String username) {
String theUrl = "http://my-jira-domain/rest/api/latest/search?jql=assignee="+username+"&fields=worklog";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<JiraWorklogIssue> response = null;
try {
HttpHeaders headers = createHttpHeaders();
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
response = restTemplate.exchange(theUrl, HttpMethod.GET, entity, JiraWorklogIssue.class);
System.out.println("Result - status ("+ response.getStatusCode() + ") has body: " + response.hasBody());
}
catch (Exception eek) {
System.out.println("** Exception: "+ eek.getMessage());
}
return response;
}
private HttpHeaders createHttpHeaders()
{
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Basic encoded64 username:password");
return headers;
}
}
The code above works, but can someone explain to me these two lines ?
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
response = restTemplate.exchange(theUrl, HttpMethod.GET, entity, JiraWorklogIssue.class);
And, this is a good code ?
thx (:
I'm quite new to Spring and I'm trying to use it to save Mandrill webhooks events. After creating all the required validators I've discovered that it's sending its events using application/x-www-form-urlencoded header, which was preventing all the validators to work.
After that, I've decided to handle Mandrill webhooks remapping its request as a valid JSON object, with its header Content-Type: application/json.
Searching on SO, I've found so many ways to send a POST request from Spring to itself, and after a few tries, I've started using RestTemplate, like this
Webhook Controller
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void remap(#RequestParam String mandrill_events) throws IOException {
JsonNodeFactory factory = JsonNodeFactory.instance;
ArrayNode eventsNode = factory.arrayNode();
ObjectNode eventNode = factory.objectNode();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(mandrill_events);
if (node.isArray()) {
for (int i = 0; i < node.size(); i++) {
eventsNode.add(node.get(i));
}
}
eventNode.set("mandrill_events", eventsNode);
String webhookStoreURL = this.globalConfig.APIUrl + "/webhook/store";
RestTemplate restTemplate = new RestTemplate();
try {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<String> httpEntity = new HttpEntity<>(eventNode.toString(), httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(webhookStoreURL, httpEntity, String.class);
} catch (Exception e) {
e.printStackTrace();
}
}
#RequestMapping(path = "/store", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void store(#RequestBody #Valid MandrillEventList eventList) throws StorageException, NotFoundException, SQLException {
System.out.println("Saving " + eventList.getMandrillEvents().size() + " events");
//this.eventService.storeEvents(eventList);
}
The problem is, when I send the RestTemplate request, the only thing I get it's a 400 null error from the catch block.
So this is what I've tried so far:
Removing #Valid annotation from controller method. Didn't work
Removing all the validators from the global one. Didn't work
Seemed like it was trying to validate request body, even without annotations, so I've tried those two as well
Copying the request body from debug and testing it on Postman. Works (shows validation errors correctly)
Using the Mandrill test webhook. Works (Spring and Postman)
I have no idea about where to look
I came out with a solution. Instead of re-sending the API request to himself, I've done this
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public void store(#RequestParam String mandrill_events) throws IOException {
JsonNodeFactory factory = JsonNodeFactory.instance;
ArrayNode eventsNode = factory.arrayNode();
ObjectNode eventNode = factory.objectNode();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(mandrill_events);
if (node.isArray()) {
for (int i = 0; i < node.size(); i++) {
eventsNode.add(node.get(i));
}
}
eventNode.set("mandrill_events", eventsNode);
try {
MandrillEventList eventList = mapper.readValue(eventNode.toString(), MandrillEventList.class);
logger.log(Level.FINE, "Webhook has " + eventList.getMandrillEvents().size() + " events to save");
this.eventService.storeEvents(eventList);
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.SEVERE, "Unable to setup mandrill event list " + e.getMessage());
throw new IOException("Unable to setup mandrill event list");
}
}
I rebuild the JSON as Controller expected it then I map it as the original MandrillEventList object with Jackson ObjectMapper. It gave me some problems, for example when he didn't recognize properties which weren't inside class description, so I've added the #JsonIgnoreProperty annotation like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class MandrillEvent {
[...]
}
Everything works right now
I have a problem.
I don't know how to return a view in a method with a return type of ResponseEntity.
I want to download a file with my controller.
The download works fine if a file was uploaded.
If no file were uploaded, it just should do nothing (return the actual view).
Now I´m not sure how to do this because I guess it's not possible returning a view (For that I needed return-type String).
Do you have any idea?
#Controller
public class FileDownloadController {
#RequestMapping(value="/download", method = RequestMethod.GET)
public ResponseEntity fileDownload (#Valid DownloadForm form, BindingResult result) throws IOException{
RestTemplate template = new RestTemplate();
template.getMessageConverters().add(new FormHttpMessageConverter());
HttpEntity<String> entity = new HttpEntity<String>(createHttpHeaders("test.jpg", "image/jpeg"));
UrlResource url = new UrlResource("www.thisismyurl.com/images" + form.getImageId());
return new ResponseEntity<>(new InputStreamResource(url.getInputStream()), createHttpHeaders("test.jpg", "image/jpeg"), HttpStatus.OK);
}
private HttpHeaders createHttpHeaders(String filename, String contentType) {
HttpHeaders headers = new HttpHeaders();
headers.setAll(getHttpHeaderMap(filename, contentType));
return headers;
}
private Map<String,String> getHttpHeaderMap(String filename, String contentType) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-disposition", "attachment; filename=\"" + filename + "\"");
headers.put("Content-Type", contentType);
return headers;
}
}
Hi i had a similar problem in my project once, i.e., I have to different return types view vs string based on some logic.
First it’s definitely not possible to return a model and view when you have response entity as return type.
I solved this using generic return type
public <T> T fileDownload (#Valid DownloadForm form, BindingResult result) throws IOException{
//your code
//here you can return response entity or
//modelAndView based on your logic
}
I've found that this works with Spring Boot 2 and JSP views:
#GetMapping(value = "/view/theobject/{id}")
public Object getDomainObject(ModelAndView mav, #PathVariable Long id) {
Optional<DomainObject> theObject = svc.getDomainObject(id);
if (theObject.isPresent()) {
mav.setViewName("viewdomainobject");
mav.addObject("theObject", theObject.get());
return mav;
}
return ResponseEntity.notFound().build();
}
There's no need for the unpleasant <T> T generic return type, or casting the returned object.
Here is my controller. I used postman to test if it's working but I am getting an empty response. I used #EnableAsync in application configuration and #Async on the service. If I remove #Async on service layer it works but it doesn't run asynchronously.
#ApiOperation(value = "search person by passing search criteria event/title/role/host/is_current", response = ElasticSearchResultData.class)
#RequestMapping(value = "/async2/searchPerson", produces = "application/json", method = RequestMethod.POST)
public #ResponseBody CompletableFuture<ElasticSearchResultData> searchPersonAsync2(#RequestBody SearchCriteriaTo criteriaForDivNetFolderTo,
HttpServletRequest request, HttpServletResponse response){
LOGGER.info("searchPerson controller start");
SearchCriteria searchCriteria = criteriaForDivNetFolderTo.getSearchCriteria();
if (Util.isNull(searchCriteria))
throw new IllegalArgumentException("search criteria should not be null.");
try {
CompletableFuture<ElasticSearchResultData> searchPerson = cubService.searchPersonAsync2(criteriaForDivNetFolderTo);
ObjectMapper mapper = new ObjectMapper();
LOGGER.info("search Person "+mapper.writeValueAsString(searchPerson));
return searchPerson;
} catch (Exception e) {
LOGGER.error("Exception in searchPersonAsync controller "+e.getMessage());
}
return null;
}
Service
#Async
#Override
public CompletableFuture<ElasticSearchResultData> searchPersonAsync2(SearchCriteriaTo searchCriteriaForDivNetFolderTo) {
Long start = System.currentTimeMillis();
LOGGER.info(":in searchPerson");
CompletableFuture<ElasticSearchResultData> completableFuture = new CompletableFuture<>();
ElasticSearchResultData searchResultData = null;
SearchCriteria searchCriteria = searchCriteriaForDivNetFolderTo.getSearchCriteria();
try {
LOGGER.info("************ Started searchPerson by criteria ************");
StringBuilder url = new StringBuilder();
url.append(equilarSearchEngineApiUrl)
.append(focusCompanySearchUrl)
.append("/")
.append("searchPerson")
.append("?view=").append(VIEW_ALL)
.append("&isProcessing=true");
LOGGER.debug("Calling equilar search engine for focused company search, URL : " + url);
LOGGER.info(searchCriteria.toString());
String output = null;
if (redisEnable != null && redisEnable) {
output = cacheDao.getDataFromRestApi(url.toString(), RequestMethod.POST.name(), searchCriteria);
} else {
output = Util.getDataFromRestApi(url.toString(), RequestMethod.POST.name(), searchCriteria);
}
if (!Util.isEmptyString(output)) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
searchResultData = objectMapper.readValue(output,
objectMapper.getTypeFactory().constructType(ElasticSearchResultData.class));
}
List<PersonSearchDetails> newPersonDetails = new ArrayList<PersonSearchDetails>();
if (!Util.isNull(searchResultData) && !Util.isNullOrEmptyCollection(searchResultData.getPersonDetails())
&& !Util.isNullOrEmptyCollection(searchCriteriaForDivNetFolderTo.getNetworkFoldersData())) {
for (PersonSearchDetails personDetail : searchResultData.getPersonDetails()) {
String logoUrl = null;
if(!Util.isNull(searchCriteria.getTargetFolderId())){
List<DiversityNetworkFolderTo> filteredFolderTos = searchCriteriaForDivNetFolderTo
.getNetworkFoldersData()
.stream()
.filter(folder -> folder.getId()
.longValue() == searchCriteria
.getTargetFolderId())
.collect(Collectors.toList());
logoUrl = getLogoUrl(personDetail.getPersonId(),
filteredFolderTos);
} else {
logoUrl = getLogoUrl(personDetail.getPersonId(),
searchCriteriaForDivNetFolderTo.getNetworkFoldersData());
}
personDetail.setLogoUrl(logoUrl);
newPersonDetails.add(personDetail);
}
searchResultData.setPersonDetails(newPersonDetails);
}
completableFuture.complete(searchResultData);
return completableFuture;
} catch (Exception e) {
completableFuture.completeExceptionally(e);
LOGGER.error(
" ************** Error in proccessing searchPerson by criteria ************** " + e.getMessage());
}
Long end = System.currentTimeMillis();
LOGGER.info(TIME_DURATION+(end - start)+"ms");
return null;
}
It would be good to read more about async processing. javadocs are usually a great start!
If you really want to get the result from a Future method, you need to wait for it.
There is a method public T get() method in the CompletableFuture API to wait for wait for the result to be created and return the result once it's done.
If your job is to search a database for the result and then return it - you will still have to wait for it async is not much help in here. It would help you if you had to make multiple things at the same time, e.g. a call to DB, a web service and something else at the same time, then you can create an array of futures and wait for all of them to complete.
Or, let's say you're creating a POST method, so you can quickly validate the input and send to store to DB async while quickly returning the response to UI and hoping that your async method will be completed in another thread and not returning any errors to UI.
This is a great technique when you know what you're doing, but think if & when you really need it before using it.
The short way to "fix" this is:
CompletableFuture<ElasticSearchResultData> searchPerson = cubService.searchPersonAsync2(criteriaForDivNetFolderTo);
ElasticSearchResultData result = searchPerson.get();
ObjectMapper mapper = new ObjectMapper();
LOGGER.info("search Person "+mapper.writeValueAsString(result));
return result;
( and obviously change the method return signature )