Im trying to work through a few tutorials of quarkus and got a problem with creating a simple REST endpoint. Im following this tutorial: https://quarkus.io/guides/rest-data-panache .
Im using the approach from the guide to create an interface that extends PanacheEntityResource<Entity, Id>
public interface ActorResource extends PanacheEntityResource<Actor, Long>{
}
The respective Entity is:
#Entity
public class Actor extends PanacheEntity{
public String first_name;
public String last_name;
public Timestamp last_update;
public static List<Actor> findByFirstName(String name) {
return list("first_name", name);
}
}
As in the guide, doing it like this auto-generates the basic rest endpoints for getById, getAll, create, update and delete. As you can see in my Entity class I have a findByFirstName method which gets all Entities, which match the given method parameter "name" . Now I want to expose a REST endpoint for this method. Ive so far found a way to implement this, but that doesnt seem quite right. Ive had no luck with implementing the REST endpoint for the method directly into the interface
#ResourceProperties(path = "actors")
public interface ActorResource extends PanacheEntityResource<Actor, Long>{
#GET
#Produces("application/json")
#Path("/first_name={name}")
public static List<Actor> getByFirstName(#PathParam("name") String name) {
return Actor.findByFirstName(name);
}
}
No errors with this implementation, but the REST endpoint isnt exposed.
Now as I said, Ive found a way to do this and this is to create an interface as shown in the first code bracket with nothing in it and in addition to that a ResourceClass in which I implement my custom endpoint. To reduce the clutter (in the IDE) Ive combined this so the interface is created inside my ResourceClass like this (example is for another entity):
#Path("/countries")
#ApplicationScoped
#Produces("application/json")
public class CountryResource {
#GET
#Path("/name={name}")
public List<Country> getByName(#PathParam("name") String name) {
return Country.findByName(name);
}
#ResourceProperties(path = "/countries")
public interface CountryResourceTest extends PanacheEntityResource<Country, Long>{
}
}
This works, by creating the interface the basic rest endpoints are auto-generated and inside the resource class I can add other endpoints, but it just feels like this is not the right approach. Am I wrong, and this is just how Im supposed to do this, or is there a way to implement this with only the interface approach that was originally used in the guide?
I think adding custom endpoints straight to generated resource classes isnt supported yet because this is still an experimental feature for evaluation only. Feel free to give feedback as issues in quarkus' GitHub issue tracker.
I think the best way to add custom endpoints is by creating a second resource class with the same path. Then you can define your custom endpoints there. If you have no name collisions that will work. I tested it with the hibernate-orm-panache-quickstart with following code changes:
My Entity with my custom query:
#Entity
#Cacheable
public class Fruit extends PanacheEntity {
#Column(length = 40, unique = true)
public String name;
public Fruit() {
}
public Fruit(String name) {
this.name = name;
}
public static Fruit findByName(String name) {
return find("name=?1", name).firstResult();
}
}
Resource for generated rest-api (remoe the given resource in this tutorial):
public interface FruitResource extends PanacheEntityResource<Fruit, Long> {
}
My specific resource:
#Path("fruit")
#ApplicationScoped
#Produces("application/json")
#Consumes("application/json")
public class FruitSpecificResource {
#GET
#Path("/first_name={name}")
public Fruit getByName(#PathParam("name") String name) {
return Fruit.findByName(name);
}
}
I also added openapi and replaced postgres with h2 for testing purposes. Here are my dependencies:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-rest-data-panache</artifactId>
</dependency>
And i changed the application.properties to this:
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:test
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql
Ok, now i can start quarkus:dev and there will be a swagger-ui under localhost:8080/swagger-ui
The new endpoint is there. I tested it manually. It works.
Related
For example, I have a simple controller in Micronaut
#Controller("/hello")
public class MyApi implements Api {
#Override
public String hello(Integer i) {
return "Num: " + i;
}
}
#Validated
interface Api {
#Get("/")
String hello(#QueryValue #Max(10) Integer i);
}
Annotation #Max doesn't work, any numbers aren't validated. But the same example written in Spring works fine.
Everything works when add dependency
<dependency>
<groupId>io.micronaut.beanvalidation</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
I'm trying to use a Lookup strategy of JDBC repository:
#Query("select * from USERS where USERNAME = :id")
User findById(#Param("id") UserId id);
As you can see an id parameter has the custom type UserId. How can I convert it to a simple String? Should I register some converter? Where and how?
I also tried the next queries ... where USERNAME = :id.value, ... where USERNAME = :#{id.value} but they doesn't work.
UPD
This is my Spring Boot configuration:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
#Configuration
#EnableJdbcRepositories
public class PersistenceConfig extends AbstractJdbcConfiguration {
}
I analyzed a Spring source code and found that for such cases they used #WritingConverters (before I thought about #ReadingConverters, that's why i didn't get success) which can be registered in usual way through JdbcConfiguration:
#Configuration
#EnableJdbcRepositories
public class PersistenceConfig extends AbstractJdbcConfiguration {
#Override
#Bean
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(Arrays.asList(
new UserIdToStringConverter()));
}
}
where UserIdToStringConverter has the next implementation:
#WritingConverter
public class UserIdToStringConverter implements Converter<UserId, String> {
#Override
public String convert(UserId source) {
return (String) source.value();
}
}
UPD
I also found that ConditionalGenericConverter doesn't work. For conditional converters the method getConvertibleTypes() must return null (see here). but when I do so a matches method is not called. Look's like Spring bug (spring-data-jdbc-2.0.4.RELEASE).
This should work:
#Query("select * from USERS where USERNAME = :#{#id.value}")
User findById(#Param("id") UserId id);
Read more at https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
I am following spring data rest from https://spring.io/guides/gs/accessing-data-rest/ and I am only using
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
I would like to know how can I return all records (without pagination) but not using spring-boot-starter-web.I wants to keep my code as small as possible.
I tried following but it is not working
#RepositoryRestResource(collectionResourceRel = "people" , path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
List<Person> findAllByLastName(#Param("name") String name);
default List<Person> findAll(){
Pageable pageable = null;
return (List<Person>) this.findAll(pageable);
};
}
I mean if I have whole MVC, I can do it but I like to keep my code to minimum.
Spring Data REST is itself a Spring MVC application and is designed in
such a way that it should integrate with your existing Spring MVC
applications with little effort. An existing (or future) layer of
services can run alongside Spring Data REST with only minor additional
work.
If you are using current version of spring boot, there is no need to mark your repository with #RepositoryRestResource; also spring will auto-configure Spring Data Rest once it found the spring-data-rest dependency in your path, bellow you will find steps with minimum config :
In pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
Define your Entity + Repository :
Order.java
#Entity(name = "SampleOrder")
#Data
public class Order {
#Id #GeneratedValue//
private Long id;
private String name;
}
OrderRepository.java
public interface OrderRepository extends CrudRepository<Order, Long> {
}
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Test your API :
curl http://localhost:8080
< HTTP/1.1 200 OK
< Content-Type: application/hal+json
{ "_links" : {
"orders" : {
"href" : "http://localhost:8080/orders"
}
}
}
As #abdelghani-roussi shows, you can use the CrudRepository instead of the PagingAndSortingRepository, e.g.:
public interface PersonRepository extends CrudRepository<Person, Long> {
List<Person> findAllByLastName(#Param("name") String name);
// don't need to define findAll(), it's defined by CrudRepository
}
and then the default findAll() method will return a List<Person> that isn't paged.
Note: as I mentioned in my comment, by including the dependency on spring-boot-starter-data-rest you are also pulling in the Web dependencies, so you can't avoid that.
I use guice, jetty, jersey+jackson in my stack to run a restful app. It works perfectly.
Then, I tried to add Jersey's Bean validation but I got no errors, no warnings... and no validation. I've read many articles, but non of these helped me out.
Here is my JerseyConfigModule:
public class JerseyConfigModule extends ServletModule {
#Override
protected void configureServlets() {
Map<String, String> initParams = new HashMap<String, String>();
initParams.put("com.sun.jersey.api.json.POJOMappingFeature", "true");
bind(GuiceContainer.class);
Set<Class<?>> classes=new ResourceConfig().property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE,true).getClasses();
for (Class<?> aClass : classes) {
bind(aClass);
}
serve("/rest/*").with(GuiceContainer.class, initParams);
}
}
My Jersey resource:
#Path("/user")
public class UserResource {
#POST
#Consumes(MediaType.APPLICATION_JSON)
public Response post(#Valid StoreUserDTO user){
}
}
In an another Guice module I bind this resource:
bind(UserResource.class);
The Bean used in parameter:
public class StoreUserDTO {
#NotNull
private String name;
#NotNull
private String email;
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
I use jersey-guice and glassfish's jersey-bean-validation (and I want this):
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-guice</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-bean-validation</artifactId>
<version>2.22.1</version>
</dependency>
What did I forget? Lots of examples I've found is not worked or wasn't for Jersey 2, just for 1.
Yes, I know Jersey supports the bean validation officially, but in the offical docs, I didn't find any info about how to integrate with Guice.
I have a data access layer made with Spring-Data. I'm now creating a web application on top of it. This one controller method should return a Spring-Data Page formatted as JSON.
Such a Page is a List with additional Paging info like total amount of records and so forth.
Is that possible and if yes how?
And directly related to that can I define the mapping of property names? Eg. meaning I would need define how the paging info properties are named in JSON (differently than in page). Is this possible and how?
There's support for a scenario like this upcoming in Spring HATEOAS and Spring Data Commons. Spring HATEOAS comes with a PageMetadata object that essentially contains the same data as a Page but in a less enforcing manner, so that it can be more easily marshaled and unmarshaled.
Another aspect of the reason we implement this in combination with Spring HATEOAS and Spring Data commons is that there's little value in simply marshaling the page, it's content and the metadata but also want to generate the links to maybe existing next or previous pages, so that the client doesn't have to construct URIs to traverse these pages itself.
An example
Assume a domain class Person:
class Person {
Long id;
String firstname, lastname;
}
as well as it's corresponding repository:
interface PersonRepository extends PagingAndSortingRepository<Person, Long> { }
You can now expose a Spring MVC controller as follows:
#Controller
class PersonController {
#Autowired PersonRepository repository;
#RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}
There's probably quite a bit to explain here. Let's take it step by step:
We have a Spring MVC controller getting the repository wired into it. This requires Spring Data being set up (either through #Enable(Jpa|Mongo|Neo4j|Gemfire)Repositories or the XML equivalents). The controller method is mapped to /persons, which means it will accept all GET requests to that method.
The core type returned from the method is a PagedResources - a type from Spring HATEOAS that represents some content enriched with Links plus a PageMetadata.
When the method is invoked, Spring MVC will have to create instances for Pageable and PagedResourcesAssembler. To get this working you need to enable the Spring Data web support either through the #EnableSpringDataWebSupport annotation about to be introduced in the upcoming milestone of Spring Data Commons or via standalone bean definitions (documented here).
The Pageable will be populated with information from the request. The default configuration will turn ?page=0&size=10 into a Pageable requesting the first page by a page size of 10.
The PageableResourcesAssembler allows you to easily turn a Page into a PagedResources instances. It will not only add the page metadata to the response but also add the appropriate links to the representation based on what page you access and how your Pageable resolution is configured.
A sample JavaConfig configuration to enable this for JPA would look like this:
#Configuration
#EnableWebMvc
#EnableSpringDataWebSupport
#EnableJpaRepositories
class ApplicationConfig {
// declare infrastructure components like EntityManagerFactory etc. here
}
A sample request and response
Assume we have 30 Persons in the database. You can now trigger a request GET http://localhost:8080/persons and you'll see something similar to this:
{ "links" : [
{ "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20 }
],
"content" : [
… // 20 Person instances rendered here
],
"pageMetadata" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
Note that the assembler produced the correct URI and also picks up the default configuration present to resolve the parameters into a Pageable for an upcoming request. This means, if you change that configuration, the links will automatically adhere to the change. By default the assembler points to the controller method it was invoked in but that can be customized by handing in a custom Link to be used as base to build the pagination links to overloads of the PagedResourcesAssembler.toResource(…) method.
Outlook
The PagedResourcesAssembler bits will be available in the upcoming milestone release of the Spring Data Babbage release train. It's already available in the current snapshots. You can see a working example of this in my Spring RESTBucks sample application. Simply clone it, run mvn jetty:run and curl http://localhost:8080/pages.
Oliver, your answer is great and I mark it as answer. Here just for completeness what I came up with for the mean time which might be useful for someone else.
I use JQuery Datatables as my grid/table widget. It sends very specific parameter to server and excepts a very specific response: see http://datatables.net/usage/server-side.
To achieve this is created a custom helper object reflecting what datatables expects. Note that getter and setter must be named like they are else the produced json is wrong (case sensitive property names and datatables uses this "pseudo Hungarian notation"...).
public class JQueryDatatablesPage<T> implements java.io.Serializable {
private final int iTotalRecords;
private final int iTotalDisplayRecords;
private final String sEcho;
private final List<T> aaData;
public JQueryDatatablesPage(final List<T> pageContent,
final int iTotalRecords,
final int iTotalDisplayRecords,
final String sEcho){
this.aaData = pageContent;
this.iTotalRecords = iTotalRecords;
this.iTotalDisplayRecords = iTotalDisplayRecords;
this.sEcho = sEcho;
}
public int getiTotalRecords(){
return this.iTotalRecords;
}
public int getiTotalDisplayRecords(){
return this.iTotalDisplayRecords;
}
public String getsEcho(){
return this.sEcho;
}
public List<T> getaaData(){
return this.aaData;
}
}
The second part is a method in the according controller:
#RequestMapping(value = "/search", method = RequestMethod.GET, produces = "application/json")
public #ResponseBody String search (
#RequestParam int iDisplayStart,
#RequestParam int iDisplayLength,
#RequestParam int sEcho, // for datatables draw count
#RequestParam String search) throws IOException {
int pageNumber = (iDisplayStart + 1) / iDisplayLength;
PageRequest pageable = new PageRequest(pageNumber, iDisplayLength);
Page<SimpleCompound> page = compoundService.myCustomSearchMethod(search, pageable);
int iTotalRecords = (int) (int) page.getTotalElements();
int iTotalDisplayRecords = page.getTotalPages() * iDisplayLength;
JQueryDatatablesPage<SimpleCompound> dtPage = new JQueryDatatablesPage<>(
page.getContent(), iTotalRecords, iTotalDisplayRecords,
Integer.toString(sEcho));
String result = toJson(dtPage);
return result;
}
private String toJson(JQueryDatatablesPage<?> dt) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate4Module());
return mapper.writeValueAsString(dt);
}
compoundService is backed by a Spring-Data repository. It manages transactions and method level security. toJSON() method uses Jackson 2.0 and you need to register the appropriate module to the mapper, in my case for hibernate 4.
In case you have bidirectional relationships, you need to annotate all your entity classes with
#JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="jsonId")
This enables Jackson 2.0 to serialize circular dependencies (was not possible in earlier version and requires that your entities are annotated).
You will need to add following dependencies:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate4</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.2.1</version>
<type>jar</type>
</dependency>
Using Spring Boot (and for Mongo DB) I was able to do the following with successful results:
#RestController
#RequestMapping("/product")
public class ProductController {
//...
#RequestMapping(value = "/all", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE })
HttpEntity<PagedResources<Product>> get(#PageableDefault Pageable p, PagedResourcesAssembler assembler) {
Page<Product> product = productRepository.findAll(p);
return new ResponseEntity<>(assembler.toResource(product), HttpStatus.OK);
}
}
and the model class is like this:
#Document(collection = "my_product")
#Data
#ToString(callSuper = true)
public class Product extends BaseProduct {
private String itemCode;
private String brand;
private String sku;
}