From 964f8dd5620178a7bfde4da8ec90d769e9b03bd1 Mon Sep 17 00:00:00 2001 From: frantuma Date: Thu, 22 Feb 2018 17:44:38 +0100 Subject: [PATCH] refs #2079 - JsonView support minor fixes and tests --- .../io/swagger/annotations/ApiOperation.java | 6 + .../io/swagger/converter/ModelConverters.java | 34 +- .../io/swagger/jackson/ModelResolver.java | 6 +- .../main/java/io/swagger/jaxrs/Reader.java | 3 + .../test/java/io/swagger/JsonViewTest.java | 243 +++-- .../functional/test/ApiListingResourceIT.java | 939 +++++++++--------- .../test/resources/CarResource.java | 135 +-- 7 files changed, 731 insertions(+), 635 deletions(-) diff --git a/modules/swagger-annotations/src/main/java/io/swagger/annotations/ApiOperation.java b/modules/swagger-annotations/src/main/java/io/swagger/annotations/ApiOperation.java index ea1e733c1c..a618ee9cfb 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/annotations/ApiOperation.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/annotations/ApiOperation.java @@ -180,4 +180,10 @@ */ Extension[] extensions() default @Extension(properties = @ExtensionProperty(name = "", value = "")); + + /** + * Ignores JsonView annotations while resolving operations and types. For backward compatibility + * + */ + boolean ignoreJsonView() default false; } diff --git a/modules/swagger-core/src/main/java/io/swagger/converter/ModelConverters.java b/modules/swagger-core/src/main/java/io/swagger/converter/ModelConverters.java index b6ea70f048..dc3395fad9 100644 --- a/modules/swagger-core/src/main/java/io/swagger/converter/ModelConverters.java +++ b/modules/swagger-core/src/main/java/io/swagger/converter/ModelConverters.java @@ -53,32 +53,18 @@ public void addClassToSkip(String cls) { this.skippedClasses.add(cls); } + public Property readAsProperty(Type type) { + return readAsProperty(type, null); + } + public Property readAsProperty(Type type, JsonView jsonView) { ModelConverterContextImpl context = new ModelConverterContextImpl(converters); context.setJsonView(jsonView); return context.resolveProperty(type, null); } - public Property readAsProperty(Type type) { - ModelConverterContextImpl context = new ModelConverterContextImpl( - converters); - return context.resolveProperty(type, null); - } - public Map read(Type type) { - Map modelMap = new HashMap(); - if (shouldProcess(type)) { - ModelConverterContextImpl context = new ModelConverterContextImpl( - converters); - Model resolve = context.resolve(type); - for (Entry entry : context.getDefinedModels() - .entrySet()) { - if (entry.getValue().equals(resolve)) { - modelMap.put(entry.getKey(), entry.getValue()); - } - } - } - return modelMap; + return read(type, null); } public Map read(Type type, JsonView jsonView) { @@ -99,15 +85,7 @@ public Map read(Type type, JsonView jsonView) { } public Map readAll(Type type) { - if (shouldProcess(type)) { - ModelConverterContextImpl context = new ModelConverterContextImpl( - converters); - - LOGGER.debug("ModelConverters readAll from " + type); - context.resolve(type); - return context.getDefinedModels(); - } - return new HashMap(); + return readAll(type, null); } public Map readAll(Type type, JsonView annotation) { diff --git a/modules/swagger-core/src/main/java/io/swagger/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/jackson/ModelResolver.java index 770c8c4d75..67b6b516aa 100644 --- a/modules/swagger-core/src/main/java/io/swagger/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/jackson/ModelResolver.java @@ -389,7 +389,7 @@ public Model resolve(JavaType type, ModelConverterContext context, Iterator 0) { String COMBINER = "-or-"; StringBuffer sb = new StringBuffer(); for (Class view : context.getJsonView().value()) { @@ -651,7 +651,7 @@ private String decorateModelName(ModelConverterContext context, String originalN return name; } - private boolean hidenByJsonView(Annotation[] annotations, + private boolean hiddenByJsonView(Annotation[] annotations, ModelConverterContext context) { JsonView jsonView = context.getJsonView(); if (jsonView == null) diff --git a/modules/swagger-jaxrs/src/main/java/io/swagger/jaxrs/Reader.java b/modules/swagger-jaxrs/src/main/java/io/swagger/jaxrs/Reader.java index 481db4e462..3d4cbd7db3 100644 --- a/modules/swagger-jaxrs/src/main/java/io/swagger/jaxrs/Reader.java +++ b/modules/swagger-jaxrs/src/main/java/io/swagger/jaxrs/Reader.java @@ -841,6 +841,9 @@ private Operation parseMethod(Class cls, Method method, AnnotatedMethod annot if (apiOperation.hidden()) { return null; } + if (apiOperation.ignoreJsonView()) { + jsonViewAnnotation = null; + } if (!apiOperation.nickname().isEmpty()) { operationId = apiOperation.nickname(); } diff --git a/modules/swagger-jaxrs/src/test/java/io/swagger/JsonViewTest.java b/modules/swagger-jaxrs/src/test/java/io/swagger/JsonViewTest.java index da2706b6b8..3de83f2a5f 100644 --- a/modules/swagger-jaxrs/src/test/java/io/swagger/JsonViewTest.java +++ b/modules/swagger-jaxrs/src/test/java/io/swagger/JsonViewTest.java @@ -9,139 +9,184 @@ import io.swagger.models.Operation; import io.swagger.models.Swagger; import io.swagger.util.Json; + import java.util.Arrays; import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Response; + import org.testng.Assert; import org.testng.annotations.Test; - public class JsonViewTest { - private static class View { + private static class View { - interface Summary { + interface Summary { - } + } - interface Detail extends Summary { + interface Detail extends Summary { - } + } - interface Sale { + interface Sale { + } } - } - private static class Car { + private static class Car { + + @JsonView(View.Summary.class) + @JsonProperty("manufacture") + private String made = "Honda"; + + @JsonView({View.Summary.class, View.Detail.class}) + private String model = "Accord Hybrid"; + + @JsonView({View.Detail.class}) + @JsonProperty + private List tires = Arrays.asList(new Tire()); - @JsonView(View.Summary.class) - @JsonProperty("manufacture") - private String made = "Honda"; + @JsonView({View.Sale.class}) + @JsonProperty + private int price = 40000; - @JsonView({View.Summary.class, View.Detail.class}) - private String model = "Accord Hybrid"; + // always in + private String color = "White"; - @JsonView({View.Detail.class}) - @JsonProperty - private List tires = Arrays.asList(new Tire()); + public String getColor() { + return color; + } + } + + private static class Tire { - @JsonView({View.Sale.class}) - @JsonProperty - private int price = 40000; + @JsonView(View.Summary.class) + @JsonProperty("brand") + private String made = "Michelin"; + + @JsonView(View.Detail.class) + @JsonProperty + private String condition = "new"; + } - // always in - private String color = "White"; + @Api + @Path("/") + private static class CarSummaryApi { - public String getColor() { - return color; + @GET + @Path("/cars/summary") + @JsonView({View.Summary.class}) + public List getSummaries() { + return Arrays.asList(new Car()); + } } - } - private static class Tire { + @Api + @Path("/") + private static class CarDetailApi { + + @GET + @Path("/cars/detail") + @JsonView({View.Detail.class}) + @ApiOperation(value = "Return car detail", response = Car.class, responseContainer = "List") + public Response getDetails() { + return Response.ok(Arrays.asList(new Car())).build(); + } + } - @JsonView(View.Summary.class) - @JsonProperty("brand") - private String made = "Michelin"; + @Api + @Path("/") + private static class CarSaleSummaryApi { - @JsonView(View.Detail.class) - @JsonProperty - private String condition = "new"; - } + @GET + @Path("/cars/sale") + @JsonView({View.Summary.class, View.Sale.class}) + public List getSaleSummaries() { + return Arrays.asList(new Car()); + } + } - @Api - @Path("/") - private static class CarSummaryApi { + @Api + @Path("/") + private static class CarApi { - @GET - @Path("/cars/summary") - @JsonView({View.Summary.class}) - public List getSummaries() { - return Arrays.asList(new Car()); + @GET + @Path("/cars") + public List getCars() { + return Arrays.asList(new Car()); + } } - } - - @Api - @Path("/") - private static class CarDetailApi { - - @GET - @Path("/cars/detail") - @JsonView({View.Detail.class}) - @ApiOperation(value = "Return car detail", response = Car.class, responseContainer = "List") - public Response getDetails() { - return Response.ok(Arrays.asList(new Car())).build(); + + @Api + @Path("/ignore") + private static class CarIgnoreApi { + + @GET + @Path("/cars") + @JsonView({View.Summary.class, View.Sale.class}) + @ApiOperation(value = "getCars", ignoreJsonView = true) + public List getCars() { + return Arrays.asList(new Car()); + } } - } - @Api - @Path("/") - private static class CarSaleSummaryApi { + @Test(description = "check awareness of JsonView") + public void shouldSerializeTypeParameter() throws JsonProcessingException { + Swagger swagger = getSwagger(CarSummaryApi.class); + String swaggerJson = Json.mapper().writeValueAsString(swagger); + Assert.assertTrue(swaggerJson.contains("manufacture")); + Assert.assertTrue(swaggerJson.contains("model")); + Assert.assertTrue(swaggerJson.contains("color")); + Assert.assertFalse(swaggerJson.contains("tires")); + Assert.assertFalse(swaggerJson.contains("made")); + Assert.assertFalse(swaggerJson.contains("condition")); + + swagger = getSwagger(CarDetailApi.class); + swaggerJson = Json.mapper().writeValueAsString(swagger); + Assert.assertTrue(swaggerJson.contains("manufacture")); + Assert.assertTrue(swaggerJson.contains("model")); + Assert.assertTrue(swaggerJson.contains("color")); + Assert.assertTrue(swaggerJson.contains("tires")); + Assert.assertTrue(swaggerJson.contains("brand")); + Assert.assertTrue(swaggerJson.contains("condition")); + Assert.assertTrue(swaggerJson.contains("Car_Detail")); + + swagger = getSwagger(CarSaleSummaryApi.class); + swaggerJson = Json.mapper().writeValueAsString(swagger); + Assert.assertTrue(swaggerJson.contains("manufacture")); + Assert.assertTrue(swaggerJson.contains("model")); + Assert.assertTrue(swaggerJson.contains("color")); + Assert.assertTrue(swaggerJson.contains("price")); + Assert.assertFalse(swaggerJson.contains("tires")); + Assert.assertFalse(swaggerJson.contains("made")); + Assert.assertFalse(swaggerJson.contains("condition")); + + swagger = getSwagger(CarApi.class); + swaggerJson = Json.mapper().writeValueAsString(swagger); + Assert.assertTrue(swaggerJson.contains("manufacture")); + Assert.assertTrue(swaggerJson.contains("model")); + Assert.assertTrue(swaggerJson.contains("color")); + Assert.assertTrue(swaggerJson.contains("price")); + Assert.assertTrue(swaggerJson.contains("tires")); + Assert.assertFalse(swaggerJson.contains("made")); + Assert.assertTrue(swaggerJson.contains("condition")); + + swagger = getSwagger(CarIgnoreApi.class); + swaggerJson = Json.mapper().writeValueAsString(swagger); + Assert.assertTrue(swaggerJson.contains("manufacture")); + Assert.assertTrue(swaggerJson.contains("model")); + Assert.assertTrue(swaggerJson.contains("color")); + Assert.assertTrue(swaggerJson.contains("price")); + Assert.assertTrue(swaggerJson.contains("tires")); + Assert.assertFalse(swaggerJson.contains("made")); + Assert.assertTrue(swaggerJson.contains("condition")); + } - @GET - @Path("/cars/sale") - @JsonView({View.Summary.class, View.Sale.class}) - public List getSaleSummaries() { - return Arrays.asList(new Car()); + private Swagger getSwagger(Class cls) { + return new Reader(new Swagger()).read(cls); } - } - - @Test(description = "check awareness of JsonView") - public void shouldSerializeTypeParameter() throws JsonProcessingException { - Swagger swagger = getSwagger(CarSummaryApi.class); - String swaggerJson = Json.mapper().writeValueAsString(swagger); - Assert.assertTrue(swaggerJson.contains("manufacture")); - Assert.assertTrue(swaggerJson.contains("model")); - Assert.assertTrue(swaggerJson.contains("color")); - Assert.assertFalse(swaggerJson.contains("tires")); - Assert.assertFalse(swaggerJson.contains("made")); - Assert.assertFalse(swaggerJson.contains("condition")); - - swagger = getSwagger(CarDetailApi.class); - swaggerJson = Json.mapper().writeValueAsString(swagger); - Assert.assertTrue(swaggerJson.contains("manufacture")); - Assert.assertTrue(swaggerJson.contains("model")); - Assert.assertTrue(swaggerJson.contains("color")); - Assert.assertTrue(swaggerJson.contains("tires")); - Assert.assertTrue(swaggerJson.contains("brand")); - Assert.assertTrue(swaggerJson.contains("condition")); - Assert.assertTrue(swaggerJson.contains("Car_Detail")); - - swagger = getSwagger(CarSaleSummaryApi.class); - swaggerJson = Json.mapper().writeValueAsString(swagger); - Assert.assertTrue(swaggerJson.contains("manufacture")); - Assert.assertTrue(swaggerJson.contains("model")); - Assert.assertTrue(swaggerJson.contains("color")); - Assert.assertTrue(swaggerJson.contains("price")); - Assert.assertFalse(swaggerJson.contains("tires")); - Assert.assertFalse(swaggerJson.contains("made")); - Assert.assertFalse(swaggerJson.contains("condition")); - } - - private Swagger getSwagger(Class cls) { - return new Reader(new Swagger()).read(cls); - } } \ No newline at end of file diff --git a/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/ApiListingResourceIT.java b/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/ApiListingResourceIT.java index a207f26e3a..2550a06128 100644 --- a/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/ApiListingResourceIT.java +++ b/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/ApiListingResourceIT.java @@ -7,472 +7,519 @@ import com.jayway.restassured.http.ContentType; import io.swagger.util.Json; import io.swagger.util.Yaml; + import java.io.IOException; + import org.testng.SkipException; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** - * Created by rbolles on 2/16/16.

An functional integration test that runs during maven's - * integration-test phase, uses RestAssured to define REST API tests, and Jetty's Maven plugin to - * serve a simple sample app just prior to the integration-test phase starting. + * Created by rbolles on 2/16/16. + *

+ * An functional integration test that runs during maven's integration-test phase, + * uses RestAssured to define REST API tests, and Jetty's Maven plugin to serve a simple + * sample app just prior to the integration-test phase starting. */ public class ApiListingResourceIT { - private static final String EXPECTED_JSON = "{\n" - + " \"definitions\" : {\n" - + " \"Car\" : {\n" - + " \"properties\" : {\n" - + " \"color\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"manufacture\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"model\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"price\" : {\n" - + " \"format\" : \"int32\",\n" - + " \"type\" : \"integer\"\n" - + " },\n" - + " \"tires\" : {\n" - + " \"items\" : {\n" - + " \"$ref\" : \"#/definitions/Tire\"\n" - + " },\n" - + " \"type\" : \"array\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Car_Detail\" : {\n" - + " \"properties\" : {\n" - + " \"color\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"manufacture\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"model\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"tires\" : {\n" - + " \"items\" : {\n" - + " \"$ref\" : \"#/definitions/Tire_Detail\"\n" - + " },\n" - + " \"type\" : \"array\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Car_Summary\" : {\n" - + " \"properties\" : {\n" - + " \"color\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"manufacture\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"model\" : {\n" - + " \"type\" : \"string\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Car_Summary-or-Sale\" : {\n" - + " \"properties\" : {\n" - + " \"color\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"manufacture\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"model\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"price\" : {\n" - + " \"format\" : \"int32\",\n" - + " \"type\" : \"integer\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Tire\" : {\n" - + " \"properties\" : {\n" - + " \"brand\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"condition\" : {\n" - + " \"type\" : \"string\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Tire_Detail\" : {\n" - + " \"properties\" : {\n" - + " \"brand\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"condition\" : {\n" - + " \"type\" : \"string\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " },\n" - + " \"Widget\" : {\n" - + " \"properties\" : {\n" - + " \"a\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"b\" : {\n" - + " \"type\" : \"string\"\n" - + " },\n" - + " \"id\" : {\n" - + " \"type\" : \"string\"\n" - + " }\n" - + " },\n" - + " \"type\" : \"object\"\n" - + " }\n" - + " },\n" - + " \"paths\" : {\n" - + " \"/cars/detail\" : {\n" - + " \"get\" : {\n" - + " \"consumes\" : [ \"application/json\" ],\n" - + " \"description\" : \"\",\n" - + " \"operationId\" : \"getDetails\",\n" - + " \"parameters\" : [ ],\n" - + " \"produces\" : [ \"application/json\" ],\n" - + " \"responses\" : {\n" - + " \"200\" : {\n" - + " \"description\" : \"Return car detail\",\n" - + " \"schema\" : {\n" - + " \"items\" : {\n" - + " \"$ref\" : \"#/definitions/Car_Detail\"\n" - + " },\n" - + " \"type\" : \"array\"\n" - + " }\n" - + " }\n" - + " },\n" - + " \"summary\" : \"Return car detail\",\n" - + " \"tags\" : [ \"cars\" ]\n" - + " }\n" - + " },\n" - + " \"/cars/sale\" : {\n" - + " \"get\" : {\n" - + " \"consumes\" : [ \"application/json\" ],\n" - + " \"description\" : \"\",\n" - + " \"operationId\" : \"getSaleSummaries\",\n" - + " \"parameters\" : [ ],\n" - + " \"produces\" : [ \"application/json\" ],\n" - + " \"responses\" : {\n" - + " \"200\" : {\n" - + " \"description\" : \"successful operation\",\n" - + " \"schema\" : {\n" - + " \"items\" : {\n" - + " \"$ref\" : \"#/definitions/Car_Summary-or-Sale\"\n" - + " },\n" - + " \"type\" : \"array\"\n" - + " }\n" - + " }\n" - + " },\n" - + " \"summary\" : \"Return car sale summary\",\n" - + " \"tags\" : [ \"cars\" ]\n" - + " }\n" - + " },\n" - + " \"/cars/summary\" : {\n" - + " \"get\" : {\n" - + " \"consumes\" : [ \"List\" ],\n" - + " \"description\" : \"\",\n" - + " \"operationId\" : \"getSummaries\",\n" - + " \"parameters\" : [ ],\n" - + " \"produces\" : [ \"application/json\" ],\n" - + " \"responses\" : {\n" - + " \"200\" : {\n" - + " \"description\" : \"successful operation\",\n" - + " \"schema\" : {\n" - + " \"$ref\" : \"#/definitions/Car_Summary\"\n" - + " }\n" - + " }\n" - + " },\n" - + " \"summary\" : \"Return car summaries\",\n" - + " \"tags\" : [ \"cars\" ]\n" - + " }\n" - + " },\n" - + " \"/widgets/{widgetId}\" : {\n" - + " \"get\" : {\n" - + " \"consumes\" : [ \"application/json\" ],\n" - + " \"description\" : \"Returns a pet when ID <= 10. ID > 10 or nonintegers will simulate API error conditions\",\n" - + " \"operationId\" : \"getWidget\",\n" - + " \"parameters\" : [ {\n" - + " \"in\" : \"path\",\n" - + " \"name\" : \"widgetId\",\n" - + " \"required\" : true,\n" - + " \"type\" : \"string\"\n" - + " } ],\n" - + " \"produces\" : [ \"application/json\" ],\n" - + " \"responses\" : {\n" - + " \"200\" : {\n" - + " \"description\" : \"Returns widget with matching id\"\n" - + " }\n" - + " },\n" - + " \"summary\" : \"Find pet by ID\",\n" - + " \"tags\" : [ \"widgets\" ]\n" - + " }\n" - + " }\n" - + " },\n" - + " \"swagger\" : \"2.0\",\n" - + " \"tags\" : [ {\n" - + " \"name\" : \"cars\"\n" - + " }, {\n" - + " \"name\" : \"widgets\"\n" - + " } ]\n" - + "}"; - private static final String EXPECTED_YAML = "---\n" - + "definitions:\n" - + " Car:\n" - + " properties:\n" - + " color:\n" - + " type: \"string\"\n" - + " manufacture:\n" - + " type: \"string\"\n" - + " model:\n" - + " type: \"string\"\n" - + " price:\n" - + " format: \"int32\"\n" - + " type: \"integer\"\n" - + " tires:\n" - + " items:\n" - + " $ref: \"#/definitions/Tire\"\n" - + " type: \"array\"\n" - + " type: \"object\"\n" - + " Car_Detail:\n" - + " properties:\n" - + " color:\n" - + " type: \"string\"\n" - + " manufacture:\n" - + " type: \"string\"\n" - + " model:\n" - + " type: \"string\"\n" - + " tires:\n" - + " items:\n" - + " $ref: \"#/definitions/Tire_Detail\"\n" - + " type: \"array\"\n" - + " type: \"object\"\n" - + " Car_Summary:\n" - + " properties:\n" - + " color:\n" - + " type: \"string\"\n" - + " manufacture:\n" - + " type: \"string\"\n" - + " model:\n" - + " type: \"string\"\n" - + " type: \"object\"\n" - + " Car_Summary-or-Sale:\n" - + " properties:\n" - + " color:\n" - + " type: \"string\"\n" - + " manufacture:\n" - + " type: \"string\"\n" - + " model:\n" - + " type: \"string\"\n" - + " price:\n" - + " format: \"int32\"\n" - + " type: \"integer\"\n" - + " type: \"object\"\n" - + " Tire:\n" - + " properties:\n" - + " brand:\n" - + " type: \"string\"\n" - + " condition:\n" - + " type: \"string\"\n" - + " type: \"object\"\n" - + " Tire_Detail:\n" - + " properties:\n" - + " brand:\n" - + " type: \"string\"\n" - + " condition:\n" - + " type: \"string\"\n" - + " type: \"object\"\n" - + " Widget:\n" - + " properties:\n" - + " a:\n" - + " type: \"string\"\n" - + " b:\n" - + " type: \"string\"\n" - + " id:\n" - + " type: \"string\"\n" - + " type: \"object\"\n" - + "paths:\n" - + " /cars/detail:\n" - + " get:\n" - + " consumes:\n" - + " - \"application/json\"\n" - + " description: \"\"\n" - + " operationId: \"getDetails\"\n" - + " parameters: []\n" - + " produces:\n" - + " - \"application/json\"\n" - + " responses:\n" - + " 200:\n" - + " description: \"Return car detail\"\n" - + " schema:\n" - + " items:\n" - + " $ref: \"#/definitions/Car_Detail\"\n" - + " type: \"array\"\n" - + " summary: \"Return car detail\"\n" - + " tags:\n" - + " - \"cars\"\n" - + " /cars/sale:\n" - + " get:\n" - + " consumes:\n" - + " - \"application/json\"\n" - + " description: \"\"\n" - + " operationId: \"getSaleSummaries\"\n" - + " parameters: []\n" - + " produces:\n" - + " - \"application/json\"\n" - + " responses:\n" - + " 200:\n" - + " description: \"successful operation\"\n" - + " schema:\n" - + " items:\n" - + " $ref: \"#/definitions/Car_Summary-or-Sale\"\n" - + " type: \"array\"\n" - + " summary: \"Return car sale summary\"\n" - + " tags:\n" - + " - \"cars\"\n" - + " /cars/summary:\n" - + " get:\n" - + " consumes:\n" - + " - \"List\"\n" - + " description: \"\"\n" - + " operationId: \"getSummaries\"\n" - + " parameters: []\n" - + " produces:\n" - + " - \"application/json\"\n" - + " responses:\n" - + " 200:\n" - + " description: \"successful operation\"\n" - + " schema:\n" - + " $ref: \"#/definitions/Car_Summary\"\n" - + " summary: \"Return car summaries\"\n" - + " tags:\n" - + " - \"cars\"\n" - + " /widgets/{widgetId}:\n" - + " get:\n" - + " consumes:\n" - + " - \"application/json\"\n" - + " description: \"Returns a pet when ID <= 10. ID > 10 or nonintegers will simulate\\\n" - + " \\ API error conditions\"\n" - + " operationId: \"getWidget\"\n" - + " parameters:\n" - + " - in: \"path\"\n" - + " name: \"widgetId\"\n" - + " required: true\n" - + " type: \"string\"\n" - + " produces:\n" - + " - \"application/json\"\n" - + " responses:\n" - + " 200:\n" - + " description: \"Returns widget with matching id\"\n" - + " summary: \"Find pet by ID\"\n" - + " tags:\n" - + " - \"widgets\"\n" - + "swagger: \"2.0\"\n" - + "tags:\n" - + "- name: \"cars\"\n" - + "- name: \"widgets\"\n"; - private static final int jettyPort = System.getProperties().containsKey("jetty.port") ? Integer - .parseInt(System.getProperty("jetty.port")) : -1; + private static final String EXPECTED_JSON = "{\n" + + " \"definitions\" : {\n" + + " \"Car\" : {\n" + + " \"properties\" : {\n" + + " \"color\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"manufacture\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"model\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"price\" : {\n" + + " \"format\" : \"int32\",\n" + + " \"type\" : \"integer\"\n" + + " },\n" + + " \"tires\" : {\n" + + " \"items\" : {\n" + + " \"$ref\" : \"#/definitions/Tire\"\n" + + " },\n" + + " \"type\" : \"array\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Car_Detail\" : {\n" + + " \"properties\" : {\n" + + " \"color\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"manufacture\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"model\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"tires\" : {\n" + + " \"items\" : {\n" + + " \"$ref\" : \"#/definitions/Tire_Detail\"\n" + + " },\n" + + " \"type\" : \"array\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Car_Summary\" : {\n" + + " \"properties\" : {\n" + + " \"color\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"manufacture\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"model\" : {\n" + + " \"type\" : \"string\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Car_Summary-or-Sale\" : {\n" + + " \"properties\" : {\n" + + " \"color\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"manufacture\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"model\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"price\" : {\n" + + " \"format\" : \"int32\",\n" + + " \"type\" : \"integer\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Tire\" : {\n" + + " \"properties\" : {\n" + + " \"brand\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"condition\" : {\n" + + " \"type\" : \"string\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Tire_Detail\" : {\n" + + " \"properties\" : {\n" + + " \"brand\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"condition\" : {\n" + + " \"type\" : \"string\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " },\n" + + " \"Widget\" : {\n" + + " \"properties\" : {\n" + + " \"a\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"b\" : {\n" + + " \"type\" : \"string\"\n" + + " },\n" + + " \"id\" : {\n" + + " \"type\" : \"string\"\n" + + " }\n" + + " },\n" + + " \"type\" : \"object\"\n" + + " }\n" + + " },\n" + + " \"paths\" : {\n" + + " \"/cars/all\" : {\n" + + " \"get\" : {\n" + + " \"consumes\" : [ \"application/json\" ],\n" + + " \"description\" : \"\",\n" + + " \"operationId\" : \"getAll\",\n" + + " \"parameters\" : [ ],\n" + + " \"produces\" : [ \"application/json\" ],\n" + + " \"responses\" : {\n" + + " \"200\" : {\n" + + " \"description\" : \"Return whole car\",\n" + + " \"schema\" : {\n" + + " \"items\" : {\n" + + " \"$ref\" : \"#/definitions/Car\"\n" + + " },\n" + + " \"type\" : \"array\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"summary\" : \"Return whole car\",\n" + + " \"tags\" : [ \"cars\" ]\n" + + " }\n" + + " },\n" + + " \"/cars/detail\" : {\n" + + " \"get\" : {\n" + + " \"consumes\" : [ \"application/json\" ],\n" + + " \"description\" : \"\",\n" + + " \"operationId\" : \"getDetails\",\n" + + " \"parameters\" : [ ],\n" + + " \"produces\" : [ \"application/json\" ],\n" + + " \"responses\" : {\n" + + " \"200\" : {\n" + + " \"description\" : \"Return car detail\",\n" + + " \"schema\" : {\n" + + " \"items\" : {\n" + + " \"$ref\" : \"#/definitions/Car_Detail\"\n" + + " },\n" + + " \"type\" : \"array\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"summary\" : \"Return car detail\",\n" + + " \"tags\" : [ \"cars\" ]\n" + + " }\n" + + " },\n" + + " \"/cars/sale\" : {\n" + + " \"get\" : {\n" + + " \"consumes\" : [ \"application/json\" ],\n" + + " \"description\" : \"\",\n" + + " \"operationId\" : \"getSaleSummaries\",\n" + + " \"parameters\" : [ ],\n" + + " \"produces\" : [ \"application/json\" ],\n" + + " \"responses\" : {\n" + + " \"200\" : {\n" + + " \"description\" : \"successful operation\",\n" + + " \"schema\" : {\n" + + " \"items\" : {\n" + + " \"$ref\" : \"#/definitions/Car_Summary-or-Sale\"\n" + + " },\n" + + " \"type\" : \"array\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"summary\" : \"Return car sale summary\",\n" + + " \"tags\" : [ \"cars\" ]\n" + + " }\n" + + " },\n" + + " \"/cars/summary\" : {\n" + + " \"get\" : {\n" + + " \"consumes\" : [ \"List\" ],\n" + + " \"description\" : \"\",\n" + + " \"operationId\" : \"getSummaries\",\n" + + " \"parameters\" : [ ],\n" + + " \"produces\" : [ \"application/json\" ],\n" + + " \"responses\" : {\n" + + " \"200\" : {\n" + + " \"description\" : \"successful operation\",\n" + + " \"schema\" : {\n" + + " \"$ref\" : \"#/definitions/Car_Summary\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"summary\" : \"Return car summaries\",\n" + + " \"tags\" : [ \"cars\" ]\n" + + " }\n" + + " },\n" + + " \"/widgets/{widgetId}\" : {\n" + + " \"get\" : {\n" + + " \"consumes\" : [ \"application/json\" ],\n" + + " \"description\" : \"Returns a pet when ID <= 10. ID > 10 or nonintegers will simulate API error conditions\",\n" + + " \"operationId\" : \"getWidget\",\n" + + " \"parameters\" : [ {\n" + + " \"in\" : \"path\",\n" + + " \"name\" : \"widgetId\",\n" + + " \"required\" : true,\n" + + " \"type\" : \"string\"\n" + + " } ],\n" + + " \"produces\" : [ \"application/json\" ],\n" + + " \"responses\" : {\n" + + " \"200\" : {\n" + + " \"description\" : \"Returns widget with matching id\"\n" + + " }\n" + + " },\n" + + " \"summary\" : \"Find pet by ID\",\n" + + " \"tags\" : [ \"widgets\" ]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"swagger\" : \"2.0\",\n" + + " \"tags\" : [ {\n" + + " \"name\" : \"cars\"\n" + + " }, {\n" + + " \"name\" : \"widgets\"\n" + + " } ]\n" + + "}"; + private static final String EXPECTED_YAML = "---\n" + + "definitions:\n" + + " Car:\n" + + " properties:\n" + + " color:\n" + + " type: \"string\"\n" + + " manufacture:\n" + + " type: \"string\"\n" + + " model:\n" + + " type: \"string\"\n" + + " price:\n" + + " format: \"int32\"\n" + + " type: \"integer\"\n" + + " tires:\n" + + " items:\n" + + " $ref: \"#/definitions/Tire\"\n" + + " type: \"array\"\n" + + " type: \"object\"\n" + + " Car_Detail:\n" + + " properties:\n" + + " color:\n" + + " type: \"string\"\n" + + " manufacture:\n" + + " type: \"string\"\n" + + " model:\n" + + " type: \"string\"\n" + + " tires:\n" + + " items:\n" + + " $ref: \"#/definitions/Tire_Detail\"\n" + + " type: \"array\"\n" + + " type: \"object\"\n" + + " Car_Summary:\n" + + " properties:\n" + + " color:\n" + + " type: \"string\"\n" + + " manufacture:\n" + + " type: \"string\"\n" + + " model:\n" + + " type: \"string\"\n" + + " type: \"object\"\n" + + " Car_Summary-or-Sale:\n" + + " properties:\n" + + " color:\n" + + " type: \"string\"\n" + + " manufacture:\n" + + " type: \"string\"\n" + + " model:\n" + + " type: \"string\"\n" + + " price:\n" + + " format: \"int32\"\n" + + " type: \"integer\"\n" + + " type: \"object\"\n" + + " Tire:\n" + + " properties:\n" + + " brand:\n" + + " type: \"string\"\n" + + " condition:\n" + + " type: \"string\"\n" + + " type: \"object\"\n" + + " Tire_Detail:\n" + + " properties:\n" + + " brand:\n" + + " type: \"string\"\n" + + " condition:\n" + + " type: \"string\"\n" + + " type: \"object\"\n" + + " Widget:\n" + + " properties:\n" + + " a:\n" + + " type: \"string\"\n" + + " b:\n" + + " type: \"string\"\n" + + " id:\n" + + " type: \"string\"\n" + + " type: \"object\"\n" + + "paths:\n" + + " /cars/all:\n" + + " get:\n" + + " consumes:\n" + + " - \"application/json\"\n" + + " description: \"\"\n" + + " operationId: \"getAll\"\n" + + " parameters: []\n" + + " produces:\n" + + " - \"application/json\"\n" + + " responses:\n" + + " 200:\n" + + " description: \"Return whole car\"\n" + + " schema:\n" + + " items:\n" + + " $ref: \"#/definitions/Car\"\n" + + " type: \"array\"\n" + + " summary: \"Return whole car\"\n" + + " tags:\n" + + " - \"cars\"\n" + + " /cars/detail:\n" + + " get:\n" + + " consumes:\n" + + " - \"application/json\"\n" + + " description: \"\"\n" + + " operationId: \"getDetails\"\n" + + " parameters: []\n" + + " produces:\n" + + " - \"application/json\"\n" + + " responses:\n" + + " 200:\n" + + " description: \"Return car detail\"\n" + + " schema:\n" + + " items:\n" + + " $ref: \"#/definitions/Car_Detail\"\n" + + " type: \"array\"\n" + + " summary: \"Return car detail\"\n" + + " tags:\n" + + " - \"cars\"\n" + + " /cars/sale:\n" + + " get:\n" + + " consumes:\n" + + " - \"application/json\"\n" + + " description: \"\"\n" + + " operationId: \"getSaleSummaries\"\n" + + " parameters: []\n" + + " produces:\n" + + " - \"application/json\"\n" + + " responses:\n" + + " 200:\n" + + " description: \"successful operation\"\n" + + " schema:\n" + + " items:\n" + + " $ref: \"#/definitions/Car_Summary-or-Sale\"\n" + + " type: \"array\"\n" + + " summary: \"Return car sale summary\"\n" + + " tags:\n" + + " - \"cars\"\n" + + " /cars/summary:\n" + + " get:\n" + + " consumes:\n" + + " - \"List\"\n" + + " description: \"\"\n" + + " operationId: \"getSummaries\"\n" + + " parameters: []\n" + + " produces:\n" + + " - \"application/json\"\n" + + " responses:\n" + + " 200:\n" + + " description: \"successful operation\"\n" + + " schema:\n" + + " $ref: \"#/definitions/Car_Summary\"\n" + + " summary: \"Return car summaries\"\n" + + " tags:\n" + + " - \"cars\"\n" + + " /widgets/{widgetId}:\n" + + " get:\n" + + " consumes:\n" + + " - \"application/json\"\n" + + " description: \"Returns a pet when ID <= 10. ID > 10 or nonintegers will simulate\\\n" + + " \\ API error conditions\"\n" + + " operationId: \"getWidget\"\n" + + " parameters:\n" + + " - in: \"path\"\n" + + " name: \"widgetId\"\n" + + " required: true\n" + + " type: \"string\"\n" + + " produces:\n" + + " - \"application/json\"\n" + + " responses:\n" + + " 200:\n" + + " description: \"Returns widget with matching id\"\n" + + " summary: \"Find pet by ID\"\n" + + " tags:\n" + + " - \"widgets\"\n" + + "swagger: \"2.0\"\n" + + "tags:\n" + + "- name: \"cars\"\n" + + "- name: \"widgets\"\n"; + private static final int jettyPort = System.getProperties().containsKey("jetty.port") ? Integer + .parseInt(System.getProperty("jetty.port")) : -1; - @BeforeMethod - public void checkJetty() { - if (jettyPort == -1) { - throw new SkipException("Jetty not configured"); + @BeforeMethod + public void checkJetty() { + if (jettyPort == -1) { + throw new SkipException("Jetty not configured"); + } } - } - @Test - public void testSwaggerJson() throws Exception { + @Test + public void testSwaggerJson() throws Exception { - String actualBody = given() - .port(jettyPort) - .log().all() - .when() - .get("/swagger.json") - .then() - .log().all() - .assertThat() - .statusCode(200) - .contentType(ContentType.JSON) - .extract() - .response().body().asString(); + String actualBody = given() + .port(jettyPort) + .log().all() + .when() + .get("/swagger.json") + .then() + .log().all() + .assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .response().body().asString(); - assertEquals(formatJson(actualBody), EXPECTED_JSON); - } + System.out.println(EXPECTED_JSON); + System.out.println(formatJson(actualBody)); + assertEquals(formatJson(actualBody), EXPECTED_JSON); + } - @Test - public void testSwaggerJsonUsingAcceptHeader() throws Exception { - String actualBody = given() - .port(jettyPort) - .log().all() - .accept(ContentType.JSON) - .when() - .get("/swagger") - .then() - .log().all() - .assertThat() - .statusCode(200) - .contentType(ContentType.JSON) - .extract().response().body().asString(); + @Test + public void testSwaggerJsonUsingAcceptHeader() throws Exception { + String actualBody = given() + .port(jettyPort) + .log().all() + .accept(ContentType.JSON) + .when() + .get("/swagger") + .then() + .log().all() + .assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .extract().response().body().asString(); - assertEquals(formatJson(actualBody), EXPECTED_JSON); - } + assertEquals(formatJson(actualBody), EXPECTED_JSON); + } - @Test - public void testSwaggerYaml() throws Exception { - String actualBody = given() - .port(jettyPort) - .log().all() - .when() - .get("/swagger.yaml") - .then() - .log().all() - .assertThat() - .statusCode(200) - .contentType("application/yaml") - .extract().response().body().asString(); + @Test + public void testSwaggerYaml() throws Exception { + String actualBody = given() + .port(jettyPort) + .log().all() + .when() + .get("/swagger.yaml") + .then() + .log().all() + .assertThat() + .statusCode(200) + .contentType("application/yaml") + .extract().response().body().asString(); - assertEquals(formatYaml(actualBody), EXPECTED_YAML); - } + assertEquals(formatYaml(actualBody), EXPECTED_YAML); + } - @Test - public void testSwaggerYamlUsingAcceptHeader() throws Exception { - String actualBody = given() - .port(jettyPort) - .log().all() - .accept("application/yaml") - .when() - .get("/swagger") - .then() - .log().all() - .assertThat() - .statusCode(200) - .contentType("application/yaml") - .extract().response().body().asString(); + @Test + public void testSwaggerYamlUsingAcceptHeader() throws Exception { + String actualBody = given() + .port(jettyPort) + .log().all() + .accept("application/yaml") + .when() + .get("/swagger") + .then() + .log().all() + .assertThat() + .statusCode(200) + .contentType("application/yaml") + .extract().response().body().asString(); - assertEquals(formatYaml(actualBody), EXPECTED_YAML); - } + assertEquals(formatYaml(actualBody), EXPECTED_YAML); + } - private String formatYaml(String source) throws IOException { - return Yaml.mapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) - .writerWithDefaultPrettyPrinter() - .writeValueAsString(Yaml.mapper().readValue(source, Object.class)); - } + private String formatYaml(String source) throws IOException { + return Yaml.mapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(Yaml.mapper().readValue(source, Object.class)); + } - private String formatJson(String source) throws IOException { - return Json.mapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) - .writerWithDefaultPrettyPrinter() - .writeValueAsString(Json.mapper().readValue(source, Object.class)); - } + private String formatJson(String source) throws IOException { + return Json.mapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(Json.mapper().readValue(source, Object.class)); + } } diff --git a/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/resources/CarResource.java b/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/resources/CarResource.java index c0403596e8..54580ec9e5 100644 --- a/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/resources/CarResource.java +++ b/modules/swagger-jaxrs/src/test/java/io/swagger/functional/test/resources/CarResource.java @@ -6,6 +6,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; + import java.util.Arrays; import java.util.List; import javax.ws.rs.Consumes; @@ -15,77 +16,93 @@ import javax.ws.rs.core.Response; @Path("/cars") -@Api(tags="cars", description = "Car operations") +@Api(tags = "cars", description = "Car operations") @Produces("application/json") @Consumes("application/json") public class CarResource { - private static class View { + private static class View { + + interface Summary { + } + + interface Detail extends View.Summary { + } + + interface Sale { + } + } + + private static class Car { + + @JsonView(View.Summary.class) + @JsonProperty("manufacture") + private String made = "Honda"; + + @JsonView({View.Summary.class, View.Detail.class}) + private String model = "Accord Hybrid"; - interface Summary {} - interface Detail extends View.Summary {} - interface Sale {} - } + @JsonView({View.Detail.class}) + @JsonProperty + private List tires = Arrays.asList(new Tire()); - private static class Car { + @JsonView({View.Sale.class}) + @JsonProperty + private int price = 40000; - @JsonView(View.Summary.class) - @JsonProperty("manufacture") - private String made = "Honda"; + // always in + private String color = "White"; - @JsonView({View.Summary.class, View.Detail.class}) - private String model = "Accord Hybrid"; + public String getColor() { + return color; + } + } + + private static class Tire { + @JsonView(View.Summary.class) + @JsonProperty("brand") + private String made = "Michelin"; + + @JsonView(View.Detail.class) + @JsonProperty + private String condition = "new"; + } + @GET + @Path("/summary") + @JsonView({View.Summary.class}) + @ApiOperation(value = "Return car summaries", response = Car.class, consumes = "List") + public Response getSummaries() { + return Response.ok(Arrays.asList(new Car())).build(); + } + + @GET + @Path("/detail") @JsonView({View.Detail.class}) - @JsonProperty - private List tires = Arrays.asList(new Tire()); + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Return car detail", response = Car.class, responseContainer = "List") + }) + @ApiOperation(value = "Return car detail") + public List getDetails() { + return Arrays.asList(new Car()); + } - @JsonView({View.Sale.class}) - @JsonProperty - private int price = 40000; + @GET + @Path("/sale") + @JsonView({View.Summary.class, View.Sale.class}) + @ApiOperation(value = "Return car sale summary") + public List getSaleSummaries() { + return Arrays.asList(new Car()); + } - // always in - private String color = "White"; - public String getColor() { - return color; + @GET + @Path("/all") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Return whole car", response = Car.class, responseContainer = "List") + }) + @ApiOperation(value = "Return whole car") + public List getAll() { + return Arrays.asList(new Car()); } - } - - private static class Tire { - @JsonView(View.Summary.class) - @JsonProperty("brand") - private String made = "Michelin"; - - @JsonView(View.Detail.class) - @JsonProperty - private String condition = "new"; - } - - @GET - @Path("/summary") - @JsonView({View.Summary.class}) - @ApiOperation(value = "Return car summaries", response = Car.class, consumes = "List") - public Response getSummaries() { - return Response.ok(Arrays.asList(new Car())).build(); - } - - @GET - @Path("/detail") - @JsonView({View.Detail.class}) - @ApiResponses(value = { - @ApiResponse(code=200, message="Return car detail", response = Car.class, responseContainer = "List") - }) - @ApiOperation(value = "Return car detail") - public List getDetails() { - return Arrays.asList(new Car()); - } - - @GET - @Path("/sale") - @JsonView({View.Summary.class, View.Sale.class}) - @ApiOperation(value = "Return car sale summary") - public List getSaleSummaries() { - return Arrays.asList(new Car()); - } }