I'm reading spring in action 5th and learning spring-cloud, hateoas and webflux.I tried to write a rest controller as following
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;
#RestController
#RequestMapping(path = "/")
public class ServiceController {
private IngredientServiceClient ingredientClient;
#Autowired
public ServiceController(IngredientServiceClient ingredientClient) {
this.ingredientClient = ingredientClient;
}
#GetMapping("/ingredients/{id}")
public Mono<EntityModel<Ingredient>> getIngredientById(#PathVariable("id") String id) {
return ingredientClient.getIngredientById(id)
.flatMap(ingredient -> {
EntityModel<Ingredient> model = EntityModel.of(ingredient);
Mono<Link> link = linkTo(methodOn(ServiceController.class).getIngredientById(id)).withSelfRel().toMono();
return link.map(lk -> model.add(lk));
});
}
}
IngredientServiceClient.getIngredientById
public Mono<Ingredient> getIngredientById(String id) {
return wcBuilder.build()
.get().uri("http://ingredient-api/ingredients/{id}", id)
.retrieve().bodyToMono(Ingredient.class);
}
When I access to localhost:8082/ingredients/FLTO a node of my webapp, it shows me only the relative path like this
{
"id": "FLTO",
"name": "Flour Tortilla",
"type": "WRAP",
"_links": {
"self": {
"href": "/ingredients/FLTO"
}
}
}
I've tried the WebMvcLinkBuilder but it still did not work correctly. I found some explanations about my problem. But I'm not sure whether the context/exchange was null (and why). Could you help me?
try to set spring.main.web-application-type=reactive
Related
I'm trying to get the first 5 articles from this API: https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=19acc3a371d145ecb37a093f9985ea21
My code works perfectly for now, but it parses all 10 articles of NewsAPI.
The code is:
public News parse() {
return restTemplate.getForObject
("https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=19acc3a371d145ecb37a093f9985ea21", News.class);
}
}
and the result is:
{
"totalResults": 10,
"articles": [
{
"source": {
"id": "bbc-news",
"name": "BBC News"
},
"author": "BBC News",
"title": "Measles returns to four European nations, WHO says",
"url": "http://www.bbc.co.uk/news/health-49507253"
},
etc......
Of course, i created the classes that describe Article, Source and News. News has a List of Article.
I just want to parse the first five articles and save them into a List. I know I have to use a For cycle, but how can i do that? I tried with this code:
public News parseFive() {
List<Article> articleList = null;
for(int i = 0; i<5; i++) {
articleList = Arrays.asList(
new Article(restTemplate.getForObject
("https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=19acc3a371d145ecb37a093f9985ea21", Article.class)));
}
News news = new News();
news.setArticles(articleList);
return news;
}
The News class is:
public class News {
private int totalResults;
private List<Article> articles;
public News() {
}
public int getTotalResults() {
return totalResults;
}
public void setTotalResults(int totalResults) {
this.totalResults = totalResults;
}
public List<Article> getArticles() {
return articles;
}
public void setArticles() {
this.articles = articles;
}
}
and the result is:
{
"totalResults": 0,
"articles": [
{
"source": null,
"author": null,
"title": null,
"url": null
}
]
}
Where is the problem? Maybe because the first class who finds is not Article but is News? Thanks everyone for the effort.
When you are using RestTemplate.getForObject you are technically parsing the whole response: Spring reads all the bytes and uses JSON parser (Jackson) to create an object. Your for loop, which is covered later, only filters out elements past 5th. If you really want to parse only first 5 articles, you should consider using Jackson Streaming API. It is quiet problematically to use with RestTemplate, read this answer for more info.
Now let's try to fix your parseFive.
First, create a class to capture whole response:
public class Response {
private String status;
private Integer totalResults;
private List<Artice> articles;
// Getters & Setters
}
Now, get first five articles:
public News parseFive() {
final Response response = restTemplate
.getForObject("https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=19acc3a371d145ecb37a093f9985ea21", Response.class);
final News news = new News();
news.setArticles(response.articles.stream().limit(5).collect(Collectors.toList());
return news;
}
You have not provided your News class, probably it is the same as response. Then, the code may look like:
public News parseFive() {
final News news = restTemplate
.getForObject("https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=19acc3a371d145ecb37a093f9985ea21", Response.class);
news.setArticles(news.articles.stream().limit(5).collect(Collectors.toList());
return news;
}
With the following setup, spring doesn't pick a transformer defined in JSON file which is under mappings directory, but it does when I declare it directly in test code.
#Configuration
public class WiremockConfiguration {
#Bean
WireMockConfigurationCustomizer optionsCustomizer() {
return new WireMockConfigurationCustomizer() {
#Override
public void customize(WireMockConfiguration options) {
options.extensions(BodyDefinitionTransformer.class);
}
};
}
}
{
"request": {
"method": "POST",
"urlPattern": "/some/thing"
},
"response": {
"status": 200,
"bodyFileName": "my_payload.json",
"transformers": [
"body-transformer"
],
"headers": {
"Content-Type": "application/json"
}
}
}
public class BodyDefinitionTransformer extends ResponseDefinitionTransformer {
#Override
public ResponseDefinition transform(Request request, ResponseDefinition responseDefinition, FileSource files,
Parameters parameters) {
return responseDefinition; //checking if this work by putting breakpoint here
}
#Override
public boolean applyGlobally() {
return false;
}
#Override
public String getName() {
return "body-transformer";
}
}
#ContextConfiguration
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#TestPropertySource
#AutoConfigureWireMock(port = 9632)
class DummyTestClass extends Specification {
def "some dummy test" () {
when:
stubFor(post("/some/thing").willReturn(aResponse()
//.withTransformers("body-transformer") transformer work if I declare it in this way
.withTransformerParameter("test", "test"
)))
// rest of my test where execute above request
}
}
The code works perfectly when I declare it using .withTransformers("body-transformer") but when I put transformer name into transformers array in JSON file, it doesn't work. Do you have any ideas why?
I want to generate a swagger from a JAX-RS endpoint with an external enumeration definition however the generated swagger directly includes the enumeration into the definition of the model. It implies that the enumeration documentation is not generated but also that the same enumeration is duplicated on the client side.
I use the swagger-jaxrs dependency to scan my endpoint and generate the swagger json file. This GitHub repository can be used to reproduce the problem. I also have created a GitHub issue on the swagger-core repository.
The JAX-RS endpoint
#Api("hello")
#Path("/helloSwagger")
public class HelloSwagger {
#ApiOperation(value = "Get all unique customers", notes = "Get all customers matching the given search string.", responseContainer = "Set", response = User.class)
#GET
#Path("/getUniqueUsers")
#Produces(MediaType.APPLICATION_JSON)
public Set<User> getUniqueUsers(
#ApiParam(value = "The search string is used to find customer by their name. Not case sensitive.") #QueryParam("search") String searchString,
#ApiParam(value = "Limits the size of the result set", defaultValue = "50") #QueryParam("limit") int limit
) {
return new HashSet<>(Arrays.asList(new User(), new User()));
}
}
The model with the enumeration
public class User {
private String name = "unknown";
private SynchronizationStatus ldap1 = SynchronizationStatus.UNKNOWN;
private SynchronizationStatus ldap2 = SynchronizationStatus.OFFLINE;
#ApiModelProperty(value = "The user name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#ApiModelProperty(value = "The synchronization status with the LDAP1")
public SynchronizationStatus getLdap1() {
return ldap1;
}
public void setLdap1(SynchronizationStatus ldap1) {
this.ldap1 = ldap1;
}
public SynchronizationStatus getLdap2() {
return ldap2;
}
public void setLdap2(SynchronizationStatus ldap2) {
this.ldap2 = ldap2;
}
}
#ApiModel("The synchronization status with LDAP instance.")
public enum SynchronizationStatus {
UNKNOWN,
SYNC,
OFFLINE,
CONFLICT
}
An extract of the swagger generated
{
(...)
},
"definitions" : {
"User" : {
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "The user name"
},
"ldap1" : {
"type" : "string",
"description" : "The synchronization status with the LDAP1",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ]
},
"ldap2" : {
"type" : "string",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ]
}
}
}
}
}
Expected result
{
(...)
"definitions" : {
"SynchronizationStatus" : {
"description" : "The synchronization status with LDAP instance.",
"enum" : [ "UNKNOWN", "SYNC", "OFFLINE", "CONFLICT" ],
"type" : "string"
},
"User" : {
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "The user name"
},
"ldap1" : {
"$ref" : "#/definitions/SynchronizationStatus"
},
"ldap2" : {
"$ref" : "#/definitions/SynchronizationStatus"
}
}
}
}
}
Am I doing something wrong or is it a 'feature' of the swagger-jaxrs library ?
Thanks for your help
Am I doing something wrong or is it a 'feature' of the swagger-jaxrs
library ?
Enum value are treat as primitive value type by swagger and swagger out-of-the-box does not generate model definition for enum type (see code line 209 under). So the is a feature and not related with swagger-jaxrs.
However, you can generate the swagger definition, as per your expectation, by providing the custom model converter(io.swagger.converter.ModelConverter).
But it seems to me a nice feature to be available in swagger out-of-the-box.
Following is a ruff implementation which can help you to generate the expected swagger definition.
package nhenneaux.test.swagger.ext;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.databind.JavaType;
import io.swagger.annotations.ApiModel;
import io.swagger.converter.ModelConverter;
import io.swagger.converter.ModelConverterContext;
import io.swagger.jackson.ModelResolver;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.models.properties.StringProperty;
import io.swagger.util.Json;
public class EnumAsModelAwareResolver extends ModelResolver {
static final EnumAsModelAwareResolver INSTANCE = new EnumAsModelAwareResolver();
public EnumAsModelAwareResolver() {
super(Json.mapper());
}
#Override
public Property resolveProperty(Type type, ModelConverterContext context, Annotation[] annotations,
Iterator<ModelConverter> chain) {
if (isEnumAnApiModel(type)) {
String name = findName(type);
// ask context to resolver enum type (for adding model definition
// for enum under definitions section
context.resolve(type);
return new RefProperty(name);
}
return chain.next().resolveProperty(type, context, annotations, chain);
}
private String findName(Type type) {
JavaType javaType = _mapper.constructType(type);
Class<?> rawClass = javaType.getRawClass();
ApiModel annotation = rawClass.getAnnotation(ApiModel.class);
String name = annotation.value();
if (name == null || name.length() == 0) {
name = rawClass.getSimpleName();
}
return name;
}
private boolean isEnumAnApiModel(Type type) {
JavaType javaType = _mapper.constructType(type);
return javaType.isEnumType()
&& javaType.getRawClass().isAnnotationPresent(ApiModel.class);
}
#Override
public Model resolve(Type type, ModelConverterContext context, Iterator<ModelConverter> chain) {
JavaType javaType = Json.mapper().constructType(type);
if (javaType.isEnumType()) {
ModelImpl model = new ModelImpl();
Class<?> rawClass = javaType.getRawClass();
ApiModel annotation = rawClass.getAnnotation(ApiModel.class);
String name = annotation.value();
if (name == null || name.length() == 0) {
name = rawClass.getSimpleName();
}
model.setName(name);
model.setDescription(annotation.description());
model.setType(StringProperty.TYPE);
List<String> constants = findEnumConstants(rawClass);
model.setEnum(constants);
return model;
}
return chain.next().resolve(type, context, chain);
}
private List<String> findEnumConstants(Class<?> rawClass) {
StringProperty p = new StringProperty();
_addEnumProps(rawClass, p);
return p.getEnum();
}
}
package nhenneaux.test.swagger.ext;
import io.swagger.converter.ModelConverters;
import io.swagger.jaxrs.config.BeanConfig;
import nhenneaux.test.swagger.ext.EnumAsModelAwareResolver;
public class EnumModelAwareBeanConfig extends BeanConfig {
public EnumModelAwareBeanConfig() {
registerResolver();
}
private void registerResolver() {
ModelConverters modelConverters = ModelConverters.getInstance();
// remove and add; in case it is called multiple times.
// should find a better way to register this.
modelConverters.removeConverter(EnumAsModelAwareResolver.INSTANCE);
modelConverters.addConverter(EnumAsModelAwareResolver.INSTANCE);
}
}
In your test use:
final BeanConfig beanConfig = new nhenneaux.test.endpoint.model.EnumModelAwareBeanConfig();
Hops this helps.
You could try the reference attribute of the #ApiModelProperty annotation:
#ApiModelProperty(reference = "#/definitions/SynchronizationStatus")
public SynchronizationStatus getLdap1() {
return ldap1;
}
Based on this mailing list post from last year I believe it is not trivial and one may have to extend the appropriate Swagger resources. The only other option would be to manually reference the model as per Cássio Mazzochi Molin's answer (just be careful that renaming SynchronizationStatus doesn't break the API docs due to the forced use of a non-generated string)
I was able to achieve this with (using swagger-jaxrs-2.1.3)
System.setProperty(ModelResolver.SET_PROPERTY_OF_ENUMS_AS_REF, "true");
Reader reader = new Reader();
OpenAPI api = reader.read(...);
I'm creating a client to query our JSON Api and I'm trying to extend a resource with the content coming from another resource. I want to do this because the resources are part of the same entity and our users will get back the single entity without the need to query the two services separately.
I'll put here a base version of the resources, to better explain the problem:
/* Sample class */
public class Sample {
public String accession;
public String name;
//...
public Map<RelationType, List<Relation>> relations
// ... classic getters and setters
}
// Relation class
public class Relation {
public String id;
// getters and setters
}
Now the JSON Api is something similar to this:
Here the Sample
{
"_embedded":{
"samples":[
{
"accession":"SAME1500861",
"name":"1380435",
"_links":{
"self":{ "href":"https://www.ebi.ac.uk/biosamples/api/samples/SAME1500861"
},
"sample":{
"href":"https://www.ebi.ac.uk/biosamples/api/samples/SAME1500861"
},
"relations":{
"href":"https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861"
}
}
}
]
},
"_links":{ }
}
Here the relations:
{
"accession": "SAME1500861",
"_links": {
"self": {
"href": "https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861"
},
"derivedFrom": {
"href": "https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861/derivedFrom"
},
"derivedTo": {
"href": "https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861/derivedTo"
},
"externalLinks": {
"href": "https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861/externalLinks"
},
"recuratedTo": {
"href": "https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861/recuratedTo"
}
}
}
Any suggestion would be great.
Thanks
You just need to create a Java class structure that represents the data in your JSON. You can use any of online viewers or some chrome extension to see stricture...
I've just done small part of your relations JSON...
public class Relations {
public String accession;
public Links _links;
}
public class Links {
public Self self;
}
public class Self {
public String href;
}
Here at least 2 option in my eyes...
Retrive JSON as string and convert via Gson
Use spring RestTemplate
Basically I would recommend to use Spring RestTemplate
RestTemplate restTemplate = new RestTemplate();
Relations result = restTemplate.getForObject("https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861", Relations.class);
Or you can use any lib to get JSON response and convert via Gson
String relationsPart = "{\n" + " \"accession\": \"SAME1500861\",\n" + " \"_links\": {\n" + " \"self\": {\n" + " \"href\": \"https://www.ebi.ac.uk/biosamples/api/samplesrelations/SAME1500861\"\n" + " }\n" + " }\n" + "}";
Gson gson = new GsonBuilder().setPrettyPrinting().create();
Relations result = gson.fromJson(relationsPart, Relations.class);
I'm trying to build a HAL-compliant REST API with Spring HATEOAS.
After some fiddling I managed to get to work mostly like expected.
The (sample) output looks like this right now:
{
"_links": {
"self": {
"href": "http://localhost:8080/sybil/configuration/bricks"
}
},
"_embedded": {
"brickDomainList": [
{
"hostname": "localhost",
"port": 4223,
"_links": {
"self": {
"href": "http://localhost:8080/sybil/configuration/bricks/localhost"
}
}
},
{
"hostname": "synerforge001",
"port": 4223,
"_links": {
"self": {
"href": "http://localhost:8080/sybil/configuration/bricks/synerforge001"
}
}
}
]
}
}
I don't like the "brickDomainList" array's name. It should say "bricks", ideally. How can I change it?
Here's the controller that produces the output:
#RestController
#RequestMapping("/configuration/bricks")
public class ConfigurationBricksController {
private BrickRepository brickRepository;
private GraphDatabaseService graphDatabaseService;
#Autowired
public ConfigurationBricksController(BrickRepository brickRepository, GraphDatabaseService graphDatabaseService) {
this.brickRepository = brickRepository;
this.graphDatabaseService = graphDatabaseService;
}
#ResponseBody
#RequestMapping(method = RequestMethod.GET, produces = "application/hal+json")
public Resources<BrickResource> bricks() {
List<BrickDomain> bricks;
List<BrickResource> resources = new ArrayList<>();
List<Link> links = new ArrayList<>();
Link self = linkTo(ConfigurationBricksController.class).withSelfRel();
links.add(self);
try(Transaction tx = graphDatabaseService.beginTx()) { // begin transaction
// get all Bricks from database and cast them into a list so that they're actually fetched
bricks = new ArrayList<>(IteratorUtil.asCollection(brickRepository.findAll()));
// end transaction
tx.success();
}
for (BrickDomain brick : bricks) {
self = linkTo(methodOn(ConfigurationBricksController.class).brick(brick.getHostname())).withSelfRel();
BrickResource resource = new BrickResource(brick, self);
resources.add(resource);
}
return new Resources<>(resources, links);
}
}
Is there some Annotation or something I can add to change the array's name?
If you want/need to look at the BrickResource class or the Repositories or something look here: https://github.com/ttheuer/sybil/tree/mvctest/src/main/java/org/synyx/sybil
The BrickResource is in api/resources/, the repository is in database/, and the BrickDomain in domain/.
Thanks!
Just use Evo Inflector. If you have a Maven project then add the dependency
<dependency>
<groupId>org.atteo</groupId>
<artifactId>evo-inflector</artifactId>
<version>1.2</version>
</dependency>
Or you can add #Relation(collectionRelation = "bricks") to the BrickDomain class
#Relation(collectionRelation = "bricks")
public class BrickDomain { … }