SpringDoc generate OpenApi spec with multiple media types - spring

In my Spring project I'm using Springdoc to generate a OpenApiSpecification doc.
I created my Api with these annotations. I want to have the same endpoint url with different mediatype to handle the POST of different objects.
#Validated
#Tag(name = "Calendar", description = "Api for Calendar resource")
public interface CalendarApi {
#Operation(summary = "Add an appointment to the calendar", description = "Add an appointment to the calendar", tags = {"appointment"})
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Successful operation", content = #Content(mediaType = "application/json+widget", schema = #Schema(implementation = AppointmentWidgetDto.class))),
#ApiResponse(responseCode = "400", description = "Invalid input")
})
#PostMapping(value = "/appointments", consumes = "application/json+widget")
ResponseEntity<Appointment> saveFromWidget(#Parameter(description = "The new appointment to save", required = true) #Valid #RequestBody AppointmentWidgetDto appointmentDto);
#Operation(summary = "Add an appointment to the calendar", description = "Add an appointment to the calendar", tags = {"appointment"})
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Successful operation", content = #Content(mediaType = "application/json", schema = #Schema(implementation = Appointment.class))),
#ApiResponse(responseCode = "400", description = "Invalid input")
})
#PostMapping(value = "/appointments", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Appointment> save(#Parameter(description = "The new appointment to save", required = true) #Valid #RequestBody Appointment appointmentDto);
}
The generated Open Api Spec document is:
/api/v1/appointments:
post:
tags:
- Calendar
summary: Add an appointment to the calendar
description: Add an appointment to the calendar
operationId: save_1
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Appointment'
application/json+widget:
schema:
$ref: '#/components/schemas/AppointmentWidgetDto'
required: true
responses:
'201':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Appointment'
'400':
description: Invalid input
content:
'*/*':
schema:
$ref: '#/components/schemas/Appointment'
I've a couple of porblems:
the endpoint name is not meaningful (save_1)
when I use Open Api generator to generate the Angular client from this specification, I've some warnings that prevent the generation of both methods.
[WARNING] Multiple schemas found in the OAS 'content' section, returning only the first one (application/json)
[WARNING] Multiple MediaTypes found, using only the first one
I know there is this issue opened (https://github.com/OpenAPITools/openapi-generator/issues/3990).
Is there any way to permit to POST two different bodies in the same endpoint url and using OpenApi generator to create client for different languages/platforms?
===== UPDATE =======
This is AppointmentWidgetDTO:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#SuperBuilder
public class AppointmentWidgetDto implements Serializable {
#NotNull(message = "{appointment.store.missing}")
#JsonDeserialize(using = StoreUriDeserializer.class)
private Store store;
#NotNull(message = "{appointment.title.missing}")
#Size(max = 255)
private String title;
#Lob
#Size(max = 1024)
private String description;
#Size(max = 50)
private String type;
#Size(max = 50)
private String icon;
#NotNull(message = "{appointment.startdate.missing}")
private Instant startDate;
#NotNull(message = "{appointment.enddate.missing}")
private Instant endDate;
#JsonDeserialize(using = ContactUriDeserializer.class)
private Contact contact;
#NotBlank(message = "{appointment.contactname.missing}")
private String contactName;
#NotBlank(message = "{appointment.email.missing}")
#Email
private String contactEmail;
#NotBlank(message = "{appointment.phone.missing}")
#PhoneNumber
private String contactPhone;
}
and this is Appointment:
#ScriptAssert(lang = "javascript", script = "_.startDate.isBefore(_.endDate)", alias = "_", reportOn = "endDate", message = "{appointment.invalid.end.date}")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#SuperBuilder
public class Appointment extends AbstractEntity {
#NotNull(message = "{appointment.store.missing}")
#JsonDeserialize(using = StoreUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "store_id", updatable = false)
private Store store;
#NotNull
#Size(max = 255)
#Column(nullable = false, length = 255)
private String title;
#Lob
#Size(max = 1024)
#Column(length = 1024)
private String description;
#Size(max = 30)
#Column(length = 30)
private String color;
#Size(max = 50)
#Column(length = 50)
private String type;
#Size(max = 50)
#Column(length = 50)
private String icon;
#Size(max = 255)
#Column(length = 255)
private String location;
#NotNull
#Column(nullable = false)
private Instant startDate;
#NotNull
#Column(nullable = false)
private Instant endDate;
#Builder.Default
#NotNull
#Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
private boolean allDay = false;
#JoinColumn(name = "contact_id")
#JsonDeserialize(using = ContactUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY)
private Contact contact;
private String contactName;
#Email
private String contactEmail;
#PhoneNumber
private String contactPhone;
#JoinColumn(name = "agent_id")
#JsonDeserialize(using = AgentUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY)
private Agent agent;
private String agentName;
#Builder.Default
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#NotNull
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private AppointmentStatus status = AppointmentStatus.VALID;

With OpenAPI 3, you can not have many operations for the same path.
You will have only one endpoint and only one OpenAPI description.
What you can do is to define the #Operation annotation on the top of one of the methods, where you add the OpenAPI documentation of the merged OpenAPI description of all your other methods as well and add the #Hidden annotation on the others.
Or you can define two different groups: For each one you filter using header matching, option headersToMatch of GroupedOpenApi Bean.

Related

Multipartfile charset=UTF-8 is not supported spring boot api rest

The code was working normally and I've tried in every way to solve it and I couldn't, it may be that after I transformed the MultipartFile into an array this happened
#RestController
#RequestMapping("products")
public class ProductController {
#Autowired
private ProductService productService;
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#Transactional
public ResponseEntity<ShowProductsDTO> registerProduct(
#RequestBody #Valid ProductDTO dto,
#RequestParam(name = "files", required = true) MultipartFile[] files,
UriComponentsBuilder uriBuilder) {
ShowProductsDTO showProductsDTO = null;
try {
showProductsDTO = productService.save(dto, files);
} catch (IOException e) {
e.printStackTrace();
}
var uri = uriBuilder.path("/products/{id}").buildAndExpand(showProductsDTO.id()).toUri();
return ResponseEntity.created(uri).body(showProductsDTO);
}
DTO
public record ProductDTO(
#NotBlank
String name,
#NotBlank
String description,
#NotNull
#NumberFormat
BigDecimal price,
#NumberFormat
#NotNull
Integer quantity,
#NotNull
Boolean active,
#NotNull
Long sub_category_id
) {
}
Error console
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException:
Content-Type
'multipart/form-data;boundary=--------------------------816548045966415708649211;charset=UTF-8'
is not supported]
Postman body > raw > json
{
"name": "Nome do produto",
"description": "descricao do produto",
"price": "2500.00",
"quantity": "2",
"active": "true",
"sub_category_id": "1"
}
Postman > body > form-data
KEY "files", TYPE file, VALUE uma imagem minha em png
Error postman
{
"timestamp": "2023-01-11T06:15:43.455+00:00",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content-Type 'multipart/form-data;boundary=--------------------------056640214920648036756520;charset=UTF-8' is not supported.",
"path": "/products"
}
Product entity
#Table(name = "products")
#Entity(name = "Product")
#Getter
#Setter
#NoArgsConstructor
#EqualsAndHashCode(of = "id")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(length = 100, unique = true, nullable = false)
private String name;
#Column(nullable = false, columnDefinition = "TEXT")
private String description;
#Column(length = 8, nullable = false, columnDefinition = "NUMERIC(8,2)")
private BigDecimal price;
#Column(nullable = false, columnDefinition = "INT")
private Integer quantity;
#Column(nullable = false, columnDefinition = "BOOLEAN")
private Boolean active;
#CollectionTable(name = "products_files",
joinColumns =
#JoinColumn(name = "product_id", referencedColumnName = "id"))
private List<String> productFiles;
#JoinColumn(name = "sub_category_id")
#ManyToOne(fetch = FetchType.EAGER)
private SubCategory subCategory;
how do I fix this error?
Change your attribute to #PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
and call your api using Postman body > raw > json.
The thing is, Content-Type: form-data handles file requests.

how to specify List of entites in an entity using JPA

I have two entities. A vulnerability can have multiple vulnerability identifiers.
#Entity
#JsonInclude(Include.NON_NULL)
#ApiModel(parent = ApprovableEntity.class)
public class Vulnerability {
...
#JsonProperty("vulnerabilityIdentifiers")
#JoinColumn(name = "vulnerabilityidentifier_id")
#JsonView(JsonViews.BasicChangeLogView.class)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<VulnerabilityIdentifier> vulnerabilityIdentifiers;
...
}
#Entity
#ApiModel(parent = ApprovableEntity.class)
public class VulnerabilityIdentifier {
...
#ManyToOne
#JoinColumn(name = "vulnerability_id", referencedColumnName = "id")
#NotNull(message = "vulnerability is required")
#JsonView({JsonViews.BasicApprovableView.class, JsonViews.BasicChangeLogView.class,
JsonViews.ChangeLogAnswerView.class, JsonViews.DraftAnswerView.class})
#ApiModelProperty(hidden = true)
private Vulnerability vulnerability;
#Column(name = "type")
#JsonProperty("type")
#Size(max = 12)
#NotNull(message = "CVEID type required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String cveIdType;
#Column(name = "value")
#JsonProperty("value")
#Size(max = 24)
#NotNull(message = "value is required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String value;
...
}
Now when I send in a json request to the endpoint like as under, the application throws exception that it cannot map the type and value fields in the vulnerabilityIdentifier field.
A sample json request
{
"vulnerabilityImpacts": {
},
"vulnerabilityIdentifiers": [{"type": "cveId", "value": "CVE-1234-12345"}],
"vulnerableThreeppcomponents": [],
"internalSource": "**",
"cveId": "*****",
......
}
Both the cveId and value properties are annotated with #ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) which means they are ignored when deserializing. Remove this annotation from both properties.

Can't get products by userId from repository

I have 2 tables. One of them called 'products'
#Data
#Entity
#Table(name = "products")
#NoArgsConstructor
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(length = 100)
#NotBlank(message = "Name must be written")
private String name;
#Column(length = 200)
#NotBlank(message = "Provide image (link in this case) of your product")
private String image;
#PositiveOrZero
private int amount;
#Column(length = 250)
#NotBlank(message = "description must be written")
#Size(min = 10, max = 250, message = "description is too long or empty")
private String description;
#PositiveOrZero
private float price;
#ManyToOne
#JoinColumn(name = "type_id")
private ProductType productType;
public Product(#NotBlank String name, String image, int amount, #NotBlank String description,
#PositiveOrZero float price, ProductType productType) {
this.name = name;
this.image = image;
this.amount = amount;
this.description = description;
this.price = price;
this.productType = productType;
}
}
another table is 'users'
#Data
#Entity
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(length = 50)
#Size(min = 2, max = 30, message = "enter appropriate amount of letters, min 2")
private String username;
#Column(length = 100)
#Email(message = "Enter a valid email")
#NotBlank(message = "email should have a value")
private String email;
#Column(length = 50)
#NotBlank(message = "password should have a value")
#Size(min = 6, message = "password should at least consist of 6 characters")
private String password;
private boolean enabled;
private String role;
public User(#Size(min = 2, max = 30, message = "enter appropriate amount of letters, min 2")
String username,
#Email(message = "Enter a valid email")
#NotBlank(message = "email should have a value") String email,
#NotBlank(message = "password should have a value")
#Size(min = 6, message = "password should at least consist of 6 characters")
String password, boolean enabled, String role) {
this.username = username;
this.email = email;
this.password = password;
this.enabled = enabled;
this.role = role;
}
}
and also table that include both 'product_user' (many to many relationship)
it looks like this
#Data
#Entity
#Table(name = "product_user")
#AllArgsConstructor
#NoArgsConstructor
public class ProdAndUser{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#ManyToOne
#JoinColumn(name = "product_id")
Product product;
#ManyToOne
#JoinColumn(name = "user_id")
User user;
public ProdAndUser(Product product, User user) {
this.product = product;
this.user = user;
}
}
then I tried to get them from prodAndUser repository by UserId or by User as obj:
#Repository
public interface ProdAndUserRepository extends JpaRepository<ProdAndUser, Integer> {
List<ProdAndUser> getProdAndUsersByUserId(Integer id);
List<ProdAndUser> getAllByUser(User user);
}
my controller looks like this:
#ResponseBody
#GetMapping("/findByUsr/{user}")
public List<ProdAndUser> getByUser(#PathVariable User user){
return prodAndUserRepository.getAllByUser(user);
}
error:
{
"timestamp": "2022-02-12T05:52:53.165+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/Cart/findByUsr"
}
I have tried to find them all by .findAll() and it worked fine. Also another tables work fine on their own
Look at the error it says (404) means something is not right with the path.
The path on the error does not contain user_id.

How to retrieve data based on inverseColumn data using CrudRepository in springboot?

I have two tables i.e. users and events. Users table will be filled when new user will sign up. Later same user can create calendar events. so events table will be filled and users_events will keep mapping of events based on user.
I would like to find all events based on logged in userId. so here is query, it should return data based on it.
select * from events where eventid in (select eventId from users_event where id_user=x ). Here is my Users and Event Entity class.
User.java
#Entity
#Table(name = "users")
public class User {
#Id
#GenericGenerator(name = "generator", strategy = "increment")
#GeneratedValue(generator = "generator")
#Column(name = "id", nullable = false)
private Long id;
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "family_name", nullable = false)
private String familyName;
#Column(name = "e_mail", nullable = false)
private String email;
#Column(name = "phone", nullable = false)
private String phone;
#Column(name = "language", nullable = false)
private String language;
#Column(name = "id_picture")
private String pictureId;
#Column(name = "login", nullable = false)
private String login;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "birth_date")
private Date birthDate;
#Column(name = "enabled")
private Boolean enabled;
//getter and setter
Event.java
#Entity
#Table(name = "events")
public class Event {
#Id
#GenericGenerator(name = "generator", strategy = "increment")
#GeneratedValue(generator = "generator")
#Column(name = "eventId", nullable = false)
private Long eventId;
#Column(name = "title", nullable = false)
private String title;
#Column(name = "description", nullable = true)
private String description;
#Column(name = "startAt", nullable = false)
#Temporal(TemporalType.TIMESTAMP)
private Date startAt;
#Column(name = "endAt", nullable = true)
#Temporal(TemporalType.TIMESTAMP)
private Date endAt;
#Column(name = "isFullDay", nullable = false)
private Boolean isFullDay;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "users_event", joinColumns = { #JoinColumn(name = "id_event", referencedColumnName = "eventId") }, inverseJoinColumns = { #JoinColumn(name = "id_user", table = "users", referencedColumnName = "id") })
private Set<User> user = new HashSet<User>();
/getter and setter
EventRepo.java
public interface EventRepo extends CrudRepository<Event, Long> {
Event findByUser(Set<User> user);
}
I am trying to implement something, which can give me output of this query.
select * from events where eventid in (select eventId from users_event where id_user=x )
here is my implementation.any input please?
#RequestMapping(value = "/events", method = RequestMethod.GET)
public #ResponseBody List<Event> getEvents() {
logger.debug("get event list");
User x=new User();
x.setId(1);
Set<User> user= new HashSet();
user.add(x);
return (List<Event>) eventRepo.findByUser(user);
}
Just add a following method to your EventRepo:
List<Event> findAllByUserId(Long userId);
And modify your controller to something like this:
#RequestMapping(value = "/events", method = RequestMethod.GET)
public List<Event> getEvents() {
return eventRepo.findAllByUserId(1L);
}

one to many mapping in hibernate for a messaging service

I have two JPA entities: User and Message.
Each Message has one sender and one receiver of type User. And on the other side each User has two sets of type Message: inbox and outbox.
Message:
#Entity
#Table(name = "messages")
public class Message {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "seq")
#SequenceGenerator(name = "seq", sequenceName = "MESSAGES_SEQ")
#Column(name = "ID", insertable = false, updatable = false)
private int id;
#ManyToOne
#JoinColumn(name = "SENDER")
private User sender;
#ManyToOne
#JoinColumn(name = "RECEIVER")
private User receiver;
private String subject, content;
private Date sdate;
//getters and setters
}
All the properties which not being mapped with an annotation has he same name as the columns in database and are automatically mapped by JPA.
User:
#Entity
#Table(name = "users")
public class User {
#Column(name = "USERNAME")
private String username;
#Column(name = "PASSWORD")
private String pass;
#Column(name = "EMAIL")
private String email;
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "seq")
#SequenceGenerator(name = "seq", sequenceName = "USERS_SEQ")
#Column(name = "ID", insertable = false, updatable = false)
private int id;
#OneToMany(mappedBy = "uploader")
private Set<Book> books;
#OneToMany(mappedBy = "receiver")
private Set<Message> inbox;
#OneToMany(mappedBy = "sender")
private Set<Message> outbox;
//getters and setters
}
The problem is, when I select an User from Oracle database, then the inbox property is empty. How is this caused and how can I solve it?

Resources