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 org.forgerock.http.util.Paths.addLeadingSlash;
20 import static org.forgerock.json.resource.QueryResponse.FIELD_ERROR;
21 import static org.forgerock.json.resource.QueryResponse.FIELD_PAGED_RESULTS_COOKIE;
22 import static org.forgerock.json.resource.QueryResponse.FIELD_REMAINING_PAGED_RESULTS;
23 import static org.forgerock.json.resource.QueryResponse.FIELD_RESULT;
24 import static org.forgerock.json.resource.QueryResponse.FIELD_RESULT_COUNT;
25 import static org.forgerock.json.resource.QueryResponse.FIELD_TOTAL_PAGED_RESULTS;
26 import static org.forgerock.json.resource.QueryResponse.FIELD_TOTAL_PAGED_RESULTS_POLICY;
27 import static org.forgerock.json.resource.Requests.newUpdateRequest;
28 import static org.forgerock.json.resource.ResourceException.newResourceException;
29 import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_ID;
30 import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_REVISION;
31 import static org.forgerock.json.resource.http.HttpUtils.HEADER_ETAG;
32 import static org.forgerock.json.resource.http.HttpUtils.HEADER_LOCATION;
33 import static org.forgerock.json.resource.http.HttpUtils.JSON_MAPPER;
34 import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_APPLICATION_JSON;
35 import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_TEXT_PLAIN;
36 import static org.forgerock.json.resource.http.HttpUtils.PROTOCOL_VERSION_2;
37 import static org.forgerock.json.resource.http.HttpUtils.adapt;
38 import static org.forgerock.json.resource.http.HttpUtils.fail;
39 import static org.forgerock.json.resource.http.HttpUtils.getIfNoneMatch;
40 import static org.forgerock.json.resource.http.HttpUtils.getJsonGenerator;
41 import static org.forgerock.json.resource.http.HttpUtils.getRequestedProtocolVersion;
42 import static org.forgerock.json.resource.http.HttpUtils.writeCacheControlHeader;
43 import static org.forgerock.json.resource.http.HttpUtils.writeContentTypeHeader;
44 import static org.forgerock.util.Utils.closeSilently;
45 import static org.forgerock.util.promise.Promises.newResultPromise;
46
47 import com.fasterxml.jackson.core.JsonGenerator;
48 import com.fasterxml.jackson.databind.ObjectWriter;
49 import jakarta.mail.internet.ContentType;
50 import jakarta.mail.internet.ParseException;
51 import java.io.IOException;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.concurrent.atomic.AtomicBoolean;
55 import java.util.concurrent.atomic.AtomicInteger;
56 import org.forgerock.http.header.ContentApiVersionHeader;
57 import org.forgerock.http.header.ContentTypeHeader;
58 import org.forgerock.http.header.MalformedHeaderException;
59 import org.forgerock.http.protocol.Response;
60 import org.forgerock.http.protocol.Status;
61 import org.forgerock.http.routing.UriRouterContext;
62 import org.forgerock.http.routing.Version;
63 import org.forgerock.http.util.Json;
64 import org.forgerock.json.JsonValue;
65 import org.forgerock.json.resource.ActionRequest;
66 import org.forgerock.json.resource.ActionResponse;
67 import org.forgerock.json.resource.AdviceContext;
68 import org.forgerock.json.resource.Connection;
69 import org.forgerock.json.resource.CreateNotSupportedException;
70 import org.forgerock.json.resource.CreateRequest;
71 import org.forgerock.json.resource.DeleteRequest;
72 import org.forgerock.json.resource.PatchRequest;
73 import org.forgerock.json.resource.PreconditionFailedException;
74 import org.forgerock.json.resource.QueryRequest;
75 import org.forgerock.json.resource.QueryResourceHandler;
76 import org.forgerock.json.resource.QueryResponse;
77 import org.forgerock.json.resource.ReadRequest;
78 import org.forgerock.json.resource.Request;
79 import org.forgerock.json.resource.RequestVisitor;
80 import org.forgerock.json.resource.ResourceException;
81 import org.forgerock.json.resource.ResourceResponse;
82 import org.forgerock.json.resource.UpdateRequest;
83 import org.forgerock.services.context.Context;
84 import org.forgerock.util.AsyncFunction;
85 import org.forgerock.util.encode.Base64url;
86 import org.forgerock.util.promise.ExceptionHandler;
87 import org.forgerock.util.promise.NeverThrowsException;
88 import org.forgerock.util.promise.Promise;
89 import org.forgerock.util.promise.ResultHandler;
90
91
92
93
94 final class RequestRunner implements RequestVisitor<Promise<Response, NeverThrowsException>, Void> {
95
96
97 private Connection connection = null;
98 private final Context context;
99 private final org.forgerock.http.protocol.Request httpRequest;
100 private final Response httpResponse;
101 private final Version protocolVersion;
102 private final Request request;
103 private final JsonGenerator jsonGenerator;
104
105 RequestRunner(Context context, Request request, org.forgerock.http.protocol.Request httpRequest,
106 Response httpResponse) throws Exception {
107 this.context = context;
108 this.request = request;
109 this.httpRequest = httpRequest;
110 this.httpResponse = httpResponse;
111
112 this.protocolVersion = getRequestedProtocolVersion(httpRequest);
113 this.jsonGenerator = getJsonGenerator(httpRequest, httpResponse);
114 }
115
116
117
118
119
120
121
122 private boolean isUpsertSupported(final CreateRequest request) {
123
124 return (protocolVersion.getMajor() >= 2
125 && getIfNoneMatch(httpRequest) == null
126 && request.getNewResourceId() != null);
127 }
128
129 public final Promise<Response, NeverThrowsException> handleError(final ResourceException error) {
130 onError(error);
131 writeApiVersionHeaders(error);
132 writeAdvice();
133 return fail(httpRequest, httpResponse, error);
134 }
135
136 public final Promise<Response, NeverThrowsException> handleResult(final Connection result) {
137 connection = result;
138
139
140 return request.accept(this, null);
141 }
142
143
144
145
146 @Override
147 public final Promise<Response, NeverThrowsException> visitActionRequest(final Void p, final ActionRequest request) {
148 return connection.actionAsync(context, request)
149 .thenAsync(new AsyncFunction<ActionResponse, Response, NeverThrowsException>() {
150 @Override
151 public Promise<Response, NeverThrowsException> apply(ActionResponse result) {
152 try {
153 writeApiVersionHeaders(result);
154 writeCacheControlHeader(httpResponse);
155 writeAdvice();
156 if (result != null) {
157 writeContentTypeHeader(httpResponse);
158 Json.makeLocalizingObjectWriter(JSON_MAPPER, httpRequest)
159 .writeValue(jsonGenerator, result.getJsonContent().getObject());
160 } else {
161
162 httpResponse.setStatus(Status.NO_CONTENT);
163 }
164 onSuccess();
165 } catch (final Exception e) {
166 onError(e);
167 }
168
169 return newResultPromise(httpResponse);
170 }
171 }, new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
172 @Override
173 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
174 return handleError(e);
175 }
176 });
177 }
178
179
180
181
182 @Override
183 public final Promise<Response, NeverThrowsException> visitCreateRequest(final Void p, final CreateRequest request) {
184 return connection.createAsync(context, request)
185 .thenAsync(new AsyncFunction<ResourceResponse, Response, NeverThrowsException>() {
186 @Override
187 public Promise<Response, NeverThrowsException> apply(ResourceResponse result) {
188 try {
189 writeApiVersionHeaders(result);
190 writeAdvice();
191 if (result.getId() != null) {
192 httpResponse.getHeaders().put(HEADER_LOCATION, getResourceURL(result, request));
193 }
194 httpResponse.setStatus(Status.CREATED);
195 writeContentTypeHeader(httpResponse);
196 writeCacheControlHeader(httpResponse);
197 writeResource(result);
198 onSuccess();
199 } catch (final Exception e) {
200 onError(e);
201 }
202 return newResultPromise(httpResponse);
203 }
204 }, new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
205 @Override
206 public Promise<Response, NeverThrowsException> apply(ResourceException resourceException) {
207 try {
208
209
210 if ((resourceException instanceof PreconditionFailedException
211 || resourceException instanceof CreateNotSupportedException)
212 && isUpsertSupported(request)) {
213 return visitUpdateRequest(p,
214 newUpdateRequest(
215 request.getResourcePathObject().child(request.getNewResourceId()),
216 request.getContent()));
217 } else {
218 return handleError(resourceException);
219 }
220 } catch (Exception e) {
221 onError(e);
222 }
223 return newResultPromise(httpResponse);
224 }
225 });
226 }
227
228
229
230
231 @Override
232 public final Promise<Response, NeverThrowsException> visitDeleteRequest(final Void p, final DeleteRequest request) {
233 return connection.deleteAsync(context, request)
234 .thenAsync(newResourceSuccessHandler(),
235 new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
236 @Override
237 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
238 return handleError(e);
239 }
240 });
241 }
242
243
244
245
246 @Override
247 public final Promise<Response, NeverThrowsException> visitPatchRequest(final Void p, final PatchRequest request) {
248 return connection.patchAsync(context, request)
249 .thenAsync(newResourceSuccessHandler(),
250 new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
251 @Override
252 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
253 return handleError(e);
254 }
255 });
256 }
257
258
259
260
261 @Override
262 public final Promise<Response, NeverThrowsException> visitQueryRequest(final Void p, final QueryRequest request) {
263 final AtomicBoolean isFirstResult = new AtomicBoolean(true);
264 final AtomicInteger resultCount = new AtomicInteger(0);
265 return connection.queryAsync(context, request, new QueryResourceHandler() {
266 @Override
267 public boolean handleResource(final ResourceResponse resource) {
268 try {
269 writeHeader(resource, isFirstResult);
270 writeResourceJsonContent(resource);
271 resultCount.incrementAndGet();
272 return true;
273 } catch (final Exception e) {
274 handleError(adapt(e));
275 return false;
276 }
277 }
278 }).thenOnResult(new ResultHandler<QueryResponse>() {
279 @Override
280 public void handleResult(QueryResponse result) {
281 try {
282 writeHeader(result, isFirstResult);
283 jsonGenerator.writeEndArray();
284 jsonGenerator.writeNumberField(FIELD_RESULT_COUNT, resultCount.get());
285 jsonGenerator.writeStringField(FIELD_PAGED_RESULTS_COOKIE, result.getPagedResultsCookie());
286 jsonGenerator.writeStringField(FIELD_TOTAL_PAGED_RESULTS_POLICY,
287 result.getTotalPagedResultsPolicy().toString());
288 jsonGenerator.writeNumberField(FIELD_TOTAL_PAGED_RESULTS, result.getTotalPagedResults());
289
290 jsonGenerator.writeNumberField(FIELD_REMAINING_PAGED_RESULTS, result.getRemainingPagedResults());
291 jsonGenerator.writeEndObject();
292 onSuccess();
293 } catch (final Exception e) {
294 onError(e);
295 }
296 }
297 }).thenOnException(new ExceptionHandler<ResourceException>() {
298 @Override
299 public void handleException(ResourceException error) {
300 if (isFirstResult.get()) {
301 onError(error);
302 } else {
303
304 try {
305 jsonGenerator.writeEndArray();
306 jsonGenerator.writeNumberField(FIELD_RESULT_COUNT, resultCount.get());
307 jsonGenerator.writeObjectField(FIELD_ERROR, error.toJsonValue().getObject());
308 jsonGenerator.writeEndObject();
309 onSuccess();
310 } catch (final Exception e) {
311 onError(e);
312 }
313 }
314 }
315 }).thenAsync(new AsyncFunction<QueryResponse, Response, NeverThrowsException>() {
316 @Override
317 public Promise<Response, NeverThrowsException> apply(QueryResponse queryResponse) {
318 return newResultPromise(httpResponse);
319 }
320 }, new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
321 @Override
322 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
323 return handleError(e);
324 }
325 });
326 }
327
328 private void writeHeader(org.forgerock.json.resource.Response response, AtomicBoolean isFirstResult)
329 throws IOException {
330 if (isFirstResult.compareAndSet(true, false)) {
331 writeApiVersionHeaders(response);
332 writeAdvice();
333 writeContentTypeHeader(httpResponse);
334 writeCacheControlHeader(httpResponse);
335 jsonGenerator.writeStartObject();
336 jsonGenerator.writeArrayFieldStart(FIELD_RESULT);
337 }
338 }
339
340
341
342
343 @Override
344 public final Promise<Response, NeverThrowsException> visitReadRequest(final Void p, final ReadRequest request) {
345 return connection.readAsync(context, request)
346 .thenAsync(newResourceSuccessHandler(),
347 new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
348 @Override
349 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
350 return handleError(e);
351 }
352 });
353 }
354
355
356
357
358 @Override
359 public final Promise<Response, NeverThrowsException> visitUpdateRequest(final Void p, final UpdateRequest request) {
360 return connection.updateAsync(context, request)
361 .thenAsync(newResourceSuccessHandler(),
362 new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
363 @Override
364 public Promise<Response, NeverThrowsException> apply(ResourceException e) {
365 return handleError(e);
366 }
367 });
368 }
369
370 private void onSuccess() {
371 closeSilently(connection, jsonGenerator);
372 }
373
374 private void onError(final Exception e) {
375
376
377 closeSilently(connection);
378 }
379
380 private String getResourceURL(final ResourceResponse resource, final CreateRequest request) {
381
382 StringBuilder builder = new StringBuilder()
383 .append(httpRequest.getUri().getScheme())
384 .append("://")
385 .append(httpRequest.getUri().getRawAuthority());
386
387
388 String baseUri = context.asContext(UriRouterContext.class).getBaseUri();
389 if (!baseUri.isEmpty()) {
390 builder.append(addLeadingSlash(baseUri));
391 }
392
393
394 String resourcePath = request.getResourcePath();
395 if (!resourcePath.isEmpty()) {
396 builder.append(addLeadingSlash(resourcePath));
397 }
398
399
400 builder.append('/');
401 builder.append(resource.getId());
402
403 return builder.toString();
404 }
405
406 private AsyncFunction<ResourceResponse, Response, NeverThrowsException> newResourceSuccessHandler() {
407 return new AsyncFunction<ResourceResponse, Response, NeverThrowsException>() {
408 @Override
409 public Promise<Response, NeverThrowsException> apply(ResourceResponse result) {
410 try {
411 writeApiVersionHeaders(result);
412 writeAdvice();
413
414
415 if (request instanceof ReadRequest) {
416 final String rev = getIfNoneMatch(httpRequest);
417 if (rev != null && rev.equals(result.getRevision())) {
418
419 Map<String, Object> responseBody = newResourceException(304)
420 .setReason("Not Modified").toJsonValue().asMap();
421 return newResultPromise(new Response(Status.valueOf(304))
422 .setEntity(responseBody));
423 }
424 }
425 writeContentTypeHeader(httpResponse);
426 writeCacheControlHeader(httpResponse);
427 writeResource(result);
428 onSuccess();
429 } catch (final Exception e) {
430 onError(e);
431 }
432 return newResultPromise(httpResponse);
433 }
434 };
435 }
436
437 private void writeTextValue(final JsonValue json) throws IOException {
438 if (json.isMap() && !json.asMap().isEmpty()) {
439 writeToResponse(json.asMap().entrySet().iterator().next().getValue().toString().getBytes());
440 } else if (json.isList() && !json.asList().isEmpty()) {
441 writeToResponse(json.asList(String.class).iterator().next().getBytes());
442 } else if (json.isString()) {
443 writeToResponse(json.asString().getBytes());
444 } else if (json.isBoolean()) {
445 writeToResponse(json.asBoolean().toString().getBytes());
446 } else if (json.isNumber()) {
447 writeToResponse(json.asNumber().toString().getBytes());
448 } else {
449 throw new IOException("Content is unknown type or is empty");
450 }
451 }
452
453 private void writeBinaryValue(final JsonValue json) throws IOException {
454 if (json.isMap() && !json.asMap().isEmpty()) {
455 writeToResponse(Base64url.decode(json.asMap().entrySet().iterator().next().getValue().toString()));
456 } else if (json.isList() && !json.asList().isEmpty()) {
457 writeToResponse(Base64url.decode(json.asList(String.class).iterator().next()));
458 } else if (json.isString()) {
459 writeToResponse(Base64url.decode(json.asString()));
460 } else {
461 throw new IOException("Content is not an accepted type or is empty");
462 }
463 }
464
465 private void writeToResponse(byte[] data) throws IOException {
466 if (data == null || data.length == 0) {
467 throw new IOException("Content is empty or corrupt");
468 }
469 httpResponse.setEntity(data);
470 }
471
472 private void writeResource(final ResourceResponse resource)
473 throws IOException, ParseException, MalformedHeaderException {
474 if (resource.getRevision() != null) {
475 final StringBuilder builder = new StringBuilder();
476 builder.append('"');
477 builder.append(resource.getRevision());
478 builder.append('"');
479 httpResponse.getHeaders().put(HEADER_ETAG, builder.toString());
480 }
481
482 ContentType contentType = new ContentType(httpResponse.getHeaders().getFirst(ContentTypeHeader.class));
483
484 if (contentType.match(MIME_TYPE_APPLICATION_JSON)) {
485 writeResourceJsonContent(resource);
486 } else if (contentType.match(MIME_TYPE_TEXT_PLAIN)) {
487 writeTextValue(resource.getContent());
488 } else {
489 writeBinaryValue(resource.getContent());
490 }
491 }
492
493
494
495
496
497
498
499
500 private void writeResourceJsonContent(final ResourceResponse resource)
501 throws IOException, MalformedHeaderException {
502 ObjectWriter objectWriter = Json.makeLocalizingObjectWriter(JSON_MAPPER, httpRequest);
503 if (getRequestedProtocolVersion(httpRequest).getMajor() >= PROTOCOL_VERSION_2.getMajor()) {
504 jsonGenerator.writeStartObject();
505 final JsonValue content = resource.getContent();
506
507 if (resource.getId() != null) {
508 jsonGenerator.writeObjectField(FIELD_CONTENT_ID, resource.getId());
509 } else {
510
511
512 final Object id = content.get(FIELD_CONTENT_ID).getObject();
513 if (id != null) {
514 jsonGenerator.writeObjectField(FIELD_CONTENT_ID, id.toString());
515 }
516 }
517
518 if (resource.getRevision() != null) {
519 jsonGenerator.writeObjectField(FIELD_CONTENT_REVISION, resource.getRevision());
520 } else {
521
522
523 final Object rev = content.get(FIELD_CONTENT_REVISION).getObject();
524 if (rev != null) {
525 jsonGenerator.writeObjectField(FIELD_CONTENT_REVISION, rev.toString());
526 }
527 }
528
529 for (Map.Entry<String, Object> property : content.asMap().entrySet()) {
530 final String key = property.getKey();
531 if (!FIELD_CONTENT_ID.equals(key) && !FIELD_CONTENT_REVISION.equals(key)) {
532 jsonGenerator.writeFieldName(key);
533 objectWriter.writeValue(jsonGenerator, property.getValue());
534 }
535 }
536 jsonGenerator.writeEndObject();
537 } else {
538 objectWriter.writeValue(jsonGenerator, resource.getContent().getObject());
539 }
540 }
541
542 private void writeApiVersionHeaders(org.forgerock.json.resource.Response response) {
543 if (response.getResourceApiVersion() != null) {
544 httpResponse.getHeaders().put(
545 new ContentApiVersionHeader(protocolVersion, response.getResourceApiVersion()));
546 }
547 }
548
549 private void writeAdvice() {
550 if (context.containsContext(AdviceContext.class)) {
551 AdviceContext adviceContext = context.asContext(AdviceContext.class);
552 for (Map.Entry<String, List<String>> entry : adviceContext.getAdvices().entrySet()) {
553 httpResponse.getHeaders().put(entry.getKey(), entry.getValue());
554 }
555 }
556 }
557 }