I've got a SOAP web-service server using Apache CXF as implementation. Due to some external technical constraint I'd like to be able to rename some XML tags naming an operation parameter (which are deprecated) in the inbound SOAP request. I was reading about using Interceptors for this, but the documentation on how to setup/configure them is not very clear.
My code to publish an endpoint is the following:
Endpoint endpoint = Endpoint.create(
"http://schemas.xmlsoap.org/soap/", new MyServer());
endpoint.publish("ws/endpoint");
Ideally I'd like to add a filter only to a given endpoint (I have several of them).
Apache's documentations about interceptors are quite clear (IMO), anyway, there is a helloworld project (based on spring boot, cxf and maven) in my github profile which you can take a look for setting up interceptors (in fact it's a baisc autentication interceptor).
For setting up an interceptor (e.g InInterceptor), your class should extend AbstractPhaseInterceptor<Message> and override handleMessage(Message message) method, then in the constructor you should declare the phase in which the interceptor is going to be applied. Finally you have to instantiate it and apply in on an Endpoint.
As you said:
rename some XML tags naming an operation parameter (which are
deprecated) in the inbound SOAP request
I think the name of the operation parameter (in WSDL file) is something different from the argument of your web method. Suppose that there is method in your endpoint named addPerson:
#WebMethod
String addPerson(Person person) {
/*method logic*/
}
and Person class:
class Person {
private String firstName;
private String lastName;
private Date birthDate;
//getters and setters
}
in order to map lastName property to a different name, you have to annotate it with
#XmlElement(name = "sureName")
private String lastName;
after applying this anotation, sureName (in wsdl file) is going to be mapped to lastName.
In addition, there is #WebParam annotation which can be used for changing the name of web method arguments:
#WebMethod
String sayHello( #WebParam(name = "sureName") String lastName);
Hope it helps.
on official Api-Platform website there is a General Design Considerations page.
Last but not least, to create Event Sourcing-based systems, a convenient approach is:
to persist data in an event store using a custom data persister
to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
to map those projections with read-only Doctrine entity classes and to mark those classes with #ApiResource
You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.
So, I tried to implement this approach with one simplification (one DB is used, but with separated reads and writes).
But failed... there is a problem, which I don't know how to resolve, so kindly asking you for a help!
I created a User Doctrine entity and annotated fields I want to expose with #Serializer\Groups({"Read"}). I will omit it here as it's very generic.
User resource in yaml format for api-platform:
# config/api_platform/entities/user.yaml
App\Entity\User\User:
attributes:
normalization_context:
groups: ["Read"]
itemOperations:
get: ~
collectionOperations:
get:
access_control: "is_granted('ROLE_ADMIN')"
So, as it's shown above User Doctrine entity is read-only, as only GET methods are defined.
Then I created a CreateUser DTO:
# src/Dto/User/CreateUser.php
namespace App\Dto\User;
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
final class CreateUser
{
/**
* #var string
* #Assert\NotBlank()
* #Assert\Email()
* #AppAssert\FakeEmailChecker()
*/
public $email;
/**
* #var string
* #Assert\NotBlank()
* #AppAssert\PlainPassword()
*/
public $plainPassword;
}
CreateUser resource in yaml format for api-platform:
# config/api_platform/dtos/create_user.yaml
App\Dto\User\CreateUser:
itemOperations: {}
collectionOperations:
post:
access_control: "is_anonymous()"
path: "/users"
swagger_context:
tags: ["User"]
summary: "Create new User resource"
So, here you can see that only one POST method is defined, exactly for creation of a new User.
And here what router shows:
$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name Method Scheme Host Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection POST ANY ANY /users
api_users_get_collection GET ANY ANY /users.{_format}
api_users_get_item GET ANY ANY /users/{id}.{_format}
I also added a custom DataPersister to handle POST to /users. In CreateUserDataPersister::persist I used Doctrine entity to write data, but for this case it doesn't matter as Api-platform do not know anything about how DataPersister will write it.
So, from the concept - it's a separation of reads and writes.
Reads are performed by Doctrine's DataProvider shipped with Api-platform, and writes are performed by custom DataPersister.
# src/DataPersister/CreateUserDataPersister.php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;
class CreateUserDataPersister implements DataPersisterInterface
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function supports($data): bool
{
return $data instanceof CreateUser;
}
public function persist($data)
{
$user = new User();
$user
->setEmail($data->email)
->setPlainPassword($data->plainPassword);
$this->manager->persist($user);
$this->flush();
return $user;
}
public function remove($data)
{
}
}
When I perform a request to create new User:
POST https://{{host}}/users
Content-Type: application/json
{
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
Problem!
I'm getting a 400 response ... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...
However, a new record is added to database, so custom DataPersister works ;)
According to General Design Considerations separations of writes and reads are implemented, but not working as expected.
I'm pretty sure, that I could be missing something to configure or implement. So, that's why it's not working.
Would be happy to get any help!
Update 1:
The problem is in \ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName(). At lines 48-59 it iterates through all routes trying to find appropriate route for:
$operationType = 'item'
$resourceClass = 'App\Dto\User\CreateUser'
But $operationType = 'item' is defined only for $resourceClass = 'App\Entity\User\User', so it fails to find the route and throws an exception.
Update 2:
So, the question could sound like this:
How it's possible to implement separation of reads and writes (CQS?) using Doctrine entity for reads and DTO for writes, both residing on the same route, but with different methods?
Update 3:
Data Persisters
store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
not publicly expose the internal model mapped with the database through the API
use a separate model for read operations and for updates by implementing patterns such as CQRS
Yes! I want that... but how to achieve it in my example?
Short Answer
The problem is that the Dto\User\CreateUser object is getting serialized for the response, when in fact, you actually want the Entity\User to be returned and serialized.
Long Answer
When API Platform serializes a resource, they will generate an IRI for the resource. The IRI generation is where the code is puking. The default IRI generator uses the Symfony Router to actually build the route based on the API routes created by API Platform.
So for generating an IRI on an entity, it will need to have a GET item operation defined because that is the route that will be the IRI for the resource.
In your case, the DTO doesn't have a GET item operation (and shouldn't have one), but when API Platform tries to serialize your DTO, it throws that error.
Steps to Fix
From your code sample, it looks like the User is being returned, however, it's clear from the error that the User entity is not the one being serialized.
One thing to do would be to install the debug-pack, start the dump server with bin/console server:dump, and add a few dump statements in the API Platform WriteListener: ApiPlatform\Core\EventListener\WriteListener near line 53:
dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);
The Controller Result should be an instance of your DTO, the Persist Result should be an instance of your User entity, but I'm guessing it's returning your DTO.
If it is returning your DTO, you need to just debug and figure out why the DTO is being returned from the dataPersister->persist instead of the User entity. Maybe you have other data persisters or things in your system that can be causing conflict.
Hopefully this helps!
You need to send the "id" in your answer.
If User is Doctrine entity, use:
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
If User isn't Doctrine entity, use:
/**
* #Assert\Type(type="integer")
* #ApiProperty(identifier=true)
*/
private $id;
Anyway, your answer would be like this:
{
"id": 1, // Your unique id of User
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
P.S.: sorry for my english :)
Only Work in the 2.4 version but really helpful.
Just add
output_class=false for the CreateUserDTO and everything will be fine for POST|PUT|PATCH
output_class to false allow you to bypass the get item operation. You can see that in the ApiPlatform\Core\EventListener#L68.
{"vehicle_number": "KA222009","vehicle_type":"sedan"}
This json I am sending a POST request and it gets saved !! I have used Request body serverside in spring boot so its fine up to here but when I send like
{"vehicle_number": "KA222009","vehicle_type":"sedan","username":"abc"}
this also gets saved with the matching attribute of my RequestBody class. I don't want in this way and restrict the user to play with the request. How can I do this?
Do you have an entity to save that request into? Like
class Vehicle{
String vehicle_number;
String vehicle_type;
}
You'd need to put in the parameters then something like this
myfunction(#RequestBody Vehicle vehicle)
I believe an exception should be thrown by then.
edit 1:
The class Vehicle should be annotated with #Entity
And it's good practice to return a ReponseEntity<?> instead of actual data types
edit 2:
On another note, if you want your Vehicle entity to have a username but restrict the user from changing the username, then you just have to exclude it from the Vehicle constructor.
To add to Rei Brown's answer, you will likely not only want to create a data class, you will also want Bean Validation on it. For example:
#RequestMapping(/*....*/)
public void save(#Valid #RequestBody Vehicle vehicle){
// ... save logic here
}
I have a REST webservice with method custom (GET).
#GET
#Path("/custom")
public UIResponse custom(final UIParameters uiParameters){...}
As you can see this method has one argument. This is my custom object.
Object UIParameters is built from argument given as query string.
eg. http://example.com/custom?objectType=article
UIParameters object will contain one field:
UIParameters {
String objectType = "article";
}
I have tried to use InInterceptor to get this parameter from URL, build UIParameter object and set Content of message. Unfortunatelly it doesn't work.
After that I've provide MessageBodyReader for UIParameters but it still doesn't work.
What should I do to achive this goal?
Thanks
Update:
In InInterceptor I've copied query string to http headers. Now part of URL where user place parameters is accessible in my MessageBodyReader. Here I can build my object UIParameters.
Everything works fine but I don't think that this solution is the best.
Does somebody know better solution?
AnnotationQueryParam("") allows to get all the query parameters injected.
You do not need an interceptor and it is not the recommended way. See CXF documentation http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Parameterbeans
#GET
#Path("/custom")
public UIResponse custom(#QueryParam("") UIParameters uiParameters)
class UIParameters {
String objectType;
}
If you want to build the bean yourself using the query parameters use #Context UriInfo annotation
#GET
#Path("/custom")
public UIResponse custom( #Context UriInfo uriInfo){
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
new UIParameters().Builder()
.objectType(params.getFirst("type"))
.build();
}
I have a property on my "Contact" entity:
public partial class Contact
{
public string FullName { get { return this.FirstName + this.LastName; } set { } }
}
I then use breeze get the Contact data from my Web API function that returns Contacts. My data returned from my Web API call has the "FullName" property and the correct value coming down to the client, but my "Metadata" does not have the "FullName" property anywhere in it. What do I need to do to get the Metadata?
I found no sensible solution to adding partial class items to the metatata from the server. My opinion is that this should be considered as a bug to the server Breeze metadata function.
However the extended items data do get served from the server.
So if you add the extended properties manually on the client metadata-store, everything should be fine.
Here is an example how you do it in the javascript client code:
var man = new breeze.EntityManager(myServiceName);
man.metadataStore.registerEntityTypeCtor('Contact', function () { this.FullName = ''; });
You don't really want that computed properties coming over the wire, do you? Why do you have a setter? This is a read-only property. And if this is an EF Code First class, how did you keep EF from believing that FullName is mapped to a "FullName" column in the "Contact" table?
I'm going to assume that you don't actually want the "FullName" to come over the wire. You want to extend the type with either a custom EntityType constructor or an initializer. I think you want an initializer in this case.
Take a look at "Extending Entities" which happens to illustrate the recommended technique with a fullName property.