validationErrors = exception.getConstraintViolations().stream().map(constraintViolation -> {
77 |
78 | Path.Node leafNode = getLeafNode(constraintViolation.getPropertyPath()).get();
79 |
80 | switch (leafNode.getKind()) {
81 |
82 | case PROPERTY:
83 | return handleInvalidProperty(constraintViolation);
84 |
85 | case PARAMETER:
86 | return handleInvalidParameter(constraintViolation);
87 |
88 | case BEAN:
89 | return handleInvalidBean(constraintViolation);
90 |
91 | default:
92 | return handleUnknownSource(constraintViolation);
93 | }
94 |
95 | }).collect(Collectors.toList());
96 | error.setValidationErrors(validationErrors);
97 |
98 | return error;
99 | }
100 |
101 | /**
102 | * Handle an invalid property. Can be:
103 | *
104 | * 1. Invalid request parameter (annotated bean param field or annotated resource class field)
105 | * 2. Invalid request entity property (annotated bean param field)
106 | *
107 | * @param constraintViolation
108 | */
109 | private ValidationError handleInvalidProperty(ConstraintViolation constraintViolation) {
110 |
111 | Path.Node leafNode = getLeafNode(constraintViolation.getPropertyPath()).get();
112 | Class> beanClass = constraintViolation.getLeafBean().getClass();
113 |
114 | // Can be an invalid request parameter (annotated bean param field or annotated resource class field)
115 | Optional optionalField = getField(leafNode.getName(), beanClass);
116 | if (optionalField.isPresent()) {
117 | Optional optionalParameterDetails = getParameterDetails(optionalField.get().getAnnotations());
118 | if (optionalParameterDetails.isPresent()) {
119 | return createErrorForParameter(optionalParameterDetails.get(), constraintViolation);
120 | }
121 | }
122 |
123 | // Get Jackson ObjectMapper
124 | ContextResolver resolver = providers.getContextResolver(ObjectMapper.class, MediaType.WILDCARD_TYPE);
125 | ObjectMapper mapper = resolver.getContext(ObjectMapper.class);
126 |
127 | // Can be an invalid request entity property (annotated bean param field)
128 | Optional optionalJsonProperty = getJsonPropertyName(mapper, beanClass, leafNode.getName());
129 | if (optionalJsonProperty.isPresent()) {
130 | ValidationError error = new ValidationError();
131 | error.setType(REQUEST_ENTITY_PROPERTY);
132 | error.setName(optionalJsonProperty.get());
133 | error.setMessage(constraintViolation.getMessage());
134 | return error;
135 | }
136 |
137 | return handleUnknownSource(constraintViolation);
138 | }
139 |
140 | /**
141 | * Handle an invalid parameter. Can be:
142 | *
143 | * 1. Invalid request parameter (annotated method parameter)
144 | * 2. Invalid request entity (annotated method parameter)
145 | *
146 | * @param constraintViolation
147 | */
148 | private ValidationError handleInvalidParameter(ConstraintViolation constraintViolation) {
149 |
150 | List nodes = new ArrayList<>();
151 | constraintViolation.getPropertyPath().iterator().forEachRemaining(nodes::add);
152 |
153 | Path.Node parent = nodes.get(nodes.size() - 2);
154 | Path.Node child = nodes.get(nodes.size() - 1);
155 |
156 | if (ElementKind.METHOD == parent.getKind()) {
157 |
158 | Path.MethodNode methodNode = parent.as(Path.MethodNode.class);
159 | Path.ParameterNode parameterNode = child.as(Path.ParameterNode.class);
160 |
161 | try {
162 |
163 | // Can be an invalid request parameter (annotated method parameter)
164 | Class> beanClass = constraintViolation.getLeafBean().getClass();
165 | Method method = beanClass.getMethod(methodNode.getName(), methodNode.getParameterTypes().stream().toArray(Class[]::new));
166 | Annotation[] annotations = method.getParameterAnnotations()[parameterNode.getParameterIndex()];
167 | Optional optionalParameterDetails = getParameterDetails(annotations);
168 | if (optionalParameterDetails.isPresent()) {
169 | return createErrorForParameter(optionalParameterDetails.get(), constraintViolation);
170 | }
171 |
172 | } catch (NoSuchMethodException e) {
173 | e.printStackTrace();
174 | }
175 |
176 | // Assumes that the request entity is invalid (annotated method parameter)
177 | ValidationError error = new ValidationError();
178 | error.setType(REQUEST_ENTITY);
179 | error.setMessage(constraintViolation.getMessage());
180 | return error;
181 | }
182 |
183 | return handleUnknownSource(constraintViolation);
184 | }
185 |
186 | /**
187 | * Handle an invalid bean. Can be:
188 | *
189 | * 1. Invalid request bean (annotated bean class)
190 | *
191 | * @param constraintViolation
192 | */
193 | private ValidationError handleInvalidBean(ConstraintViolation constraintViolation) {
194 |
195 | ValidationError error = new ValidationError();
196 | error.setType("entity");
197 | error.setMessage(constraintViolation.getMessage());
198 |
199 | return error;
200 | }
201 |
202 |
203 | /**
204 | * Handle other error situations. Works as as fallback.
205 | *
206 | * @param constraintViolation
207 | * @return
208 | */
209 | private ValidationError handleUnknownSource(ConstraintViolation constraintViolation) {
210 | ValidationError error = new ValidationError();
211 | error.setName(constraintViolation.getPropertyPath().toString());
212 | error.setMessage(constraintViolation.getMessage());
213 | return error;
214 | }
215 |
216 | /**
217 | * Create error for parameter.
218 | *
219 | * @param parameterDetails
220 | * @param constraintViolation
221 | * @return
222 | */
223 | private ValidationError createErrorForParameter(ParameterDetails parameterDetails, ConstraintViolation constraintViolation) {
224 | ValidationError error = new ValidationError();
225 | error.setType(parameterDetails.getType());
226 | error.setName(parameterDetails.getName());
227 | error.setMessage(constraintViolation.getMessage());
228 | return error;
229 | }
230 |
231 | /**
232 | * Get the leaf node.
233 | *
234 | * @param path
235 | * @return
236 | */
237 | private Optional getLeafNode(Path path) {
238 | return StreamSupport.stream(path.spliterator(), false).reduce((a, b) -> b);
239 | }
240 |
241 | /**
242 | * Get field from class.
243 | *
244 | * @param fieldName
245 | * @param beanClass
246 | * @return
247 | */
248 | private Optional getField(String fieldName, Class> beanClass) {
249 | return Arrays.stream(beanClass.getDeclaredFields())
250 | .filter(field -> field.getName().equals(fieldName))
251 | .findFirst();
252 | }
253 |
254 | /**
255 | * Get parameter details from JAX-RS annotation.
256 | *
257 | * @param annotations
258 | * @return
259 | */
260 | private Optional getParameterDetails(Annotation[] annotations) {
261 |
262 | for (Annotation annotation : annotations) {
263 | Optional optionalParameterDetails = getParameterDetails(annotation);
264 | if (optionalParameterDetails.isPresent()) {
265 | return optionalParameterDetails;
266 | }
267 | }
268 |
269 | return Optional.empty();
270 | }
271 |
272 | /**
273 | * Get the parameter details from JAX-RS annotation.
274 | *
275 | * @param annotation
276 | * @return
277 | */
278 | private Optional getParameterDetails(Annotation annotation) {
279 |
280 | ParameterDetails parameterDetails = new ParameterDetails();
281 |
282 | if (annotation instanceof QueryParam) {
283 | parameterDetails.setType(REQUEST_QUERY_PARAMETER);
284 | parameterDetails.setName(((QueryParam) annotation).value());
285 | return Optional.of(parameterDetails);
286 |
287 | } else if (annotation instanceof PathParam) {
288 | parameterDetails.setType(REQUEST_PATH_PARAMETER);
289 | parameterDetails.setName(((PathParam) annotation).value());
290 | return Optional.of(parameterDetails);
291 |
292 | } else if (annotation instanceof HeaderParam) {
293 | parameterDetails.setType(REQUEST_HEADER_PARAMETER);
294 | parameterDetails.setName(((HeaderParam) annotation).value());
295 | return Optional.of(parameterDetails);
296 |
297 | } else if (annotation instanceof CookieParam) {
298 | parameterDetails.setType(REQUEST_COOKIE_PARAMETER);
299 | parameterDetails.setName(((CookieParam) annotation).value());
300 | return Optional.of(parameterDetails);
301 |
302 | } else if (annotation instanceof FormParam) {
303 | parameterDetails.setType(REQUEST_FORM_PARAMETER);
304 | parameterDetails.setName(((FormParam) annotation).value());
305 | return Optional.of(parameterDetails);
306 |
307 | } else if (annotation instanceof MatrixParam) {
308 | parameterDetails.setType(REQUEST_MATRIX_PARAMETER);
309 | parameterDetails.setName(((MatrixParam) annotation).value());
310 | return Optional.of(parameterDetails);
311 | }
312 |
313 | return Optional.empty();
314 | }
315 |
316 | /**
317 | * Get the JSON property name.
318 | *
319 | * @param mapper
320 | * @param beanClass
321 | * @param nodeName
322 | * @return
323 | */
324 | private Optional getJsonPropertyName(ObjectMapper mapper, Class> beanClass, String nodeName) {
325 |
326 | JavaType javaType = mapper.getTypeFactory().constructType(beanClass);
327 | BeanDescription introspection = mapper.getSerializationConfig().introspect(javaType);
328 | List properties = introspection.findProperties();
329 |
330 | return properties.stream()
331 | .filter(propertyDefinition -> nodeName.equals(propertyDefinition.getField().getName()))
332 | .map(BeanPropertyDefinition::getName)
333 | .findFirst();
334 | }
335 |
336 | /**
337 | * Class to hold JAX-RS parameter details.
338 | */
339 | private static class ParameterDetails {
340 |
341 | private String type;
342 |
343 | private String name;
344 |
345 | public ParameterDetails() {
346 |
347 | }
348 |
349 | public String getType() {
350 | return type;
351 | }
352 |
353 | public void setType(String type) {
354 | this.type = type;
355 | }
356 |
357 | public String getName() {
358 | return name;
359 | }
360 |
361 | public void setName(String name) {
362 | this.name = name;
363 | }
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/exceptionmapper/JsonMappingExceptionMapper.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.exceptionmapper;
2 |
3 | import com.cassiomolin.example.common.api.model.ApiError;
4 | import com.fasterxml.jackson.databind.JsonMappingException;
5 |
6 | import javax.ws.rs.core.Context;
7 | import javax.ws.rs.core.MediaType;
8 | import javax.ws.rs.core.Response;
9 | import javax.ws.rs.core.Response.Status;
10 | import javax.ws.rs.core.UriInfo;
11 | import javax.ws.rs.ext.ExceptionMapper;
12 | import javax.ws.rs.ext.Provider;
13 | import java.time.Instant;
14 |
15 | /**
16 | * Component that maps a {@link JsonMappingException} to a HTTP response.
17 | *
18 | * @author cassiomolin
19 | */
20 | @Provider
21 | public class JsonMappingExceptionMapper implements ExceptionMapper {
22 |
23 | @Context
24 | private UriInfo uriInfo;
25 |
26 | @Override
27 | public Response toResponse(JsonMappingException exception) {
28 |
29 | ApiError error = new ApiError();
30 | error.setTimestamp(Instant.now().toEpochMilli());
31 | error.setStatus(Status.BAD_REQUEST.getStatusCode());
32 | error.setError(Status.BAD_REQUEST.getReasonPhrase());
33 | error.setMessage("Request JSON cannot be parsed");
34 | error.setPath(uriInfo.getAbsolutePath().getPath());
35 |
36 | return Response.status(Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON).build();
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/exceptionmapper/JsonParseExceptionMapper.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.exceptionmapper;
2 |
3 | import com.cassiomolin.example.common.api.model.ApiError;
4 | import com.fasterxml.jackson.core.JsonParseException;
5 |
6 | import javax.ws.rs.core.Context;
7 | import javax.ws.rs.core.MediaType;
8 | import javax.ws.rs.core.Response;
9 | import javax.ws.rs.core.Response.Status;
10 | import javax.ws.rs.core.UriInfo;
11 | import javax.ws.rs.ext.ExceptionMapper;
12 | import javax.ws.rs.ext.Provider;
13 | import java.time.Instant;
14 |
15 | /**
16 | * Component that maps a {@link JsonParseException} to a HTTP response.
17 | *
18 | * @author cassiomolin
19 | */
20 | @Provider
21 | public class JsonParseExceptionMapper implements ExceptionMapper {
22 |
23 | @Context
24 | private UriInfo uriInfo;
25 |
26 | @Override
27 | public Response toResponse(JsonParseException exception) {
28 |
29 | ApiError error = new ApiError();
30 | error.setTimestamp(Instant.now().toEpochMilli());
31 | error.setStatus(Status.BAD_REQUEST.getStatusCode());
32 | error.setError(Status.BAD_REQUEST.getReasonPhrase());
33 | error.setMessage("Request JSON cannot be parsed");
34 | error.setPath(uriInfo.getAbsolutePath().getPath());
35 |
36 | return Response.status(Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON).build();
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/filter/CorsFilter.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.filter;
2 |
3 | import javax.ws.rs.container.ContainerRequestContext;
4 | import javax.ws.rs.container.ContainerResponseContext;
5 | import javax.ws.rs.container.ContainerResponseFilter;
6 | import javax.ws.rs.ext.Provider;
7 |
8 | /**
9 | * Filter that adds CORS headers to the HTTP response.
10 | *
11 | * @author cassiomolin
12 | */
13 | @Provider
14 | public class CorsFilter implements ContainerResponseFilter {
15 |
16 | @Override
17 | public void filter(ContainerRequestContext request, ContainerResponseContext response) {
18 |
19 | response.getHeaders().add("Access-Control-Allow-Origin", "*");
20 | response.getHeaders().add("Access-Control-Allow-Credentials", "true");
21 | response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
22 | response.getHeaders().add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, Location");
23 | response.getHeaders().add("Access-Control-Expose-Headers", "Content-Type, Location");
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/model/ApiError.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 |
5 | /**
6 | * Model that represents an API error.
7 | *
8 | * @author cassiomolin
9 | */
10 | @JsonInclude(JsonInclude.Include.NON_NULL)
11 | public class ApiError {
12 |
13 | private Long timestamp;
14 |
15 | private Integer status;
16 |
17 | private String error;
18 |
19 | private String message;
20 |
21 | private String path;
22 |
23 | public ApiError() {
24 |
25 | }
26 |
27 | public Long getTimestamp() {
28 | return timestamp;
29 | }
30 |
31 | public void setTimestamp(Long timestamp) {
32 | this.timestamp = timestamp;
33 | }
34 |
35 | public Integer getStatus() {
36 | return status;
37 | }
38 |
39 | public void setStatus(Integer status) {
40 | this.status = status;
41 | }
42 |
43 | public String getError() {
44 | return error;
45 | }
46 |
47 | public void setError(String error) {
48 | this.error = error;
49 | }
50 |
51 | public String getMessage() {
52 | return message;
53 | }
54 |
55 | public void setMessage(String message) {
56 | this.message = message;
57 | }
58 |
59 | public String getPath() {
60 | return path;
61 | }
62 |
63 | public void setPath(String path) {
64 | this.path = path;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/model/ApiValidationError.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Model that represents an API validation error.
9 | *
10 | * @author cassiomolin
11 | */
12 | @JsonInclude(JsonInclude.Include.NON_NULL)
13 | public class ApiValidationError extends ApiError {
14 |
15 | private List validationErrors;
16 |
17 | public ApiValidationError() {
18 |
19 | }
20 |
21 | public List getValidationErrors() {
22 | return validationErrors;
23 | }
24 |
25 | public void setValidationErrors(List validationErrors) {
26 | this.validationErrors = validationErrors;
27 | }
28 |
29 | /**
30 | * Model that represents a validation error.
31 | *
32 | * @author cassiomolin
33 | */
34 | @JsonInclude(JsonInclude.Include.NON_NULL)
35 | public static class ValidationError {
36 |
37 | private String type;
38 |
39 | private String name;
40 |
41 | private String message;
42 |
43 | public ValidationError() {
44 |
45 | }
46 |
47 | public String getType() {
48 | return type;
49 | }
50 |
51 | public void setType(String type) {
52 | this.type = type;
53 | }
54 |
55 | public String getName() {
56 | return name;
57 | }
58 |
59 | public void setName(String name) {
60 | this.name = name;
61 | }
62 |
63 | public String getMessage() {
64 | return message;
65 | }
66 |
67 | public void setMessage(String message) {
68 | this.message = message;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/common/api/provider/ObjectMapperProvider.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.common.api.provider;
2 |
3 | import com.fasterxml.jackson.databind.DeserializationFeature;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.fasterxml.jackson.databind.SerializationFeature;
6 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
7 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
8 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
9 |
10 | import javax.ws.rs.ext.ContextResolver;
11 | import javax.ws.rs.ext.Provider;
12 |
13 | /**
14 | * Provider for {@link ObjectMapper}.
15 | *
16 | * @author cassiomolin
17 | */
18 | @Provider
19 | public class ObjectMapperProvider implements ContextResolver {
20 |
21 | private final ObjectMapper mapper;
22 |
23 | public ObjectMapperProvider() {
24 | mapper = createObjectMapper();
25 | }
26 |
27 | @Override
28 | public ObjectMapper getContext(Class> type) {
29 | return mapper;
30 | }
31 |
32 | private static ObjectMapper createObjectMapper() {
33 |
34 | ObjectMapper mapper = new ObjectMapper();
35 |
36 | mapper.registerModule(new ParameterNamesModule());
37 | mapper.registerModule(new Jdk8Module());
38 | mapper.registerModule(new JavaTimeModule());
39 |
40 | mapper.enable(SerializationFeature.INDENT_OUTPUT);
41 | mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
42 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
43 |
44 | mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
45 | mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
46 |
47 | return mapper;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/model/CreateTaskDetails.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.model;
2 |
3 | import org.hibernate.validator.constraints.NotBlank;
4 |
5 | /**
6 | * API model that holds details for creating a task.
7 | *
8 | * @author cassiomolin
9 | */
10 | public class CreateTaskDetails {
11 |
12 | @NotBlank
13 | private String description;
14 |
15 | public CreateTaskDetails() {
16 |
17 | }
18 |
19 | public String getDescription() {
20 | return description;
21 | }
22 |
23 | public void setDescription(String description) {
24 | this.description = description;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/model/QueryTaskResult.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.model;
2 |
3 | import java.time.ZonedDateTime;
4 |
5 | /**
6 | * API model for returning a task query result.
7 | *
8 | * @author cassiomolin
9 | */
10 | public class QueryTaskResult {
11 |
12 | private Long id;
13 |
14 | private String description;
15 |
16 | private Boolean completed;
17 |
18 | private ZonedDateTime createdDate;
19 |
20 | public QueryTaskResult() {
21 |
22 | }
23 |
24 | public Long getId() {
25 | return id;
26 | }
27 |
28 | public void setId(Long id) {
29 | this.id = id;
30 | }
31 |
32 | public String getDescription() {
33 | return description;
34 | }
35 |
36 | public void setDescription(String description) {
37 | this.description = description;
38 | }
39 |
40 | public Boolean getCompleted() {
41 | return completed;
42 | }
43 |
44 | public void setCompleted(Boolean completed) {
45 | this.completed = completed;
46 | }
47 |
48 | public ZonedDateTime getCreatedDate() {
49 | return createdDate;
50 | }
51 |
52 | public void setCreatedDate(ZonedDateTime createdDate) {
53 | this.createdDate = createdDate;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/model/UpdateTaskDetails.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.model;
2 |
3 | import org.hibernate.validator.constraints.NotBlank;
4 |
5 | import javax.validation.constraints.NotNull;
6 |
7 | /**
8 | * API model that holds details for updating a task.
9 | *
10 | * @author cassiomolin
11 | */
12 | public class UpdateTaskDetails {
13 |
14 | @NotBlank
15 | private String description;
16 |
17 | @NotNull
18 | private Boolean completed;
19 |
20 | public UpdateTaskDetails() {
21 |
22 | }
23 |
24 | public String getDescription() {
25 | return description;
26 | }
27 |
28 | public void setDescription(String description) {
29 | this.description = description;
30 | }
31 |
32 | public Boolean getCompleted() {
33 | return completed;
34 | }
35 |
36 | public void setCompleted(Boolean completed) {
37 | this.completed = completed;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/model/UpdateTaskStatusDetails.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.model;
2 |
3 | import javax.validation.constraints.NotNull;
4 |
5 | /**
6 | * API model that holds details for updating a task status.
7 | *
8 | * @author cassiomolin
9 | */
10 | public class UpdateTaskStatusDetails {
11 |
12 | @NotNull
13 | private Boolean value;
14 |
15 | public UpdateTaskStatusDetails() {
16 |
17 | }
18 |
19 | public Boolean getValue() {
20 | return value;
21 | }
22 |
23 | public void setValue(Boolean value) {
24 | this.value = value;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/model/mapper/TaskMapper.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.model.mapper;
2 |
3 |
4 | import com.cassiomolin.example.task.api.model.CreateTaskDetails;
5 | import com.cassiomolin.example.task.api.model.QueryTaskResult;
6 | import com.cassiomolin.example.task.api.model.UpdateTaskDetails;
7 | import com.cassiomolin.example.task.api.model.UpdateTaskStatusDetails;
8 | import com.cassiomolin.example.task.domain.Task;
9 | import org.mapstruct.Mapper;
10 | import org.mapstruct.Mapping;
11 | import org.mapstruct.MappingTarget;
12 | import org.mapstruct.ReportingPolicy;
13 |
14 | import java.util.List;
15 |
16 | /**
17 | * Component that maps a {@link Task} domain model to API models and vice versa.
18 | *
19 | * @author cassiomolin
20 | */
21 | @Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
22 | public interface TaskMapper {
23 |
24 | Task toTask(CreateTaskDetails createTaskDetails);
25 |
26 | QueryTaskResult toQueryTaskResult(Task task);
27 |
28 | List toQueryTaskResults(List tasks);
29 |
30 | void updateTask(UpdateTaskDetails updateTaskDetails, @MappingTarget Task task);
31 |
32 | @Mapping(source = "value", target = "completed")
33 | void updateTask(UpdateTaskStatusDetails updateTaskStatusDetails, @MappingTarget Task task);
34 | }
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/api/resource/TaskResource.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api.resource;
2 |
3 |
4 | import com.cassiomolin.example.task.api.model.CreateTaskDetails;
5 | import com.cassiomolin.example.task.api.model.QueryTaskResult;
6 | import com.cassiomolin.example.task.api.model.UpdateTaskDetails;
7 | import com.cassiomolin.example.task.api.model.UpdateTaskStatusDetails;
8 | import com.cassiomolin.example.task.api.model.mapper.TaskMapper;
9 | import com.cassiomolin.example.task.domain.Task;
10 | import com.cassiomolin.example.task.domain.TaskFilter;
11 | import com.cassiomolin.example.task.service.TaskService;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.stereotype.Component;
14 |
15 | import javax.validation.Valid;
16 | import javax.validation.constraints.NotNull;
17 | import javax.ws.rs.*;
18 | import javax.ws.rs.core.Context;
19 | import javax.ws.rs.core.MediaType;
20 | import javax.ws.rs.core.Response;
21 | import javax.ws.rs.core.UriInfo;
22 | import java.net.URI;
23 | import java.util.List;
24 |
25 | /**
26 | * Component that exposes REST API endpoints for {@link Task}s.
27 | *
28 | * @author cassiomolin
29 | */
30 | @Component
31 | @Path("tasks")
32 | @Consumes(MediaType.APPLICATION_JSON)
33 | @Produces(MediaType.APPLICATION_JSON)
34 | public class TaskResource {
35 |
36 | @Context
37 | private UriInfo uriInfo;
38 |
39 | @Autowired
40 | private TaskMapper taskMapper;
41 |
42 | @Autowired
43 | private TaskService taskService;
44 |
45 | @POST
46 | public Response createTask(@Valid @NotNull CreateTaskDetails createTaskDetails) {
47 | Task task = taskMapper.toTask(createTaskDetails);
48 | task = taskService.createTask(task);
49 | URI uri = uriInfo.getAbsolutePathBuilder().path(task.getId().toString()).build();
50 | return Response.created(uri).build();
51 | }
52 |
53 | @GET
54 | public Response findTasks(@QueryParam("description") String description,
55 | @QueryParam("completed") Boolean completed) {
56 | TaskFilter filter = new TaskFilter().setDescription(description).setCompleted(completed);
57 | List tasks = taskService.findTasks(filter);
58 | List queryTaskResults = taskMapper.toQueryTaskResults(tasks);
59 | return Response.ok(queryTaskResults).build();
60 | }
61 |
62 | @DELETE
63 | public Response deleteTasks(@QueryParam("completed") Boolean completed) {
64 | taskService.deleteTasks(completed);
65 | return Response.noContent().build();
66 | }
67 |
68 | @GET
69 | @Path("{taskId}")
70 | public Response getTask(@PathParam("taskId") Long taskId) {
71 | Task task = taskService.findTask(taskId).orElseThrow(NotFoundException::new);
72 | QueryTaskResult queryTaskResult = taskMapper.toQueryTaskResult(task);
73 | return Response.ok(queryTaskResult).build();
74 | }
75 |
76 | @PUT
77 | @Path("{taskId}")
78 | public Response updateTask(@PathParam("taskId") Long taskId,
79 | @Valid @NotNull UpdateTaskDetails updateTaskDetails) {
80 | Task task = taskService.findTask(taskId).orElseThrow(NotFoundException::new);
81 | taskMapper.updateTask(updateTaskDetails, task);
82 | taskService.updateTask(task);
83 | return Response.noContent().build();
84 | }
85 |
86 | @DELETE
87 | @Path("{taskId}")
88 | public Response deleteTask(@PathParam("taskId") Long taskId) {
89 | taskService.findTask(taskId).orElseThrow(NotFoundException::new);
90 | taskService.deleteTask(taskId);
91 | return Response.noContent().build();
92 | }
93 |
94 | @PUT
95 | @Path("{taskId}/completed")
96 | public Response updateTaskStatus(@PathParam("taskId") Long taskId,
97 | @Valid @NotNull UpdateTaskStatusDetails updateTaskStatusDetails) {
98 | Task task = taskService.findTask(taskId).orElseThrow(NotFoundException::new);
99 | taskMapper.updateTask(updateTaskStatusDetails, task);
100 | taskService.updateTask(task);
101 | return Response.noContent().build();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/domain/Task.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.domain;
2 |
3 | import javax.persistence.*;
4 | import javax.validation.constraints.NotNull;
5 | import java.util.Date;
6 |
7 | /**
8 | * JPA entity that represents a task.
9 | *
10 | * @author cassiomolin
11 | */
12 | @Entity
13 | public class Task {
14 |
15 | @Id
16 | @GeneratedValue
17 | private Long id;
18 |
19 | @NotNull
20 | private String description;
21 |
22 | @NotNull
23 | private Boolean completed;
24 |
25 | @Temporal(TemporalType.TIMESTAMP)
26 | private Date createdDate;
27 |
28 | public Task() {
29 |
30 | }
31 |
32 | public Long getId() {
33 | return id;
34 | }
35 |
36 | public void setId(Long id) {
37 | this.id = id;
38 | }
39 |
40 | public String getDescription() {
41 | return description;
42 | }
43 |
44 | public void setDescription(String description) {
45 | this.description = description;
46 | }
47 |
48 | public Boolean getCompleted() {
49 | return completed;
50 | }
51 |
52 | public void setCompleted(Boolean completed) {
53 | this.completed = completed;
54 | }
55 |
56 | public Date getCreatedDate() {
57 | return createdDate;
58 | }
59 |
60 | public void setCreatedDate(Date createdDate) {
61 | this.createdDate = createdDate;
62 | }
63 |
64 | @PrePersist
65 | void onCreate() {
66 | this.setCreatedDate(new Date());
67 | }
68 |
69 | @Override
70 | public boolean equals(Object o) {
71 | if (this == o) return true;
72 | if (o == null || getClass() != o.getClass()) return false;
73 |
74 | Task task = (Task) o;
75 |
76 | if (id != null ? !id.equals(task.id) : task.id != null) return false;
77 | if (description != null ? !description.equals(task.description) : task.description != null) return false;
78 | return completed != null ? completed.equals(task.completed) : task.completed == null;
79 | }
80 |
81 | @Override
82 | public int hashCode() {
83 | int result = id != null ? id.hashCode() : 0;
84 | result = 31 * result + (description != null ? description.hashCode() : 0);
85 | result = 31 * result + (completed != null ? completed.hashCode() : 0);
86 | return result;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/domain/TaskFilter.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.domain;
2 |
3 | /**
4 | * Model that represents a filter for {@link Task}s.
5 | *
6 | * @author cassiomolin
7 | */
8 | public class TaskFilter {
9 |
10 | private String description;
11 | private Boolean completed;
12 |
13 | public TaskFilter() {
14 |
15 | }
16 |
17 | public String getDescription() {
18 | return description;
19 | }
20 |
21 | public TaskFilter setDescription(String description) {
22 | this.description = description;
23 | return this;
24 | }
25 |
26 | public Boolean getCompleted() {
27 | return completed;
28 | }
29 |
30 | public TaskFilter setCompleted(Boolean completed) {
31 | this.completed = completed;
32 | return this;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/repository/TaskRepository.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.repository;
2 |
3 | import com.cassiomolin.example.task.domain.Task;
4 | import org.springframework.data.repository.CrudRepository;
5 | import org.springframework.data.repository.query.QueryByExampleExecutor;
6 | import org.springframework.stereotype.Repository;
7 |
8 | /**
9 | * Repository for {@link Task}s.
10 | *
11 | * @author cassiomolin
12 | */
13 | @Repository
14 | public interface TaskRepository extends CrudRepository, QueryByExampleExecutor {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/cassiomolin/example/task/service/TaskService.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.service;
2 |
3 | import com.cassiomolin.example.task.domain.Task;
4 | import com.cassiomolin.example.task.domain.TaskFilter;
5 | import com.cassiomolin.example.task.repository.TaskRepository;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.data.domain.Example;
8 | import org.springframework.data.domain.ExampleMatcher;
9 | import org.springframework.data.domain.Sort;
10 | import org.springframework.stereotype.Service;
11 | import org.springframework.transaction.annotation.Transactional;
12 |
13 | import javax.validation.constraints.NotNull;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 | import java.util.Optional;
17 |
18 | /**
19 | * Service that provides operations for {@link Task}s.
20 | *
21 | * @author cassiomolin
22 | */
23 | @Service
24 | @Transactional
25 | public class TaskService {
26 |
27 | @Autowired
28 | private TaskRepository taskRepository;
29 |
30 | /**
31 | * Create a task.
32 | *
33 | * @param task
34 | * @return
35 | */
36 | public Task createTask(@NotNull Task task) {
37 | task.setCompleted(false);
38 | return taskRepository.save(task);
39 | }
40 |
41 | /**
42 | * Find all tasks.
43 | *
44 | * @return
45 | */
46 | public List findAllTasks() {
47 | List list = new ArrayList<>();
48 | taskRepository.findAll().forEach(list::add);
49 | return list;
50 | }
51 |
52 | /**
53 | * Find tasks using a filter.
54 | *
55 | * @param filter
56 | * @return
57 | */
58 | public List findTasks(@NotNull TaskFilter filter) {
59 |
60 | Task task = new Task();
61 | task.setDescription(filter.getDescription());
62 | task.setCompleted(filter.getCompleted());
63 |
64 | ExampleMatcher matcher = ExampleMatcher.matching()
65 | .withMatcher("description", match -> match.contains().ignoreCase());
66 | Example example = Example.of(task, matcher);
67 |
68 | Sort sort = new Sort(Sort.Direction.ASC, "createdDate");
69 |
70 | List list = new ArrayList<>();
71 | taskRepository.findAll(example, sort).forEach(list::add);
72 |
73 | return list;
74 | }
75 |
76 | /**
77 | * Delete tasks.
78 | *
79 | * @param completed
80 | * @return
81 | */
82 | public void deleteTasks(Boolean completed) {
83 |
84 | if (completed == null) {
85 | taskRepository.deleteAll();
86 | } else {
87 | Task task = new Task();
88 | task.setCompleted(completed);
89 | Example example = Example.of(task);
90 | taskRepository.delete(taskRepository.findAll(example));
91 | }
92 | }
93 |
94 | /**
95 | * Find a task by id.
96 | *
97 | * @param taskId
98 | * @return
99 | */
100 | public Optional findTask(@NotNull Long taskId) {
101 | return Optional.ofNullable(taskRepository.findOne(taskId));
102 | }
103 |
104 | /**
105 | * Update a task.
106 | *
107 | * @param task
108 | */
109 | public void updateTask(@NotNull Task task) {
110 | taskRepository.save(task);
111 | }
112 |
113 | /**
114 | * Delete a task.
115 | *
116 | * @param taskId
117 | */
118 | public void deleteTask(@NotNull Long taskId) {
119 | taskRepository.delete(taskId);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/postman/tasks.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": [],
3 | "info": {
4 | "name": "tasks",
5 | "_postman_id": "e7d9ffe1-387b-6ac3-8373-083413533d29",
6 | "description": "",
7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
8 | },
9 | "item": [
10 | {
11 | "name": "Create a task",
12 | "request": {
13 | "url": "{{config.baseUrl}}/api/tasks",
14 | "method": "POST",
15 | "header": [
16 | {
17 | "key": "Content-Type",
18 | "value": "application/json",
19 | "description": ""
20 | }
21 | ],
22 | "body": {
23 | "mode": "raw",
24 | "raw": "{\n \"description\": \"Pay internet bill\"\n}"
25 | },
26 | "description": ""
27 | },
28 | "response": []
29 | },
30 | {
31 | "name": "Get all tasks",
32 | "request": {
33 | "url": "{{config.baseUrl}}/api/tasks",
34 | "method": "GET",
35 | "header": [
36 | {
37 | "key": "Accept",
38 | "value": "application/json",
39 | "description": ""
40 | }
41 | ],
42 | "body": {
43 | "mode": "raw",
44 | "raw": ""
45 | },
46 | "description": ""
47 | },
48 | "response": []
49 | },
50 | {
51 | "name": "Get tasks by description",
52 | "request": {
53 | "url": {
54 | "raw": "{{config.baseUrl}}/api/tasks?description=avocado",
55 | "auth": {},
56 | "host": [
57 | "{{config",
58 | "baseUrl}}"
59 | ],
60 | "path": [
61 | "api",
62 | "tasks"
63 | ],
64 | "query": [
65 | {
66 | "key": "description",
67 | "value": "avocado",
68 | "equals": true,
69 | "description": ""
70 | }
71 | ],
72 | "variable": []
73 | },
74 | "method": "GET",
75 | "header": [
76 | {
77 | "key": "Accept",
78 | "value": "application/json",
79 | "description": ""
80 | }
81 | ],
82 | "body": {
83 | "mode": "raw",
84 | "raw": ""
85 | },
86 | "description": ""
87 | },
88 | "response": []
89 | },
90 | {
91 | "name": "Get tasks by completed status",
92 | "request": {
93 | "url": {
94 | "raw": "{{config.baseUrl}}/api/tasks?completed=true",
95 | "auth": {},
96 | "host": [
97 | "{{config",
98 | "baseUrl}}"
99 | ],
100 | "path": [
101 | "api",
102 | "tasks"
103 | ],
104 | "query": [
105 | {
106 | "key": "completed",
107 | "value": "true",
108 | "equals": true,
109 | "description": ""
110 | }
111 | ],
112 | "variable": []
113 | },
114 | "method": "GET",
115 | "header": [
116 | {
117 | "key": "Accept",
118 | "value": "application/json",
119 | "description": ""
120 | }
121 | ],
122 | "body": {
123 | "mode": "raw",
124 | "raw": ""
125 | },
126 | "description": ""
127 | },
128 | "response": []
129 | },
130 | {
131 | "name": "Get all tasks by description and completed status",
132 | "request": {
133 | "url": {
134 | "raw": "{{config.baseUrl}}/api/tasks?description=karate&completed=true",
135 | "auth": {},
136 | "host": [
137 | "{{config",
138 | "baseUrl}}"
139 | ],
140 | "path": [
141 | "api",
142 | "tasks"
143 | ],
144 | "query": [
145 | {
146 | "key": "description",
147 | "value": "karate",
148 | "equals": true,
149 | "description": ""
150 | },
151 | {
152 | "key": "completed",
153 | "value": "true",
154 | "equals": true,
155 | "description": ""
156 | }
157 | ],
158 | "variable": []
159 | },
160 | "method": "GET",
161 | "header": [
162 | {
163 | "key": "Accept",
164 | "value": "application/json",
165 | "description": ""
166 | }
167 | ],
168 | "body": {
169 | "mode": "raw",
170 | "raw": ""
171 | },
172 | "description": ""
173 | },
174 | "response": []
175 | },
176 | {
177 | "name": "Get a task by id",
178 | "request": {
179 | "url": "{{config.baseUrl}}/api/tasks/5",
180 | "method": "GET",
181 | "header": [
182 | {
183 | "key": "Accept",
184 | "value": "application/json",
185 | "description": ""
186 | }
187 | ],
188 | "body": {
189 | "mode": "raw",
190 | "raw": ""
191 | },
192 | "description": ""
193 | },
194 | "response": []
195 | },
196 | {
197 | "name": "Update a task",
198 | "request": {
199 | "url": "{{config.baseUrl}}/api/tasks/5",
200 | "method": "PUT",
201 | "header": [
202 | {
203 | "key": "Content-Type",
204 | "value": "application/json",
205 | "description": ""
206 | }
207 | ],
208 | "body": {
209 | "mode": "raw",
210 | "raw": "{\n \"description\": \"Pay electricity bill\",\n \"completed\": false\n}"
211 | },
212 | "description": ""
213 | },
214 | "response": []
215 | },
216 | {
217 | "name": "Update a task completed status",
218 | "request": {
219 | "url": "{{config.baseUrl}}/api/tasks/5/completed",
220 | "method": "PUT",
221 | "header": [
222 | {
223 | "key": "Content-Type",
224 | "value": "application/json",
225 | "description": ""
226 | }
227 | ],
228 | "body": {
229 | "mode": "raw",
230 | "raw": "{\n \"value\": true\n}"
231 | },
232 | "description": ""
233 | },
234 | "response": []
235 | },
236 | {
237 | "name": "Delete a task",
238 | "request": {
239 | "url": "{{config.baseUrl}}/api/tasks/5",
240 | "method": "DELETE",
241 | "header": [],
242 | "body": {
243 | "mode": "raw",
244 | "raw": ""
245 | },
246 | "description": ""
247 | },
248 | "response": []
249 | },
250 | {
251 | "name": "Delete all tasks",
252 | "request": {
253 | "url": "{{config.baseUrl}}/api/tasks",
254 | "method": "DELETE",
255 | "header": [],
256 | "body": {
257 | "mode": "raw",
258 | "raw": ""
259 | },
260 | "description": ""
261 | },
262 | "response": []
263 | },
264 | {
265 | "name": "Delete all tasks by completed status",
266 | "request": {
267 | "url": {
268 | "raw": "{{config.baseUrl}}/api/tasks?completed=true",
269 | "auth": {},
270 | "host": [
271 | "{{config",
272 | "baseUrl}}"
273 | ],
274 | "path": [
275 | "api",
276 | "tasks"
277 | ],
278 | "query": [
279 | {
280 | "key": "completed",
281 | "value": "true",
282 | "equals": true,
283 | "description": ""
284 | }
285 | ],
286 | "variable": []
287 | },
288 | "method": "DELETE",
289 | "header": [],
290 | "body": {
291 | "mode": "raw",
292 | "raw": ""
293 | },
294 | "description": ""
295 | },
296 | "response": []
297 | }
298 | ]
299 | }
--------------------------------------------------------------------------------
/src/main/postman/tasks.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "73c166a1-12b1-a822-bbdb-39a38a2dea93",
3 | "name": "tasks",
4 | "values": [
5 | {
6 | "enabled": true,
7 | "key": "config.baseUrl",
8 | "value": "http://localhost:8080",
9 | "type": "text"
10 | }
11 | ],
12 | "timestamp": 1499450877479,
13 | "_postman_variable_scope": "environment",
14 | "_postman_exported_at": "2017-07-07T18:09:38.434Z",
15 | "_postman_exported_using": "Postman/4.11.1"
16 | }
--------------------------------------------------------------------------------
/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cassiomolin/tasks-rest-api/c470ddcd2a3a43629fff7f6d26b8106084b9614c/src/main/resources/application.yaml
--------------------------------------------------------------------------------
/src/main/resources/data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO task (id, description, completed, created_date) VALUES (1, 'Buy avocado', FALSE, CURRENT_TIMESTAMP);
2 | INSERT INTO task (id, description, completed, created_date) VALUES (2, 'Water the plants', TRUE, CURRENT_TIMESTAMP);
3 | INSERT INTO task (id, description, completed, created_date) VALUES (3, 'Feed the hamster', FALSE, CURRENT_TIMESTAMP);
4 | INSERT INTO task (id, description, completed, created_date) VALUES (4, 'Take grandma to karate lessons', TRUE, CURRENT_TIMESTAMP);
--------------------------------------------------------------------------------
/src/test/java/com/cassiomolin/example/task/api/TaskResourceTest.java:
--------------------------------------------------------------------------------
1 | package com.cassiomolin.example.task.api;
2 |
3 | import com.cassiomolin.example.task.api.model.CreateTaskDetails;
4 | import com.cassiomolin.example.task.api.model.QueryTaskResult;
5 | import com.cassiomolin.example.task.api.model.UpdateTaskDetails;
6 | import com.cassiomolin.example.task.api.model.UpdateTaskStatusDetails;
7 | import io.restassured.RestAssured;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.springframework.boot.context.embedded.LocalServerPort;
12 | import org.springframework.boot.test.context.SpringBootTest;
13 | import org.springframework.test.context.jdbc.Sql;
14 | import org.springframework.test.context.junit4.SpringRunner;
15 |
16 | import javax.ws.rs.core.HttpHeaders;
17 | import javax.ws.rs.core.MediaType;
18 | import javax.ws.rs.core.Response.Status;
19 | import java.util.Arrays;
20 |
21 | import static io.restassured.RestAssured.given;
22 | import static org.hamcrest.Matchers.notNullValue;
23 | import static org.hibernate.validator.internal.util.Contracts.assertNotNull;
24 |
25 | /**
26 | * Tests for the task resource class.
27 | *
28 | * @author cassiomolin
29 | */
30 | @RunWith(SpringRunner.class)
31 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
32 | public class TaskResourceTest {
33 |
34 | @LocalServerPort
35 | private int port;
36 |
37 | @Before
38 | public void setUp() throws Exception {
39 | RestAssured.baseURI = "http://localhost";
40 | RestAssured.port = port;
41 | RestAssured.basePath = "/api";
42 | }
43 |
44 | @Test
45 | public void createTask() {
46 |
47 | CreateTaskDetails createTaskDetails = new CreateTaskDetails();
48 | createTaskDetails.setDescription("Pay electricity bill");
49 |
50 | given()
51 | .accept(MediaType.APPLICATION_JSON)
52 | .contentType(MediaType.APPLICATION_JSON)
53 | .body(createTaskDetails)
54 | .expect()
55 | .statusCode(Status.CREATED.getStatusCode())
56 | .header(HttpHeaders.LOCATION, notNullValue())
57 | .when()
58 | .post("/tasks")
59 | .then()
60 | .log().all();
61 | }
62 |
63 | @Test
64 | public void createTaskWithInvalidDetails() {
65 |
66 | CreateTaskDetails createTaskDetails = new CreateTaskDetails();
67 |
68 | given()
69 | .accept(MediaType.APPLICATION_JSON)
70 | .contentType(MediaType.APPLICATION_JSON)
71 | .body(createTaskDetails)
72 | .expect()
73 | .statusCode(Status.BAD_REQUEST.getStatusCode())
74 | .when()
75 | .post("/tasks")
76 | .then()
77 | .log().all();
78 | }
79 |
80 | @Test
81 | public void findTasks() {
82 |
83 | QueryTaskResult[] taskQueryResults =
84 |
85 | given()
86 | .accept(MediaType.APPLICATION_JSON)
87 | .expect()
88 | .statusCode(Status.OK.getStatusCode())
89 | .contentType(MediaType.APPLICATION_JSON)
90 | .when()
91 | .get("/tasks")
92 | .then()
93 | .log().all()
94 | .extract()
95 | .body().as(QueryTaskResult[].class);
96 |
97 | Arrays.stream(taskQueryResults).forEach(task -> {
98 | assertNotNull(task.getId());
99 | assertNotNull(task.getDescription());
100 | assertNotNull(task.getCompleted());
101 | });
102 | }
103 |
104 | @Test
105 | public void getTask() {
106 |
107 | QueryTaskResult queryTaskResult =
108 |
109 | given()
110 | .accept(MediaType.APPLICATION_JSON)
111 | .pathParam("taskId", 1)
112 | .expect()
113 | .statusCode(Status.OK.getStatusCode())
114 | .contentType(MediaType.APPLICATION_JSON)
115 | .when()
116 | .get("/tasks/{taskId}")
117 | .then()
118 | .log().all()
119 | .extract()
120 | .body().as(QueryTaskResult.class);
121 |
122 | assertNotNull(queryTaskResult.getId());
123 | assertNotNull(queryTaskResult.getDescription());
124 | assertNotNull(queryTaskResult.getCompleted());
125 | }
126 |
127 | @Test
128 | public void getTaskWithInvalidIdentifier() {
129 |
130 | given()
131 | .accept(MediaType.APPLICATION_JSON)
132 | .pathParam("taskId", Integer.MAX_VALUE)
133 | .expect()
134 | .statusCode(Status.NOT_FOUND.getStatusCode())
135 | .contentType(MediaType.APPLICATION_JSON)
136 | .when()
137 | .get("/tasks/{taskId}")
138 | .then()
139 | .log().all();
140 | }
141 |
142 | @Test
143 | public void updateTask() {
144 |
145 | UpdateTaskDetails updateTaskDetails = new UpdateTaskDetails();
146 | updateTaskDetails.setDescription("Buy chocolate");
147 | updateTaskDetails.setCompleted(false);
148 |
149 | given()
150 | .accept(MediaType.APPLICATION_JSON)
151 | .contentType(MediaType.APPLICATION_JSON)
152 | .body(updateTaskDetails)
153 | .pathParam("taskId", 1)
154 | .expect()
155 | .statusCode(Status.NO_CONTENT.getStatusCode())
156 | .when()
157 | .put("/tasks/{taskId}")
158 | .then()
159 | .log().all();
160 | }
161 |
162 | @Test
163 | public void updateTaskWithInvalidDetails() {
164 |
165 | UpdateTaskDetails updateTaskDetails = new UpdateTaskDetails();
166 |
167 | given()
168 | .accept(MediaType.APPLICATION_JSON)
169 | .contentType(MediaType.APPLICATION_JSON)
170 | .body(updateTaskDetails)
171 | .pathParam("taskId", 1)
172 | .expect()
173 | .statusCode(Status.BAD_REQUEST.getStatusCode())
174 | .when()
175 | .put("/tasks/{taskId}")
176 | .then()
177 | .log().all();
178 | }
179 |
180 | @Test
181 | public void deleteTask() {
182 |
183 | given()
184 | .pathParam("taskId", 1)
185 | .expect()
186 | .statusCode(Status.NO_CONTENT.getStatusCode())
187 | .when()
188 | .delete("/tasks/{taskId}")
189 | .then()
190 | .log().all();
191 | }
192 |
193 | @Test
194 | public void deleteAllCompletedTasks() {
195 |
196 | given()
197 | .queryParam("completed", true)
198 | .expect()
199 | .statusCode(Status.NO_CONTENT.getStatusCode())
200 | .when()
201 | .delete("/tasks")
202 | .then()
203 | .log().all();
204 | }
205 |
206 |
207 | @Test
208 | public void deleteTaskWithInvalidIdentifier() {
209 |
210 | given()
211 | .pathParam("taskId", Integer.MAX_VALUE)
212 | .expect()
213 | .statusCode(Status.NOT_FOUND.getStatusCode())
214 | .when()
215 | .delete("/tasks/{taskId}")
216 | .then()
217 | .log().all();
218 | }
219 |
220 | @Test
221 | public void updateTaskStatus() {
222 |
223 | UpdateTaskStatusDetails updateTaskStatusDetails = new UpdateTaskStatusDetails();
224 | updateTaskStatusDetails.setValue(true);
225 |
226 | given()
227 | .accept(MediaType.APPLICATION_JSON)
228 | .contentType(MediaType.APPLICATION_JSON)
229 | .body(updateTaskStatusDetails)
230 | .pathParam("taskId", 1)
231 | .expect()
232 | .statusCode(Status.NO_CONTENT.getStatusCode())
233 | .when()
234 | .put("/tasks/{taskId}/completed")
235 | .then()
236 | .log().all();
237 | }
238 | }
239 |
--------------------------------------------------------------------------------