Symfony 5 Rest, Base64 encoded file to DTO with validation as File object - rest

I have a PostController looking like this:
#[Route(name: 'add', methods: ['POST'])]
public function addPost(Request $request): JsonResponse
{
/** #var PostRequestDto $postRequest */
$postRequest = $this->serializer->deserialize(
$request->getContent(),
PostRequestDto::class,
'json'
);
return $this->postService->addPost($postRequest);
}
PostService:
public function addPost(PostRequestDto $request): JsonResponse
{
$errors = $this->validator->validate($request);
if (count($errors) > 0) {
throw new ValidationHttpException($errors);
}
#...
return new JsonResponse(null, Response::HTTP_CREATED);
}
And PostRequestDto:
class PostRequestDto
{
#[Assert\NotBlank]
#[Assert\Length(max: 250)]
private ?string $title;
#[Assert\NotBlank]
#[Assert\Length(max: 1000)]
private ?string $article;
#[Assert\NotBlank]
#[Assert\Image]
private ?File $photo;
#[Assert\NotBlank]
#[Assert\GreaterThanOrEqual('now', message: 'post_request_dto.publish_date.greater_than_or_equal')]
private ?DateTimeInterface $publishDate;
}
My Postman request looks like this:
{
"title": "Test",
"article": "lorem ipsum....",
"photo": "base64...",
"publishDate": "2021-10-15 08:00:00"
}
As you can see from postman request, I'm sending base64 encoded file.
Now, in the controller I want to deserialize it to match with PostRequestDto so I can validate it as a File in the PostService - how can I achieve this ?

I don't know how exactly your serializer ($this->serializer) is configured, but I think you have to adjust/add your normilizer with Symfony\Component\Serializer\Normalizer\DataUriNormalizer
// somewhere in your controller/service where serilaizer is configured/set
$normalizers = [
//... your other normilizers if any
new DataUriNormalizer(), // this one
];
$encoders = [new JsonEncoder()];
$this->serializer = new Serializer($normalizers, $encoders);
If you look inside DataUriNormalizer you'll see, it works with File which is exactly what you have in your PostRequestDto
The only thing to be aware of → format of base64.
If you follow the link of denormilize() method, you will see it expects data:image/png;base64,...
So it has to start with data:... and you probably have to change your postman-json-payload to
{
"title": "Test",
"article": "lorem ipsum....",
"photo": "data:<your_base64_string>",
"publishDate": "2021-10-15 08:00:00"
}
Since you work with images, I would also send the mime-type. Like:
"photo": "data:image/png;base64,<your_base64_string>",

Related

Spring Cloud Gateway altering form data does not work

I defined this GatewayFilter:
EDIT More context information:
What I would like to achieve is to avoid the client providing its credentials to get an access token from an authorization server.
The client sends a POST request with user's credentials (username/password) and the gateway adds all complementary information like scope, client_id, grant_type etc... before forwarding the request to the authorization server.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if ("x-www-form-urlencoded".equals(request.getHeaders().getContentType().getSubtype())) {
//This code is not executed, the call of formData.put does not do anything, even a breakpoint is not reached!
if (request.getMethod().equals(HttpMethod.POST)) {
exchange.getFormData().map(formData -> {
formData.put("key1", List.of("value1"));
formData.put("key2", List.of("value2"));
formData.put("key3", List.of("value3"));
return formData;
});
}
//This part of code works well, the header is added to the forwarded request
requestBuilder.header(HttpHeaders.AUTHORIZATION,
"Basic " + Base64Utils.encodeToString((this.uiClientId + ":" + this.uiClientSecret).getBytes()));
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
}
I use the filter like this:
- id: keycloak_token_route
uri: http://localhost:8180
predicates:
- Path=/kc/token
filters:
- OAuth2CredentialsAppender
- SetPath=/auth/realms/main/protocol/openid-connect/token
- name: RequestRateLimiter
args:
key-resolver: "#{#userIpKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 30
denyEmptyKey: false
The filter is well invoked but altering the incoming request body does not work.
I am new to the reactive world so I am a bit confused, any help will be appreciated.
For those who would like to do the same thing, this is how I solved my problem. Again I am not an expert of Reactive programming, I am still learning it so it might be a better answer.
#Component
public class OAuth2CredentialsAppenderGatewayFilterFactory extends AbstractGatewayFilterFactory<OAuth2CredentialsAppenderGatewayFilterFactory.Config> {
#Value("${uiservice.clientId}")
private String uiClientId;
#Value("${uiservice.clientSecret}")
private String uiClientSecret;
public OAuth2CredentialsAppenderGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (ServerWebExchange exchange, GatewayFilterChain chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if (nonNull(request.getHeaders().getContentType()) && request.getHeaders().getContentType().equals(MediaType.APPLICATION_FORM_URLENCODED)) {
if (requireNonNull(request.getMethod()).equals(HttpMethod.POST)) {
//Use this filter to modify the request body
ModifyRequestBodyGatewayFilterFactory.Config requestConf = new ModifyRequestBodyGatewayFilterFactory.Config()
.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.setRewriteFunction(String.class, String.class, this.completeRequestBody());
requestBuilder.header(HttpHeaders.AUTHORIZATION, base64Encoding(this.uiClientId, this.uiClientSecret));
return new ModifyRequestBodyGatewayFilterFactory().apply(requestConf).filter(exchange.mutate().request(requestBuilder.build()).build(), chain);
}
}
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
/** Add some config params if needed */
public static class Config {
}
/** Complete request by adding required information to get the access token. Here we can get 2 type of token: client_credentials or password. If the param client_only=true we should get a client_credentials token */
private RewriteFunction<String, String> completeRequestBody() {
return (ServerWebExchange ex, String requestBody) -> {
requireNonNull(requestBody, "Body is required");
//if body contains only this, we should get a client_credentials token
var idForClientCredentialsOnly = "client=ui&client_only=true";
String finalRequestBody;
var joiner = new StringJoiner("");
if (idForClientCredentialsOnly.equalsIgnoreCase(requestBody)) {
joiner.add("grant_type=").add("client_credentials");
}
else {
joiner.add(requestBody);
if (!containsIgnoreCase(requestBody, "grant_type")) {
joiner.add("&grant_type=").add("password");
}
}
if (!containsIgnoreCase(requestBody, "scope")) {
joiner.add("&scope=").add("uiclient");//I use Keycloak so I specify the scope to get some extra information
}
finalRequestBody = joiner.toString();
return Mono.just(isBlank(finalRequestBody) ? requestBody : finalRequestBody);
};
}
}

MongoDB Panache queries not returning any result

I am having problems with querying with MongoDB.
Whenever I try to find by ID or any other field, I always get zero results back.
And I am also having trouble to use the 'like' operator.
What I wanna query is the titles of the books in a case insensitive way. And I know you can do it like this in MongoDB:
{title: {$regex: /^query.*/i}}
And I try to do this with Panache but I can't get it working:
Book.find("title like ?1", "/^" + query + ".*/i");
I see the following line on the console getting printed out:
{'title':{'$regex':'/^Harr.*/i'}}
I've also tried it with a Document, but also not success:
Document query = new Document();
query.put("title", "$regex: /^" + query + ".*/i");
Book.find(query);
And I get zero results back.
And here is my Book class:
public class Book extends PanacheMongoEntity {
#NotEmpty
public String title;
#NotEmpty
public String isbn;
#NotEmpty
public String author;
#Min(1)
#NotNull
public BigDecimal price;
}
And here is my BookResource:
#Produces(APPLICATION_JSON)
#Consumes(APPLICATION_JSON)
#Path("/books")
public class BookResource {
#GET
public List<Book> get() {
return Book.listAll();
}
#GET
#Path("/{id}")
public Book find(#PathParam("id") String id) {
return (Book) Book.findByIdOptional(id).orElseThrow(() -> new NotFoundException("Book not found"));
}
#GET
#Path("/title/{title}")
public PanacheQuery<PanacheMongoEntityBase> findByTitle(#PathParam("title") String title) {
Document query = new Document();
// query.put("title", new BasicDBObject("$regex", ).append("$options", "i"));
return Optional.ofNullable(Book.find("title like ?1", format("/^%s.*/i", title))).orElseThrow(() -> new NotFoundException("Book not found"));
}
#POST
public void add(#Valid Book book) {
Book.persist(book);
}
#PUT
#Path("/{id}")
public void update(#PathParam("id") String id, #Valid Book book) {
Book result = Book.findById(id);
if (result != null) {
result.author = book.author;
result.isbn = book.isbn;
result.price = book.price;
result.title = book.title;
Book.update(result);
}
}
}
When I do a findAll via curl I get this:
[
{
"id": "5eb9475b8a4314145246cc10",
"author": "J.K. Rowling",
"isbn": "12345678",
"price": 24.95,
"title": "Harry Potter 1"
},
{
"id": "5eb95a758a4314145246cc25",
"isbn": "456",
"price": 0,
"title": "Java for dummies"
},
{
"id": "5eb95b1a8a4314145246cc27",
"author": "steven king",
"isbn": "456",
"price": 10,
"title": "IT"
}
]
And when I try to find a Book by id, I also get zero results:
curl -X GET "http://localhost:8080/books/5eb9475b8a4314145246cc10" -H "accept: application/json"
=> 404 No Books found.
The only operation that seems to work is the POST (persist), the rest of the methods don't return anything.
I have the following setup:
MongoDB 4.2 running via Docker
Quarkus 1.4.2
JDK 11
And here is my application.properties:
quarkus.mongodb.connection-string=mongodb://localhost:27017
quarkus.mongodb.database=books
quarkus.log.category."io.quarkus.mongodb.panache.runtime".level=DEBUG
The following piece of code is wrong:
#GET
#Path("/title/{title}")
public PanacheQuery<PanacheMongoEntityBase> findByTitle(#PathParam("title") String title) {
Document query = new Document();
//query.put("title", new BasicDBObject("$regex", ).append("$options", "i"));
return Optional.ofNullable(Book.find("title like ?1", format("/^%s.*/i", title))).orElseThrow(() -> new NotFoundException("Book not found"));
}
Book.find() return a PanacheQuery, you need to call one of the terminator operations of the query to get the results: list/stream/findFirst/findOne.
I would suggest implementing it like this:
#GET
#Path("/title/{title}")
public List<Book> findByTitle(#PathParam("title") String title) {
return Book.list("title like ?1", format("/^%s.*/i", title)));
}
list() is a shortcut for find().list(), if you don't need pagination or other things that are available on the PanacheQuery interface you can directly use list().

Call Paginated Spring rest api from another spring project

I build one API which is paginated, its output looks like this:
{
"content": [
{JSON1},
{JSON2},
...
{JSON20}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"pageSize": 20,
"pageNumber": 0,
"unpaged": false,
"paged": true
},
"totalPages": 2,
"totalElements": 32,
"last": false,
"size": 20,
"number": 0,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 20,
"first": true,
"empty": false
}
So for this call, I have two pages and one each page we have 20 JSON entity is coming.
I wanted to call this same endpoint from the rest template.
Before pagination I used to call the same endpoint like this:
MyEntity[] responseEntity;
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("Accept", MediaType.APPLICATION_JSON_VALUE);
// request entity is created with request headers
HttpEntity<MyEntity> requestEntity = new HttpEntity<>(requestHeaders);
Map<String, String> params = new HashMap<>();
params.put("feild1", val1);
params.put("feild2", val2);
responseEntity = restTemplate.getForObject(ApiEndpoint,MyEntity[].class,params);
As the endpoint was returning in the format of Array of MyEntity, above code as good enough. Now I have paginated rest endpoint.
How should I call the paginated endpoint and get the Array of MyEntity data again?
So far I have tried calling with: which is not working for me.
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ApiEndpoint);
for (Map.Entry<String, String> entry : params.entrySet()) {
builder.queryParam(entry.getKey(), entry.getValue());
}
ResponseEntity<MyEntity[]> response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, requestEntity, MyEntity[].class);
Let me know if another way you have can be implemented here. Thanks for your help in advance.
What i did is, created the new class RestPageImpl
#JsonIgnoreProperties(ignoreUnknown = true)
public class RestPageImpl<ConfigurationTable> extends PageImpl<ConfigurationTable> {
private static final long serialVersionUID = -1423116752405536063L;
#JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public RestPageImpl(
#JsonProperty("content") List<ConfigurationTable> content,
#JsonProperty("number") int number, #JsonProperty("size") int size,
#JsonProperty("totalElements") Long totalElements, #JsonProperty("pageable") JsonNode pageable,
#JsonProperty("last") boolean last, #JsonProperty("totalPages") int totalPages,
#JsonProperty("sort") JsonNode sort, #JsonProperty("first") boolean first,
#JsonProperty("numberOfElements") int numberOfElements) {
super(content, PageRequest.of(number, size), totalElements);
}
public RestPageImpl(List<ConfigurationTable> content, Pageable pageable,
long total) {
super(content, pageable, total);
}
public RestPageImpl(List<ConfigurationTable> content) {
super(content);
}
public RestPageImpl() {
super(new ArrayList<>());
}
}
And on the controller side updated my code to:
MyEntity[] responseEntity =null;
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("Accept", MediaType.APPLICATION_JSON_VALUE);
// request entity is created with request headers
HttpEntity<MyEntity> requestEntity = new HttpEntity<>(requestHeaders);
Map<String, String> params = new HashMap<>();
params.put("feild1", val1);
params.put("feild2", val2);
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ApiEndpoint);
for (Map.Entry<String, String> entry : params.entrySet()) {
builder.queryParam(entry.getKey(), entry.getValue());
}
ParameterizedTypeReference<RestPageImpl<MyEntity>> type = new ParameterizedTypeReference<RestPageImpl<MyEntity>>() {
};
responseEntity = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, requestEntity, type);
And everything works fine now.

Symfony 3.2 ODM Doctrine array of MongoId's

How to make array of MongoId's in Symfony 3.2 Doctrine ODM serialized/deserialized properly ?
Document
namespace Acme\StoreBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document(collection="Voter")
*/
class Voter
{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\Collection
*/
protected $inlist;
/**
* Get id
*
* #return id $id
*/
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Set inlist
*
* #param collection $inlist
* #return $this
*/
public function setInlist($inlist)
{
$this->inlist = $inlist;
return $this;
}
/**
* Get inlist
*
* #return collection $inlist
*/
public function getInlist()
{
return $this->inlist;
}
}
Controller:
/**
* #Route("/voter/{id}")
* #Method({"GET"})
*/
public function getAction($id)
{
$product = $this->get('doctrine_mongodb')
->getRepository('AcmeStoreBundle:Voter')
->find($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id ' . $id);
}
$serializer = $this->get('serializer');
$data = $serializer->serialize(
$product,
'json'
);
$response = new JsonResponse();
$response->setContent($data);
return $response;
}
Serialized Json:
{
"id": "593e99a8de6c84f5ecec3094",
"inlist": [
{
"timestamp": 1417718686,
"pID": 3335,
"inc": 9127278,
"$id": "5480ab9e282e26070d8b456e"
},
{
"timestamp": 1417718686,
"pID": 3335,
"inc": 9127273,
"$id": "5480ab9e282e26070d8b4569"
},
{
"timestamp": 1417718686,
"pID": 3335,
"inc": 9127272,
"$id": "5480ab9e282e26070d8b4568"
},
{
"timestamp": 1417718686,
"pID": 3335,
"inc": 9127275,
"$id": "5480ab9e282e26070d8b456b"
},
{
"timestamp": 1417718686,
"pID": 3335,
"inc": 9127274,
"$id": "5480ab9e282e26070d8b456a"
},
{
"timestamp": 1411754988,
"pID": 2674,
"inc": 9127271,
"$id": "5425abec8f3723720a8b4567"
}
]
}
I want it to be serialized to be an array of (string) id's, and deserialize it back to array of MongoId's:
{
"id": "593e99a8de6c84f5ecec3094",
"inlist": ['5425abec8f3723720a8b4567', '5480ab9e282e26070d8b456b' ...]
}
Since you mentioned "right way" in the comments here's how I would do it:
class VoterAPIRepresentation
{
public $id;
public $inlist = [];
public function __construct(Voter $voter)
{
$this->id = (string) $voter->id;
foreach ($voter->inlist as $i) {
$this->inlist[] = (string) $i['$id'];
}
}
}
Above class is responsible for data representation in API since entity itself shouldn't be concerned with that. Then in controller:
public function getAction($id)
{
$product = $this->get('doctrine_mongodb')
->getRepository('AcmeStoreBundle:Voter')
->find($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id ' . $id);
}
$serializer = $this->get('serializer');
$data = $serializer->serialize(
new VoterAPIRepresentation($product),
'json'
);
$response = new JsonResponse();
$response->setContent($data);
return $response;
}
Pro of this approach is that if you change the entity you don't need to be concerned with any endpoints and data they're returning since these two beings are not connected. On the other hand writing such classes is quite boring BUT it pays off for complex objects and representations.
setter/getter way worked for me even with serializer / deserializer
/**
* Set actions
*
* #param collection $actions
* #return $this
*/
public function setActions($actions)
{
$re = [];
foreach ($actions as $action) {
$action['cid'] = new \MongoId($action['cid']);
$re[] = $action;
}
$this->actions = $re;
return $this;
}
/**
* Get actions
*
* #return collection $actions
*/
public function getActions()
{
$re = [];
foreach ($this->actions as $action) {
$action['cid'] = (string)$action['cid'];
$re[] = $action;
}
return $re;
}

#ModelAttribute for Rest PUT - request param null

I need to populate my pojo class based on the request param 'type'.
so I have code like
#ModelAttribute
public void getModelObject(HttpServletRequest request, ModelMap modelMap) {
String typeCombo = request.getParameter("type");
System.out.println("typeCombo: " + typeCombo);
if (typeCombo != null) {
if (condition) {
modelMap.addAttribute("modelObj", new ClassB()); //ClassB extends ClassA
} else if (another condition) {
modelMap.addAttribute("modelObj", new ClassC()); //ClassC extends ClassA
} else {
System.out.println("no type found");
}
} else {
System.out.println("typecombo null");
}
}
I use above method to get create correct subclasses which will be used to add / update. The above one works fine in case of "POST" - for creating a record. But for "PUT" request.getParameter("type") always returns null. So for editing, I'm not able to get correct subclasses.
Below are my post and put request mapping:
#RequestMapping(value = "", method = RequestMethod.POST, headers = "Accept=*/*")
#ResponseBody
public String addCredentials(#ModelAttribute("modelObj") Credential credential,
ModelMap modelMap) {
//code
}
#RequestMapping(value = "/edit/{id}", method = RequestMethod.PUT, headers = "Accept=*/*")
#ResponseBody
public Credential editCredential(#ModelAttribute ("modelObj") Credential credential, #PathVariable long id, ModelMap model) {
//code
}
Any help is much appreciated.
Register the filter HttpPutFormContentFilter like this:
<beans:bean id="httpPutFormContentFilter"
class="org.springframework.web.filter.HttpPutFormContentFilter" />