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.util.concurrent.TimeUnit.MINUTES;
20 import static org.forgerock.api.commons.CommonsApi.COMMONS_API_DESCRIPTION;
21 import static org.forgerock.http.util.Paths.addLeadingSlash;
22 import static org.forgerock.http.util.Paths.removeTrailingSlash;
23 import static org.forgerock.json.resource.Applications.simpleCrestApplication;
24 import static org.forgerock.json.resource.Requests.newApiRequest;
25 import static org.forgerock.json.resource.ResourcePath.resourcePath;
26 import static org.forgerock.json.resource.http.HttpUtils.CONTENT_TYPE_REGEX;
27 import static org.forgerock.json.resource.http.HttpUtils.ETAG_ANY;
28 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_MATCH;
29 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_MODIFIED_SINCE;
30 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_NONE_MATCH;
31 import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_UNMODIFIED_SINCE;
32 import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_APPLICATION_JSON;
33 import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_MULTIPART_FORM_DATA;
34 import static org.forgerock.json.resource.http.HttpUtils.PARAM_ACTION;
35 import static org.forgerock.json.resource.http.HttpUtils.PARAM_FIELDS;
36 import static org.forgerock.json.resource.http.HttpUtils.PARAM_MIME_TYPE;
37 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_COOKIE;
38 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_OFFSET;
39 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGE_SIZE;
40 import static org.forgerock.json.resource.http.HttpUtils.PARAM_PRETTY_PRINT;
41 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_EXPRESSION;
42 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_FILTER;
43 import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_ID;
44 import static org.forgerock.json.resource.http.HttpUtils.PARAM_SORT_KEYS;
45 import static org.forgerock.json.resource.http.HttpUtils.PARAM_TOTAL_PAGED_RESULTS_POLICY;
46 import static org.forgerock.json.resource.http.HttpUtils.PROTOCOL_VERSION_1;
47 import static org.forgerock.json.resource.http.HttpUtils.RESTRICTED_HEADER_NAMES;
48 import static org.forgerock.json.resource.http.HttpUtils.SORT_KEYS_DELIMITER;
49 import static org.forgerock.json.resource.http.HttpUtils.asBooleanValue;
50 import static org.forgerock.json.resource.http.HttpUtils.asIntValue;
51 import static org.forgerock.json.resource.http.HttpUtils.asSingleValue;
52 import static org.forgerock.json.resource.http.HttpUtils.determineRequestType;
53 import static org.forgerock.json.resource.http.HttpUtils.fail;
54 import static org.forgerock.json.resource.http.HttpUtils.getIfMatch;
55 import static org.forgerock.json.resource.http.HttpUtils.getIfNoneMatch;
56 import static org.forgerock.json.resource.http.HttpUtils.getJsonActionContent;
57 import static org.forgerock.json.resource.http.HttpUtils.getJsonContent;
58 import static org.forgerock.json.resource.http.HttpUtils.getJsonPatchContent;
59 import static org.forgerock.json.resource.http.HttpUtils.getMethod;
60 import static org.forgerock.json.resource.http.HttpUtils.getParameter;
61 import static org.forgerock.json.resource.http.HttpUtils.getRequestedResourceVersion;
62 import static org.forgerock.json.resource.http.HttpUtils.rejectIfMatch;
63 import static org.forgerock.json.resource.http.HttpUtils.rejectIfNoneMatch;
64 import static org.forgerock.json.resource.http.HttpUtils.staticContextFactory;
65 import static org.forgerock.util.Reject.checkNotNull;
66 import static org.forgerock.util.promise.Promises.newResultPromise;
67 import static org.wrensecurity.guava.common.base.Optional.absent;
68 import static org.wrensecurity.guava.common.base.Strings.isNullOrEmpty;
69
70 import com.fasterxml.jackson.databind.ObjectMapper;
71 import com.fasterxml.jackson.databind.ObjectWriter;
72 import io.swagger.v3.oas.models.OpenAPI;
73 import io.swagger.v3.oas.models.PathItem;
74 import io.swagger.v3.oas.models.Paths;
75 import java.util.ArrayList;
76 import java.util.Collections;
77 import java.util.List;
78 import java.util.Map;
79 import java.util.concurrent.CopyOnWriteArrayList;
80 import java.util.concurrent.ExecutionException;
81 import org.forgerock.api.CrestApiProducer;
82 import org.forgerock.api.jackson.PathsModule;
83 import org.forgerock.api.models.ApiDescription;
84 import org.forgerock.api.transform.OpenApiTransformer;
85 import org.forgerock.http.ApiProducer;
86 import org.forgerock.http.Handler;
87 import org.forgerock.http.header.AcceptLanguageHeader;
88 import org.forgerock.http.header.ContentTypeHeader;
89 import org.forgerock.http.protocol.Form;
90 import org.forgerock.http.protocol.Response;
91 import org.forgerock.http.protocol.Status;
92 import org.forgerock.http.routing.UriRouterContext;
93 import org.forgerock.http.routing.Version;
94 import org.forgerock.http.util.Json;
95 import org.forgerock.http.util.Uris;
96 import org.forgerock.json.JsonValue;
97 import org.forgerock.json.resource.ActionRequest;
98 import org.forgerock.json.resource.AdviceContext;
99 import org.forgerock.json.resource.BadRequestException;
100 import org.forgerock.json.resource.ConflictException;
101 import org.forgerock.json.resource.Connection;
102 import org.forgerock.json.resource.ConnectionFactory;
103 import org.forgerock.json.resource.CountPolicy;
104 import org.forgerock.json.resource.CreateRequest;
105 import org.forgerock.json.resource.CrestApplication;
106 import org.forgerock.json.resource.DeleteRequest;
107 import org.forgerock.json.resource.NotSupportedException;
108 import org.forgerock.json.resource.PatchRequest;
109 import org.forgerock.json.resource.PreconditionFailedException;
110 import org.forgerock.json.resource.QueryFilters;
111 import org.forgerock.json.resource.QueryRequest;
112 import org.forgerock.json.resource.ReadRequest;
113 import org.forgerock.json.resource.Request;
114 import org.forgerock.json.resource.RequestType;
115 import org.forgerock.json.resource.Requests;
116 import org.forgerock.json.resource.ResourceException;
117 import org.forgerock.json.resource.ResourcePath;
118 import org.forgerock.json.resource.UpdateRequest;
119 import org.forgerock.services.context.Context;
120 import org.forgerock.services.context.RootContext;
121 import org.forgerock.services.descriptor.Describable;
122 import org.forgerock.util.AsyncFunction;
123 import org.forgerock.util.i18n.PreferredLocales;
124 import org.forgerock.util.promise.NeverThrowsException;
125 import org.forgerock.util.promise.Promise;
126 import org.slf4j.Logger;
127 import org.slf4j.LoggerFactory;
128 import org.wrensecurity.guava.common.base.Optional;
129 import org.wrensecurity.guava.common.cache.CacheBuilder;
130 import org.wrensecurity.guava.common.cache.CacheLoader;
131 import org.wrensecurity.guava.common.cache.LoadingCache;
132 import org.wrensecurity.guava.common.util.concurrent.UncheckedExecutionException;
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166 final class HttpAdapter implements Handler, Describable<OpenAPI, org.forgerock.http.protocol.Request>,
167 Describable.Listener {
168
169 private static final Logger logger = LoggerFactory.getLogger(HttpAdapter.class);
170 private static final ObjectMapper API_OBJECT_MAPPER = new ObjectMapper().registerModules(
171 new Json.LocalizableStringModule(),
172 new Json.JsonValueModule(),
173 new PathsModule());
174
175 private final ConnectionFactory connectionFactory;
176 private final HttpContextFactory contextFactory;
177 private final String apiId;
178 private final String apiVersion;
179 private final List<Describable.Listener> apiListeners = new CopyOnWriteArrayList<>();
180 private ApiProducer<OpenAPI> apiProducer;
181 private LoadingCache<String, Optional<OpenAPI>> descriptorCache;
182
183
184
185
186
187
188
189
190
191 @Deprecated
192 public HttpAdapter(ConnectionFactory connectionFactory) {
193 this(connectionFactory, (HttpContextFactory) null);
194 }
195
196
197
198
199
200
201
202
203
204
205
206
207 @SuppressWarnings("deprecation")
208 @Deprecated
209 public HttpAdapter(ConnectionFactory connectionFactory, final Context parentContext) {
210 this(connectionFactory, staticContextFactory(parentContext));
211 }
212
213
214
215
216
217
218
219
220
221
222
223
224
225 @SuppressWarnings("deprecation")
226 @Deprecated
227 public HttpAdapter(ConnectionFactory connectionFactory, HttpContextFactory contextFactory) {
228 this(simpleCrestApplication(connectionFactory, null, null), contextFactory);
229 }
230
231
232
233
234
235
236
237
238
239
240
241
242 @SuppressWarnings("deprecation")
243 public HttpAdapter(CrestApplication application, HttpContextFactory contextFactory) {
244 this.contextFactory = contextFactory != null ? contextFactory : SecurityContextFactory
245 .getHttpServletContextFactory();
246 this.connectionFactory = checkNotNull(application.getConnectionFactory());
247 this.apiId = application.getApiId();
248 this.apiVersion = application.getApiVersion();
249
250 try {
251 Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
252 if (describable.isPresent()) {
253 describable.get().addDescriptorListener(this);
254 }
255 } catch (ResourceException e) {
256 logger.warn("Could not create connection", e);
257 }
258
259 }
260
261
262
263
264
265
266
267
268 @Override
269 public Promise<Response, NeverThrowsException> handle(Context context,
270 org.forgerock.http.protocol.Request request) {
271 try {
272 RequestType requestType = determineRequestType(request);
273 switch (requestType) {
274 case CREATE:
275 return doCreate(context, request);
276 case READ:
277 return doRead(context, request);
278 case UPDATE:
279 return doUpdate(context, request);
280 case DELETE:
281 return doDelete(context, request);
282 case PATCH:
283 return doPatch(context, request);
284 case ACTION:
285 return doAction(context, request);
286 case QUERY:
287 return doQuery(context, request);
288 case API:
289 return doApiRequest(context, request);
290 default:
291 throw new NotSupportedException("Operation " + requestType + " not supported");
292 }
293 } catch (ResourceException e) {
294 return fail(request, e);
295 }
296 }
297
298 Promise<Response, NeverThrowsException> doDelete(Context context, org.forgerock.http.protocol.Request req) {
299 try {
300 Version requestedResourceVersion = getRequestedResourceVersion(req);
301
302
303 preprocessRequest(req);
304 rejectIfNoneMatch(req);
305
306
307 final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
308 final Form parameters = req.getForm();
309 final DeleteRequest request =
310 Requests.newDeleteRequest(getResourcePath(context, req))
311 .setRevision(ifMatchRevision)
312 .setResourceVersion(requestedResourceVersion);
313 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
314 final String name = p.getKey();
315 final List<String> values = p.getValue();
316 if (parseCommonParameter(name, values, request)) {
317 continue;
318 } else {
319 request.setAdditionalParameter(name, asSingleValue(name, values));
320 }
321 }
322 return doRequest(context, req, request);
323 } catch (final Exception e) {
324 return fail(req, e);
325 }
326 }
327
328 Promise<Response, NeverThrowsException> doRead(Context context, org.forgerock.http.protocol.Request req) {
329 try {
330 Version requestedResourceVersion = getRequestedResourceVersion(req);
331
332
333 preprocessRequest(req);
334 rejectIfMatch(req);
335
336 final Form parameters = req.getForm();
337
338 final String rev = getIfNoneMatch(req);
339 if (ETAG_ANY.equals(rev)) {
340
341 throw new PreconditionFailedException("If-None-Match * not appropriate for "
342 + getMethod(req) + " requests");
343 }
344
345 final ReadRequest request = Requests.newReadRequest(getResourcePath(context, req))
346 .setResourceVersion(requestedResourceVersion);
347 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
348 final String name = p.getKey();
349 final List<String> values = p.getValue();
350 if (parseCommonParameter(name, values, request)) {
351 continue;
352 } else if (PARAM_MIME_TYPE.equalsIgnoreCase(name)) {
353
354 } else {
355 request.setAdditionalParameter(name, asSingleValue(name, values));
356 }
357 }
358 return doRequest(context, req, request);
359 } catch (final Exception e) {
360 return fail(req, e);
361 }
362 }
363
364 Promise<Response, NeverThrowsException> doQuery(Context context, org.forgerock.http.protocol.Request req) {
365 try {
366 Version requestedResourceVersion = getRequestedResourceVersion(req);
367
368
369 preprocessRequest(req);
370 rejectIfMatch(req);
371
372 final Form parameters = req.getForm();
373
374 rejectIfNoneMatch(req);
375
376
377 final QueryRequest request = Requests.newQueryRequest(getResourcePath(context, req))
378 .setResourceVersion(requestedResourceVersion);
379
380 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
381 final String name = p.getKey();
382 final List<String> values = p.getValue();
383
384 if (parseCommonParameter(name, values, request)) {
385 continue;
386 } else if (name.equalsIgnoreCase(PARAM_SORT_KEYS)) {
387 for (final String s : values) {
388 try {
389 request.addSortKey(s.split(SORT_KEYS_DELIMITER));
390 } catch (final IllegalArgumentException e) {
391
392 throw new BadRequestException("The value '" + s
393 + "' for parameter '" + name
394 + "' could not be parsed as a comma "
395 + "separated list of sort keys");
396 }
397 }
398 } else if (name.equalsIgnoreCase(PARAM_QUERY_ID)) {
399 request.setQueryId(asSingleValue(name, values));
400 } else if (name.equalsIgnoreCase(PARAM_QUERY_EXPRESSION)) {
401 request.setQueryExpression(asSingleValue(name, values));
402 } else if (name.equalsIgnoreCase(PARAM_PAGED_RESULTS_COOKIE)) {
403 request.setPagedResultsCookie(asSingleValue(name, values));
404 } else if (name.equalsIgnoreCase(PARAM_PAGED_RESULTS_OFFSET)) {
405 request.setPagedResultsOffset(asIntValue(name, values));
406 } else if (name.equalsIgnoreCase(PARAM_PAGE_SIZE)) {
407 request.setPageSize(asIntValue(name, values));
408 } else if (name.equalsIgnoreCase(PARAM_QUERY_FILTER)) {
409 final String s = asSingleValue(name, values);
410 try {
411 request.setQueryFilter(QueryFilters.parse(s));
412 } catch (final IllegalArgumentException e) {
413
414 throw new BadRequestException("The value '" + s + "' for parameter '"
415 + name + "' could not be parsed as a valid query filter");
416 }
417 } else if (name.equalsIgnoreCase(PARAM_TOTAL_PAGED_RESULTS_POLICY)) {
418 final String policy = asSingleValue(name, values);
419
420 try {
421 request.setTotalPagedResultsPolicy(CountPolicy.valueOf(policy.toUpperCase()));
422 } catch (IllegalArgumentException e) {
423
424 throw new BadRequestException("The value '" + policy + "' for parameter '"
425 + name + "' could not be parsed as a valid count policy");
426 }
427 } else {
428 request.setAdditionalParameter(name, asSingleValue(name, values));
429 }
430 }
431
432
433 if (request.getQueryId() != null && request.getQueryFilter() != null) {
434
435 throw new BadRequestException("The parameters " + PARAM_QUERY_ID + " and "
436 + PARAM_QUERY_FILTER + " are mutually exclusive");
437 }
438
439 if (request.getQueryId() != null && request.getQueryExpression() != null) {
440
441 throw new BadRequestException("The parameters " + PARAM_QUERY_ID + " and "
442 + PARAM_QUERY_EXPRESSION + " are mutually exclusive");
443 }
444
445 if (request.getQueryFilter() != null && request.getQueryExpression() != null) {
446
447 throw new BadRequestException("The parameters " + PARAM_QUERY_FILTER + " and "
448 + PARAM_QUERY_EXPRESSION + " are mutually exclusive");
449 }
450
451 if (request.getPagedResultsOffset() > 0 && request.getPagedResultsCookie() != null) {
452
453 throw new BadRequestException("The parameters " + PARAM_PAGED_RESULTS_OFFSET + " and "
454 + PARAM_PAGED_RESULTS_COOKIE + " are mutually exclusive");
455 }
456
457 return doRequest(context, req, request);
458 } catch (final Exception e) {
459 return fail(req, e);
460 }
461 }
462
463 Promise<Response, NeverThrowsException> doPatch(Context context, org.forgerock.http.protocol.Request req) {
464 try {
465 Version requestedResourceVersion = getRequestedResourceVersion(req);
466
467
468 preprocessRequest(req);
469 if (req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
470
471 throw new PreconditionFailedException(
472 "Use of If-None-Match not supported for PATCH requests");
473 }
474
475
476 final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
477 final Form parameters = req.getForm();
478 final PatchRequest request =
479 Requests.newPatchRequest(getResourcePath(context, req))
480 .setRevision(ifMatchRevision)
481 .setResourceVersion(requestedResourceVersion);
482 request.getPatchOperations().addAll(getJsonPatchContent(req));
483 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
484 final String name = p.getKey();
485 final List<String> values = p.getValue();
486 if (parseCommonParameter(name, values, request)) {
487 continue;
488 } else {
489 request.setAdditionalParameter(name, asSingleValue(name, values));
490 }
491 }
492 return doRequest(context, req, request);
493 } catch (final Exception e) {
494 return fail(req, e);
495 }
496 }
497
498 Promise<Response, NeverThrowsException> doCreate(Context context, org.forgerock.http.protocol.Request req) {
499 try {
500 Version requestedResourceVersion = getRequestedResourceVersion(req);
501
502
503 preprocessRequest(req);
504
505 if ("POST".equals(getMethod(req))) {
506
507 rejectIfNoneMatch(req);
508 rejectIfMatch(req);
509
510 final Form parameters = req.getForm();
511 final JsonValue content = getJsonContent(req);
512 final CreateRequest request =
513 Requests.newCreateRequest(getResourcePath(context, req), content)
514 .setResourceVersion(requestedResourceVersion);
515 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
516 final String name = p.getKey();
517 final List<String> values = p.getValue();
518 if (parseCommonParameter(name, values, request)) {
519 continue;
520 } else if (name.equalsIgnoreCase(PARAM_ACTION)) {
521
522 } else {
523 request.setAdditionalParameter(name, asSingleValue(name, values));
524 }
525 }
526 return doRequest(context, req, request);
527 } else {
528
529 if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null
530 && req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
531
532 throw new PreconditionFailedException(
533 "Simultaneous use of If-Match and If-None-Match not supported for PUT requests");
534 }
535
536 final Form parameters = req.getForm();
537 final JsonValue content = getJsonContent(req);
538
539
540
541 final ResourcePath resourcePath = getResourcePath(context, req);
542 if (resourcePath.isEmpty()) {
543
544 throw new BadRequestException("No new resource ID in HTTP PUT request");
545 }
546
547
548 final CreateRequest request =
549 Requests.newCreateRequest(resourcePath.parent(), content)
550 .setNewResourceId(resourcePath.leaf())
551 .setResourceVersion(requestedResourceVersion);
552 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
553 final String name = p.getKey();
554 final List<String> values = p.getValue();
555 if (parseCommonParameter(name, values, request)) {
556 continue;
557 } else {
558 request.setAdditionalParameter(name, asSingleValue(name, values));
559 }
560 }
561 return doRequest(context, req, request);
562 }
563 } catch (final Exception e) {
564 return fail(req, e);
565 }
566 }
567
568 Promise<Response, NeverThrowsException> doAction(Context context, org.forgerock.http.protocol.Request req) {
569 try {
570 Version requestedResourceVersion = getRequestedResourceVersion(req);
571
572
573 preprocessRequest(req);
574 rejectIfNoneMatch(req);
575 rejectIfMatch(req);
576
577 final Form parameters = req.getForm();
578 final String action = asSingleValue(PARAM_ACTION, getParameter(req, PARAM_ACTION));
579
580 final JsonValue content = getJsonActionContent(req);
581 final ActionRequest request =
582 Requests.newActionRequest(getResourcePath(context, req), action)
583 .setContent(content)
584 .setResourceVersion(requestedResourceVersion);
585 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
586 final String name = p.getKey();
587 final List<String> values = p.getValue();
588 if (parseCommonParameter(name, values, request)) {
589 continue;
590 } else if (name.equalsIgnoreCase(PARAM_ACTION)) {
591
592 } else {
593 request.setAdditionalParameter(name, asSingleValue(name, values));
594 }
595 }
596 return doRequest(context, req, request);
597 } catch (final Exception e) {
598 return fail(req, e);
599 }
600 }
601
602 Promise<Response, NeverThrowsException> doUpdate(Context context, org.forgerock.http.protocol.Request req) {
603 try {
604 Version requestedResourceVersion = getRequestedResourceVersion(req);
605
606
607 preprocessRequest(req);
608
609 if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null
610 && req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
611
612 throw new PreconditionFailedException(
613 "Simultaneous use of If-Match and If-None-Match not supported for PUT requests");
614 }
615
616
617 final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
618 final Form parameters = req.getForm();
619 final JsonValue content = getJsonContent(req);
620
621 final UpdateRequest request =
622 Requests.newUpdateRequest(getResourcePath(context, req), content)
623 .setRevision(ifMatchRevision)
624 .setResourceVersion(requestedResourceVersion);
625 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
626 final String name = p.getKey();
627 final List<String> values = p.getValue();
628 if (parseCommonParameter(name, values, request)) {
629 continue;
630 } else {
631 request.setAdditionalParameter(name, asSingleValue(name, values));
632 }
633 }
634 return doRequest(context, req, request);
635 } catch (final Exception e) {
636 return fail(req, e);
637 }
638 }
639
640 private Promise<Response, NeverThrowsException> doApiRequest(Context context,
641 final org.forgerock.http.protocol.Request req) {
642 try {
643 Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
644 if (!describable.isPresent()) {
645 throw new NotSupportedException();
646 }
647 Request request = newApiRequest(getResourcePath(context, req));
648 context = prepareRequest(context, req, request);
649 ApiDescription api = describable.get().handleApiRequest(context, request);
650
651 ObjectWriter writer = Json.makeLocalizingObjectWriter(API_OBJECT_MAPPER, request.getPreferredLocales());
652
653
654 final List<String> values = getParameter(req, PARAM_PRETTY_PRINT);
655 if (values != null) {
656 if (asBooleanValue(PARAM_PRETTY_PRINT, values)) {
657 writer = writer.withDefaultPrettyPrinter();
658 }
659 }
660
661 return newResultPromise(new Response(Status.OK).setEntity(writer.writeValueAsBytes(api)));
662 } catch (Exception e) {
663 return fail(req, e);
664 }
665 }
666
667 @SuppressWarnings("unchecked")
668 private Optional<Describable<ApiDescription, Request>> getDescribableConnection()
669 throws ResourceException {
670 if (apiId == null || apiVersion == null) {
671 logger.info("CREST API Descriptor API ID and Version are not set. Not describing.");
672 return absent();
673 }
674 Connection connection = connectionFactory.getConnection();
675 if (connection instanceof Describable) {
676 return Optional.of((Describable<ApiDescription, Request>) connection);
677 } else {
678 return absent();
679 }
680 }
681
682 private Promise<Response, NeverThrowsException> doRequest(Context context, org.forgerock.http.protocol.Request req,
683 Request request) throws Exception {
684 Context ctx = prepareRequest(context, req, request);
685 final RequestRunner runner = new RequestRunner(ctx, request, req, new Response(Status.OK));
686 return connectionFactory.getConnectionAsync()
687 .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() {
688 @Override
689 public Promise<Response, NeverThrowsException> apply(Connection connection) {
690 return runner.handleResult(connection);
691 }
692 }, new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
693 @Override
694 public Promise<Response, NeverThrowsException> apply(ResourceException error) {
695 return runner.handleError(error);
696 }
697 });
698 }
699
700 private Context prepareRequest(Context context, org.forgerock.http.protocol.Request req, Request request)
701 throws ResourceException, org.forgerock.http.header.MalformedHeaderException {
702 Context ctx = newRequestContext(context, req);
703 final AcceptLanguageHeader acceptLanguageHeader = req.getHeaders().get(AcceptLanguageHeader.class);
704 request.setPreferredLocales(acceptLanguageHeader != null
705 ? acceptLanguageHeader.getLocales()
706 : new PreferredLocales(null));
707 return ctx;
708 }
709
710
711
712
713 private ResourcePath getResourcePath(Context context, org.forgerock.http.protocol.Request req)
714 throws ResourceException {
715 try {
716 if (context.containsContext(UriRouterContext.class)) {
717 ResourcePath reqPath = ResourcePath.valueOf(req.getUri().getRawPath());
718 return reqPath.subSequence(getMatchedUri(context).size(), reqPath.size());
719 } else {
720 return ResourcePath.valueOf(req.getUri().getRawPath());
721 }
722 } catch (IllegalArgumentException e) {
723 throw new BadRequestException(e.getMessage());
724 }
725 }
726
727 private ResourcePath getMatchedUri(Context context) {
728 List<ResourcePath> matched = new ArrayList<>();
729 Context ctx = context;
730 while (ctx.containsContext(UriRouterContext.class)) {
731 UriRouterContext uriRouterContext = ctx.asContext(UriRouterContext.class);
732 matched.add(ResourcePath.valueOf(uriRouterContext.getMatchedUri()));
733 ctx = uriRouterContext.getParent();
734 }
735 Collections.reverse(matched);
736 ResourcePath matchedUri = new ResourcePath();
737 for (ResourcePath resourcePath : matched) {
738 matchedUri = matchedUri.concat(resourcePath);
739 }
740 return matchedUri;
741 }
742
743 private Context newRequestContext(Context context, org.forgerock.http.protocol.Request req)
744 throws ResourceException {
745 final Context parent = contextFactory.createContext(context, req);
746 return new AdviceContext(new HttpContext(parent, req), RESTRICTED_HEADER_NAMES);
747 }
748
749 private boolean parseCommonParameter(final String name, final List<String> values,
750 final Request request) throws ResourceException {
751 if (name.equalsIgnoreCase(PARAM_FIELDS)) {
752 for (final String s : values) {
753 try {
754 request.addField(s.split(","));
755 } catch (final IllegalArgumentException e) {
756
757 throw new BadRequestException("The value '" + s + "' for parameter '" + name
758 + "' could not be parsed as a comma separated list of JSON pointers");
759 }
760 }
761 return true;
762 } else if (name.equalsIgnoreCase(PARAM_PRETTY_PRINT)) {
763
764 asBooleanValue(name, values);
765 return true;
766 } else {
767
768 return false;
769 }
770 }
771
772 private void preprocessRequest(org.forgerock.http.protocol.Request req) throws ResourceException {
773
774
775
776 final String contentType = ContentTypeHeader.valueOf(req).getType();
777 if (!req.getMethod().equalsIgnoreCase(HttpUtils.METHOD_GET)
778 && contentType != null
779 && !CONTENT_TYPE_REGEX.matcher(contentType).matches()
780 && !HttpUtils.isMultiPartRequest(contentType)) {
781
782 throw new BadRequestException(
783 "The request could not be processed because it specified the content-type '"
784 + contentType + "' when only the content-type '"
785 + MIME_TYPE_APPLICATION_JSON + "' and '"
786 + MIME_TYPE_MULTIPART_FORM_DATA + "' are supported");
787 }
788
789 if (req.getHeaders().getFirst(HEADER_IF_MODIFIED_SINCE) != null) {
790
791 throw new ConflictException("Header If-Modified-Since not supported");
792 }
793
794 if (req.getHeaders().getFirst(HEADER_IF_UNMODIFIED_SINCE) != null) {
795
796 throw new ConflictException("Header If-Unmodified-Since not supported");
797 }
798 }
799
800 @Override
801 public OpenAPI api(ApiProducer<OpenAPI> producer) {
802 this.apiProducer = producer;
803 return updateDescriptor();
804 }
805
806 private OpenAPI updateDescriptor() {
807 if (apiProducer == null) {
808
809 return null;
810 }
811 try {
812 Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
813 if (describable.isPresent()) {
814 ApiDescription api = describable.get().api(new CrestApiProducer(apiId, apiVersion));
815 if (api != null) {
816 this.descriptorCache = CacheBuilder.newBuilder().expireAfterAccess(30, MINUTES)
817 .build(new CacheLoader<String, Optional<OpenAPI>>() {
818 @Override
819 public Optional<OpenAPI> load(String uri) throws ResourceException {
820 UriRouterContext context = new UriRouterContext(new RootContext(), "", uri,
821 Collections.<String, String>emptyMap());
822 ApiDescription api = getDescribableConnection().get()
823 .handleApiRequest(context, newApiRequest(resourcePath(uri)));
824
825 if (api == null) {
826 return absent();
827 }
828 OpenAPI openApi = OpenApiTransformer.execute(api, COMMONS_API_DESCRIPTION);
829 uri = removeTrailingSlash(uri);
830 if (!isNullOrEmpty(uri)) {
831 uri = addLeadingSlash(Uris.urlDecodePathElement(uri));
832 }
833 Paths paths = new Paths();
834 if (openApi.getPaths() != null) {
835 for (Map.Entry<String, PathItem> path : openApi.getPaths().entrySet()) {
836 String pathString = path.getKey();
837
838
839 if ((pathString.startsWith("/#") || pathString.equals("/"))
840 && !uri.isEmpty()) {
841 pathString = pathString.substring(1);
842 }
843 paths.addPathItem(uri + pathString, path.getValue());
844 }
845 }
846 openApi.setPaths(paths);
847 return Optional.of(apiProducer.addApiInfo(openApi));
848 }
849 });
850 try {
851 return descriptorCache.get("").orNull();
852 } catch (ExecutionException e) {
853 throw (ResourceException) e.getCause();
854 }
855 }
856 }
857 } catch (ResourceException e) {
858 throw new IllegalStateException("Cannot get connection", e);
859 }
860 return null;
861 }
862
863 @Override
864 public OpenAPI handleApiRequest(Context context, org.forgerock.http.protocol.Request request) {
865 if (descriptorCache == null) {
866 return null;
867 }
868 Optional<OpenAPI> result;
869 try {
870 if (context.containsContext(UriRouterContext.class)) {
871 result = descriptorCache.get(context.asContext(UriRouterContext.class).getRemainingUri());
872 } else {
873 result = descriptorCache.get("");
874 }
875 } catch (ExecutionException e) {
876 throw new UnsupportedOperationException("Cannot get connection", e);
877 } catch (UncheckedExecutionException e) {
878 throw (RuntimeException) e.getCause();
879 }
880 return result.orNull();
881 }
882
883 @Override
884 public void addDescriptorListener(Listener listener) {
885 apiListeners.add(listener);
886 }
887
888 @Override
889 public void removeDescriptorListener(Listener listener) {
890 apiListeners.remove(listener);
891 }
892
893 @Override
894 public void notifyDescriptorChange() {
895 updateDescriptor();
896 for (Listener listener : apiListeners) {
897 listener.notifyDescriptorChange();
898 }
899 }
900 }