1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.json.resource.http;
18
19 import static java.lang.String.format;
20 import static java.util.Arrays.asList;
21 import static org.forgerock.http.protocol.Status.CREATED;
22 import static org.forgerock.http.protocol.Status.NO_CONTENT;
23 import static org.forgerock.http.protocol.Status.OK;
24 import static org.forgerock.json.JsonValueFunctions.enumConstant;
25 import static org.forgerock.json.resource.QueryResponse.FIELD_ERROR;
26 import static org.forgerock.json.resource.QueryResponse.FIELD_PAGED_RESULTS_COOKIE;
27 import static org.forgerock.json.resource.QueryResponse.FIELD_RESULT;
28 import static org.forgerock.json.resource.QueryResponse.FIELD_TOTAL_PAGED_RESULTS;
29 import static org.forgerock.json.resource.QueryResponse.FIELD_TOTAL_PAGED_RESULTS_POLICY;
30 import static org.forgerock.json.resource.QueryResponse.NO_COUNT;
31 import static org.forgerock.json.resource.ResourceException.FIELD_CODE;
32 import static org.forgerock.json.resource.ResourceException.FIELD_DETAIL;
33 import static org.forgerock.json.resource.ResourceException.FIELD_MESSAGE;
34 import static org.forgerock.json.resource.ResourceException.FIELD_REASON;
35 import static org.forgerock.json.resource.ResourceException.newResourceException;
36 import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_ID;
37 import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_REVISION;
38 import static org.forgerock.json.resource.Responses.newActionResponse;
39 import static org.forgerock.json.resource.Responses.newQueryResponse;
40 import static org.forgerock.json.resource.Responses.newResourceResponse;
41 import static org.forgerock.json.resource.http.HttpUtils.DEFAULT_PROTOCOL_VERSION;
42 import static org.forgerock.json.resource.http.HttpUtils.ETAG_ANY;
43 import static org.forgerock.json.resource.http.HttpUtils.FIELDS_DELIMITER;
44 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_MATCH;
45 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_NONE_MATCH;
46 import static org.forgerock.json.resource.http.HttpUtils.METHOD_DELETE;
47 import static org.forgerock.json.resource.http.HttpUtils.METHOD_GET;
48 import static org.forgerock.json.resource.http.HttpUtils.METHOD_PATCH;
49 import static org.forgerock.json.resource.http.HttpUtils.METHOD_POST;
50 import static org.forgerock.json.resource.http.HttpUtils.METHOD_PUT;
51 import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_APPLICATION_JSON;
52 import static org.forgerock.json.resource.http.HttpUtils.PARAM_ACTION;
53 import static org.forgerock.json.resource.http.HttpUtils.PARAM_FIELDS;
54 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_COOKIE;
55 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_OFFSET;
56 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGE_SIZE;
57 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_EXPRESSION;
58 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_FILTER;
59 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_ID;
60 import static org.forgerock.json.resource.http.HttpUtils.PARAM_SORT_KEYS;
61 import static org.forgerock.json.resource.http.HttpUtils.PARAM_TOTAL_PAGED_RESULTS_POLICY;
62 import static org.forgerock.json.resource.http.HttpUtils.SORT_KEYS_DELIMITER;
63 import static org.forgerock.util.CloseSilentlyFunction.closeSilently;
64 import static org.forgerock.util.Reject.checkNotNull;
65 import static org.forgerock.util.Utils.joinAsString;
66
67 import java.io.IOException;
68 import java.net.URI;
69 import java.net.URISyntaxException;
70 import java.util.LinkedList;
71 import java.util.List;
72 import java.util.Map;
73
74 import org.forgerock.http.Handler;
75 import org.forgerock.http.MutableUri;
76 import org.forgerock.http.protocol.Responses;
77 import org.forgerock.http.header.AcceptApiVersionHeader;
78 import org.forgerock.http.header.ContentApiVersionHeader;
79 import org.forgerock.http.header.ContentTypeHeader;
80 import org.forgerock.http.protocol.Form;
81 import org.forgerock.http.protocol.Request;
82 import org.forgerock.http.protocol.Response;
83 import org.forgerock.http.protocol.Status;
84 import org.forgerock.http.routing.Version;
85 import org.forgerock.json.JsonValue;
86 import org.forgerock.json.resource.ActionRequest;
87 import org.forgerock.json.resource.ActionResponse;
88 import org.forgerock.json.resource.CountPolicy;
89 import org.forgerock.json.resource.CreateRequest;
90 import org.forgerock.json.resource.DeleteRequest;
91 import org.forgerock.json.resource.InternalServerErrorException;
92 import org.forgerock.json.resource.PatchOperation;
93 import org.forgerock.json.resource.PatchRequest;
94 import org.forgerock.json.resource.QueryRequest;
95 import org.forgerock.json.resource.QueryResourceHandler;
96 import org.forgerock.json.resource.QueryResponse;
97 import org.forgerock.json.resource.ReadRequest;
98 import org.forgerock.json.resource.RequestHandler;
99 import org.forgerock.json.resource.ResourceException;
100 import org.forgerock.json.resource.ResourceResponse;
101 import org.forgerock.json.resource.SortKey;
102 import org.forgerock.json.resource.UpdateRequest;
103 import org.forgerock.services.context.Context;
104 import org.forgerock.util.Function;
105 import org.forgerock.util.promise.Promise;
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 final class CrestAdapter implements RequestHandler {
139
140 private static final Status NOT_MODIFIED = Status.valueOf(304, "Not Modified");
141
142 private final Handler handler;
143 private final URI baseUri;
144
145
146
147
148
149
150
151
152
153 public CrestAdapter(Handler handler, URI uri) {
154 this.handler = checkNotNull(handler);
155 this.baseUri = checkNotNull(uri);
156 }
157
158 @Override
159 public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
160
161 Request httpRequest = new Request();
162 prepareHttpRequest(request, httpRequest);
163 httpRequest.setMethod(METHOD_POST);
164 Form form = new Form();
165 form.putSingle(PARAM_ACTION, request.getAction());
166 form.appendRequestQuery(httpRequest);
167 if (request.getContent() != null) {
168 httpRequest.getEntity().setJson(request.getContent().getObject());
169 }
170
171
172 return handler.handle(context, httpRequest)
173 .then(closeSilently(new Function<Response, ActionResponse, ResourceException>() {
174 @Override
175 public ActionResponse apply(Response response) throws ResourceException {
176
177
178
179
180
181
182
183 JsonValue content = loadJsonValueContent(response);
184
185 if (OK.equals(response.getStatus()) || NO_CONTENT.equals(response.getStatus())) {
186 return setResourceVersion(response, newActionResponse(content));
187 } else {
188 throw createResourceException(response, content);
189 }
190 }
191 }), Responses.<ActionResponse, ResourceException>noopExceptionFunction());
192 }
193
194 @Override
195 public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
196 final Request httpRequest = new Request();
197 prepareHttpRequest(request, httpRequest);
198
199 String resourceId = request.getNewResourceId();
200 if (resourceId == null) {
201
202 httpRequest.setMethod(METHOD_POST);
203 } else {
204
205 httpRequest.setMethod(METHOD_PUT);
206 MutableUri uri = httpRequest.getUri();
207 try {
208
209 uri.setPath(uri.getPath() + "/" + request.getNewResourceId());
210 } catch (URISyntaxException e) {
211 return new InternalServerErrorException("Cannot rebuild resource path", e).asPromise();
212 }
213 setIfNoneMatchToAny(httpRequest);
214 }
215
216 if (request.getContent() != null) {
217 httpRequest.getEntity().setJson(request.getContent().getObject());
218 }
219
220
221 return handler.handle(context, httpRequest)
222 .then(closeSilently(new Function<Response, ResourceResponse, ResourceException>() {
223 @Override
224 public ResourceResponse apply(Response response) throws ResourceException {
225
226
227
228
229
230
231
232 JsonValue content = loadJsonValueContent(response);
233
234 if (CREATED.equals(response.getStatus())) {
235 return setResourceVersion(response, createResourceResponse(content));
236 } else {
237 throw createResourceException(response, content);
238 }
239 }
240 }), Responses.<ResourceResponse, ResourceException>noopExceptionFunction());
241 }
242
243 @Override
244 public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
245
246 Request httpRequest = new Request();
247 httpRequest.setMethod(METHOD_DELETE);
248 prepareHttpRequest(request, httpRequest);
249 setIfMatch(httpRequest, request.getRevision());
250
251
252 return handler.handle(context, httpRequest)
253 .then(buildCrestResponse(asList(Status.OK)),
254 Responses.<ResourceResponse, ResourceException>noopExceptionFunction());
255 }
256
257 @Override
258 public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
259 Request httpRequest = new Request();
260 httpRequest.setMethod(METHOD_PATCH);
261 prepareHttpRequest(request, httpRequest);
262 setIfMatch(httpRequest, request.getRevision());
263
264 if (!request.getPatchOperations().isEmpty()) {
265 JsonValue content = new JsonValue(new LinkedList<>());
266 for (PatchOperation operation : request.getPatchOperations()) {
267 content.add(operation.toJsonValue().getObject());
268 }
269 httpRequest.getEntity().setJson(content.getObject());
270 }
271
272
273 return handler.handle(context, httpRequest)
274 .then(buildCrestResponse(asList(Status.OK)),
275 Responses.<ResourceResponse, ResourceException>noopExceptionFunction());
276 }
277
278 @Override
279 public Promise<QueryResponse, ResourceException> handleQuery(Context context,
280 QueryRequest request,
281 final QueryResourceHandler queryHandler) {
282 Request httpRequest = new Request();
283 prepareHttpRequest(request, httpRequest);
284 httpRequest.setMethod(METHOD_GET);
285 Form form = new Form();
286 putIfNotNull(form, PARAM_QUERY_ID, request.getQueryId());
287 putIfNotNull(form, PARAM_QUERY_EXPRESSION, request.getQueryExpression());
288 putIfNotNull(form, PARAM_QUERY_FILTER, request.getQueryFilter());
289 putIfNotNull(form, PARAM_TOTAL_PAGED_RESULTS_POLICY, request.getTotalPagedResultsPolicy());
290 putIfNotNull(form, PARAM_PAGED_RESULTS_COOKIE, request.getPagedResultsCookie());
291 List<SortKey> sortKeys = request.getSortKeys();
292 if (sortKeys != null && !sortKeys.isEmpty()) {
293 form.putSingle(PARAM_SORT_KEYS, joinAsString(SORT_KEYS_DELIMITER));
294 }
295 if (request.getPageSize() > 0) {
296 form.putSingle(PARAM_PAGE_SIZE, String.valueOf(request.getPageSize()));
297 }
298 if (request.getPagedResultsOffset() >= 1) {
299 form.putSingle(PARAM_PAGED_RESULTS_OFFSET, String.valueOf(request.getPagedResultsOffset()));
300 }
301 if (!form.isEmpty()) {
302 form.appendRequestQuery(httpRequest);
303 }
304
305
306 return handler.handle(context, httpRequest)
307 .then(closeSilently(new Function<Response, QueryResponse, ResourceException>() {
308 @Override
309 public QueryResponse apply(Response response) throws ResourceException {
310
311
312
313
314
315
316
317 JsonValue content = loadJsonValueContent(response);
318
319 if (OK.equals(response.getStatus()) && !content.isDefined(FIELD_ERROR)) {
320
321 String pagedResultsCookie = content.get(FIELD_PAGED_RESULTS_COOKIE).asString();
322 CountPolicy countPolicy = content.get(FIELD_TOTAL_PAGED_RESULTS_POLICY)
323 .as(enumConstant(CountPolicy.class));
324 Integer totalPagedResults = content.get(FIELD_TOTAL_PAGED_RESULTS)
325 .defaultTo(NO_COUNT)
326 .asInteger();
327 QueryResponse queryResponse = newQueryResponse(pagedResultsCookie,
328 countPolicy,
329 totalPagedResults);
330 for (JsonValue value : content.get(FIELD_RESULT)) {
331 queryHandler.handleResource(createResourceResponse(value));
332 }
333
334 return setResourceVersion(response, queryResponse);
335 } else {
336 throw createResourceException(response, content);
337 }
338 }
339 }), Responses.<QueryResponse, ResourceException>noopExceptionFunction());
340 }
341
342 @Override
343 public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
344
345 Request httpRequest = new Request();
346 httpRequest.setMethod(METHOD_GET);
347 prepareHttpRequest(request, httpRequest);
348
349
350 return handler.handle(context, httpRequest)
351 .then(buildCrestResponse(asList(Status.OK, NOT_MODIFIED)),
352 Responses.<ResourceResponse, ResourceException>noopExceptionFunction());
353 }
354
355 @Override
356 public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
357
358 Request httpRequest = new Request();
359 httpRequest.setMethod(METHOD_PUT);
360 prepareHttpRequest(request, httpRequest);
361 setIfMatch(httpRequest, request.getRevision());
362 if (request.getContent() != null) {
363 httpRequest.getEntity().setJson(request.getContent().getObject());
364 }
365
366
367 return handler.handle(context, httpRequest)
368 .then(buildCrestResponse(asList(Status.OK)),
369 Responses.<ResourceResponse, ResourceException>noopExceptionFunction());
370 }
371
372 private static Function<Response, ResourceResponse, ResourceException> buildCrestResponse(
373 final List<Status> accepted) {
374 return closeSilently(new Function<Response, ResourceResponse, ResourceException>() {
375 @Override
376 public ResourceResponse apply(Response response) throws ResourceException {
377
378
379
380
381
382
383
384 JsonValue content = loadJsonValueContent(response);
385
386 if (accepted.contains(response.getStatus())) {
387 return setResourceVersion(response, createResourceResponse(content));
388 } else {
389 throw createResourceException(response, content);
390 }
391 }
392 });
393 }
394
395 private static void putIfNotNull(Form form, String name, Object value) {
396 if (value != null) {
397 form.putSingle(name, value.toString());
398 }
399 }
400
401 private static JsonValue loadJsonValueContent(final Response response) throws ResourceException {
402 if (MIME_TYPE_APPLICATION_JSON.equals(ContentTypeHeader.valueOf(response).getType())) {
403 try {
404 return new JsonValue(response.getEntity().getJson());
405 } catch (IOException e) {
406 throw new InternalServerErrorException("Cannot parse HTTP response content as JSON", e);
407 }
408 }
409 throw new InternalServerErrorException("Response is not application/json");
410 }
411
412 private static ResourceResponse createResourceResponse(final JsonValue content) {
413 return newResourceResponse(content.get(FIELD_CONTENT_ID).asString(),
414 content.get(FIELD_CONTENT_REVISION).asString(),
415 content);
416 }
417
418 private static <T extends org.forgerock.json.resource.Response> T setResourceVersion(final Response httpResponse,
419 final T result) {
420 if (httpResponse.getHeaders().containsKey(ContentApiVersionHeader.NAME)) {
421 Version resourceVersion = ContentApiVersionHeader.valueOf(httpResponse).getResourceVersion();
422 result.setResourceApiVersion(resourceVersion);
423 }
424 return result;
425 }
426
427 private static void setRequestedResourceVersion(Request request, Version resourceVersion) {
428
429 request.getHeaders().put(new AcceptApiVersionHeader(DEFAULT_PROTOCOL_VERSION,
430 resourceVersion));
431 }
432
433 private static ResourceException createResourceException(final Response response, final JsonValue content) {
434 ResourceException exception = newResourceException(content.get(FIELD_CODE)
435 .defaultTo(response.getStatus().getCode())
436 .asInteger(),
437 content.get(FIELD_MESSAGE).asString());
438
439 if (content.isDefined(FIELD_DETAIL)) {
440 exception.setDetail(content.get(FIELD_DETAIL));
441 }
442 if (content.isDefined(FIELD_REASON)) {
443 exception.setReason(content.get(FIELD_REASON).asString());
444 }
445
446 return setResourceVersion(response, exception);
447 }
448
449 private static void setIfMatch(final Request request, final String revision) {
450 String value = "*";
451 if (revision != null) {
452 value = format("\"%s\"", revision);
453 }
454 request.getHeaders().put(HEADER_IF_MATCH, value);
455 }
456
457 private static void setIfNoneMatchToAny(final Request request) {
458 request.getHeaders().put(HEADER_IF_NONE_MATCH, ETAG_ANY);
459 }
460
461 private void prepareHttpRequest(final org.forgerock.json.resource.Request request, final Request httpRequest) {
462 setRequestedResourceVersion(httpRequest, request.getResourceVersion());
463
464 httpRequest.setUri(baseUri.resolve(request.getResourcePath()));
465
466 final Form form = new Form();
467 if (!request.getFields().isEmpty()) {
468 form.putSingle(PARAM_FIELDS, joinAsString(FIELDS_DELIMITER, request.getFields().toArray()));
469 }
470 for (Map.Entry<String, String> entry : request.getAdditionalParameters().entrySet()) {
471 form.putSingle(entry.getKey(), entry.getValue());
472 }
473 if (!form.isEmpty()) {
474 form.toRequestQuery(httpRequest);
475 }
476 }
477
478 }