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