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