1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.forgerock.api.markup;
19
20 import static java.util.Collections.unmodifiableList;
21 import static org.forgerock.api.markup.asciidoc.AsciiDoc.asciiDoc;
22 import static org.forgerock.api.markup.asciidoc.AsciiDoc.normalizeName;
23 import static org.forgerock.api.markup.asciidoc.AsciiDocTable.COLUMN_WIDTH_MEDIUM;
24 import static org.forgerock.api.markup.asciidoc.AsciiDocTable.COLUMN_WIDTH_SMALL;
25 import static org.forgerock.api.markup.asciidoc.AsciiDocTableColumnStyles.ASCII_DOC_CELL;
26 import static org.forgerock.api.markup.asciidoc.AsciiDocTableColumnStyles.MONO_CELL;
27 import static org.forgerock.api.util.PathUtil.buildPath;
28 import static org.forgerock.api.util.PathUtil.buildPathParameters;
29 import static org.forgerock.api.util.PathUtil.mergeParameters;
30 import static org.forgerock.api.util.ValidationUtil.isEmpty;
31 import static org.forgerock.util.Reject.checkNotNull;
32
33 import java.io.IOException;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.nio.file.StandardCopyOption;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.LinkedHashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47
48 import org.forgerock.api.enums.CountPolicy;
49 import org.forgerock.api.enums.CreateMode;
50 import org.forgerock.api.enums.PagingMode;
51 import org.forgerock.api.enums.PatchOperation;
52 import org.forgerock.api.enums.Stability;
53 import org.forgerock.api.jackson.JacksonUtils;
54 import org.forgerock.api.markup.asciidoc.AsciiDoc;
55 import org.forgerock.api.markup.asciidoc.AsciiDocTable;
56 import org.forgerock.api.models.Action;
57 import org.forgerock.api.models.ApiDescription;
58 import org.forgerock.api.models.ApiError;
59 import org.forgerock.api.models.Create;
60 import org.forgerock.api.models.Items;
61 import org.forgerock.api.models.Operation;
62 import org.forgerock.api.models.Parameter;
63 import org.forgerock.api.models.Patch;
64 import org.forgerock.api.models.Paths;
65 import org.forgerock.api.models.Query;
66 import org.forgerock.api.models.Reference;
67 import org.forgerock.api.models.Resource;
68 import org.forgerock.api.models.Schema;
69 import org.forgerock.api.models.Services;
70 import org.forgerock.api.models.SubResources;
71 import org.forgerock.api.models.VersionedPath;
72 import org.forgerock.api.util.ReferenceResolver;
73 import org.forgerock.api.util.ValidationUtil;
74 import org.forgerock.http.routing.Version;
75 import org.forgerock.util.i18n.LocalizableString;
76 import org.forgerock.util.i18n.PreferredLocales;
77
78 import com.fasterxml.jackson.databind.ObjectMapper;
79
80
81
82
83 public final class ApiDocGenerator {
84
85 private static final ObjectMapper OBJECT_MAPPER = JacksonUtils.createGenericMapper();
86
87 private static final PreferredLocales PREFERRED_LOCALES = new PreferredLocales();
88
89 enum CrestMethod {
90 CREATE, READ, UPDATE, DELETE, PATCH, ACTION, QUERY
91 }
92
93
94
95
96
97
98
99
100
101 private static final Pattern SERVICE_NAME_PATTERN = Pattern.compile("^([^:]+)[:](\\d(?:\\.\\d)?)$");
102
103
104
105
106 private static final String ADOC_EXTENSION = ".adoc";
107
108
109
110
111
112 private final boolean inMemoryMode;
113
114
115
116
117 private final Path outputDirPath;
118
119
120
121
122 private final Path inputDirPath;
123
124
125
126
127 private final Map<String, Map<Version, String>> pathTree;
128
129
130
131
132 private final Map<String, String> adocMap;
133
134
135
136
137
138 private final Set<Resource> referencedServices;
139
140 private final ApiDescription apiDescription;
141
142 private final ReferenceResolver referenceResolver;
143
144
145
146
147
148
149
150
151 private ApiDocGenerator(final ApiDescription apiDescription, final Path inputDirPath, final Path outputDirPath,
152 final ApiDescription... externalApiDescriptions) {
153
154 pathTree = new HashMap<>();
155 adocMap = new HashMap<>();
156 referencedServices = new HashSet<>();
157 this.apiDescription = checkNotNull(apiDescription, "apiDescription required");
158 this.inputDirPath = inputDirPath;
159 this.outputDirPath = outputDirPath;
160 inMemoryMode = outputDirPath == null;
161
162 if (outputDirPath != null && outputDirPath.equals(inputDirPath)) {
163 throw new ApiDocGeneratorException("inputDirPath and outputDirPath can not be equal");
164 }
165
166 referenceResolver = new ReferenceResolver(apiDescription);
167 referenceResolver.registerAll(externalApiDescriptions);
168 }
169
170
171
172
173
174
175
176
177
178
179 public static void execute(final String title, final ApiDescription apiDescription,
180 final Path inputDirPath, final Path outputDirPath, final ApiDescription... externalApiDescriptions) {
181
182 final ApiDocGenerator thisInstance = new ApiDocGenerator(apiDescription, inputDirPath, outputDirPath,
183 externalApiDescriptions);
184 thisInstance.doExecute(title);
185 }
186
187
188
189
190
191
192
193
194
195
196 public static String execute(final String title, final ApiDescription apiDescription,
197 final Path inputDirPath, final ApiDescription... externalApiDescriptions) {
198
199 final ApiDocGenerator thisInstance = new ApiDocGenerator(apiDescription, inputDirPath, null,
200 externalApiDescriptions);
201 final String rootFilename = thisInstance.doExecute(title);
202 return thisInstance.toString(rootFilename);
203 }
204
205 private String doExecute(final String title) {
206 final String namespace = apiDescription.getId();
207 try {
208 final String pathsFilename = outputPaths(namespace);
209 return outputRoot(checkNotNull(title, "title is required"), pathsFilename, namespace);
210 } catch (IOException e) {
211 throw new ApiDocGeneratorException("Unable to output doc file", e);
212 }
213 }
214
215
216
217
218
219
220
221
222
223
224 private String outputRoot(final String title, final String pathsFilename, final String parentNamespace)
225 throws IOException {
226 final String namespace = normalizeName(parentNamespace, "index");
227
228 final AsciiDoc pathsDoc = asciiDoc()
229 .documentTitle(title)
230 .rawParagraph(asciiDoc().rawText("*API ID:* ").mono(apiDescription.getId()).toString())
231 .rawParagraph(asciiDoc().rawText("*API Version:* ").mono(apiDescription.getVersion()).toString())
232 .rawLine(":toc: left")
233 .rawLine(":toclevels: 5")
234 .newline();
235 final String description = toTranslatedString(apiDescription.getDescription());
236 final String descriptionFilename = outputDescriptionBlock(description, namespace);
237 pathsDoc.include(descriptionFilename);
238
239 if (pathsFilename != null) {
240 pathsDoc.include(pathsFilename);
241 }
242
243 final String filename = namespace + ADOC_EXTENSION;
244 if (inMemoryMode) {
245 adocMap.put(filename, pathsDoc.toString());
246 } else {
247 pathsDoc.toFile(outputDirPath, filename);
248 }
249 return filename;
250 }
251
252
253
254
255
256
257
258
259
260
261
262
263 private String outputDescriptionBlock(final String defaultDescription, final String parentNamespace)
264 throws IOException {
265 final String namespace = normalizeName(parentNamespace, "description");
266 final String filename = namespace + ADOC_EXTENSION;
267 if (!copyReplacementFile(filename)) {
268 final AsciiDoc blockDoc = asciiDoc();
269 if (!isEmpty(defaultDescription)) {
270 blockDoc.rawParagraph(defaultDescription);
271 }
272 if (inMemoryMode) {
273 adocMap.put(filename, blockDoc.toString());
274 } else {
275 blockDoc.toFile(outputDirPath, filename);
276 }
277 }
278 return filename;
279 }
280
281
282
283
284
285
286
287
288
289 private String outputPaths(final String parentNamespace) throws IOException {
290 final String allPathsDocNamespace = normalizeName(parentNamespace, "paths");
291 final AsciiDoc allPathsDoc = asciiDoc()
292 .sectionTitle1("Paths");
293
294 final Paths paths = apiDescription.getPaths();
295 final List<String> pathNames = new ArrayList<>(paths.getNames());
296 Collections.sort(pathNames);
297 for (final String pathName : pathNames) {
298
299 final String pathDocNamespace = normalizeName(allPathsDocNamespace, pathName);
300
301 final VersionedPath versionedPath = paths.get(pathName);
302 final List<Version> versions = new ArrayList<>(versionedPath.getVersions());
303 Collections.sort(versions);
304
305 for (final Version version : versions) {
306 final String versionDocNamespace;
307 if (VersionedPath.UNVERSIONED.equals(version)) {
308 versionDocNamespace = pathDocNamespace;
309 } else {
310
311 final String versionName = version.toString();
312 versionDocNamespace = normalizeName(pathDocNamespace, versionName);
313 }
314
315
316 Resource resource = versionedPath.get(version);
317 if (resource.getReference() != null) {
318 resource = referenceResolver.getService(resource.getReference());
319 referencedServices.add(resource);
320 }
321 final String resourceFilename = outputResource(pathName, version, resource,
322 Collections.<Parameter>emptyList(), versionDocNamespace);
323 addPathResource(pathName, version, resourceFilename);
324 }
325 }
326
327 outputUndefinedServices(allPathsDocNamespace);
328
329
330 final List<String> pathKeys = new ArrayList<>(pathTree.keySet());
331 Collections.sort(pathKeys);
332 for (final String pathKey : pathKeys) {
333 allPathsDoc.sectionTitle2(asciiDoc().mono(pathKey).toString());
334
335 final Map<Version, String> versionMap = pathTree.get(pathKey);
336 for (final Map.Entry<Version, String> entry : versionMap.entrySet()) {
337 if (!VersionedPath.UNVERSIONED.equals(entry.getKey())) {
338 allPathsDoc.sectionTitle3(asciiDoc().mono(entry.getKey().toString()).toString());
339 }
340 allPathsDoc.include(entry.getValue());
341 }
342 }
343
344
345 final String filename = allPathsDocNamespace + ADOC_EXTENSION;
346 if (inMemoryMode) {
347 adocMap.put(filename, allPathsDoc.toString());
348 } else {
349 allPathsDoc.toFile(outputDirPath, filename);
350 }
351 return filename;
352 }
353
354
355
356
357
358
359
360 private void outputUndefinedServices(final String parentNamespace) throws IOException {
361
362 Services services = apiDescription.getServices();
363 if (services != null && !services.getNames().isEmpty()) {
364 for (final String name : apiDescription.getServices().getNames()) {
365 final Resource resource = apiDescription.getServices().get(name);
366 if (!referencedServices.contains(resource)) {
367
368 final Matcher m = SERVICE_NAME_PATTERN.matcher(name);
369 final String serviceName;
370 final Version serviceVersion;
371 if (m.matches()) {
372 serviceName = m.group(1);
373 serviceVersion = Version.version(m.group(2));
374 } else {
375 serviceName = name;
376 serviceVersion = VersionedPath.UNVERSIONED;
377 }
378
379 final String pathDocNamespace = normalizeName(parentNamespace, "<undefined>");
380 final String versionDocNamespace;
381 if (VersionedPath.UNVERSIONED.equals(serviceVersion)) {
382 versionDocNamespace = pathDocNamespace;
383 } else {
384
385 final String versionName = serviceVersion.toString();
386 versionDocNamespace = normalizeName(pathDocNamespace, versionName);
387 }
388
389 final String pathName = "<undefined>/" + serviceName;
390 final String resourceFilename = outputResource(pathName, serviceVersion, resource,
391 Collections.<Parameter>emptyList(), versionDocNamespace);
392 addPathResource(pathName, serviceVersion, resourceFilename);
393 }
394 }
395 }
396 }
397
398 private String outputResource(final String pathName, final Version version, final Resource resource,
399 final List<Parameter> parameters, final String parentNamespace) throws IOException {
400 final boolean unversioned = VersionedPath.UNVERSIONED.equals(version);
401 final int sectionLevel = unversioned ? 3 : 4;
402
403 final String namespace = normalizeName(parentNamespace, "resource");
404 final AsciiDoc resourceDoc = asciiDoc();
405
406 final String descriptionFilename = outputDescriptionBlock(
407 toTranslatedString(resource.getDescription()), namespace);
408 resourceDoc.include(descriptionFilename);
409
410 outputOperation(CrestMethod.CREATE, resource, sectionLevel, parameters, namespace, resourceDoc);
411 outputOperation(CrestMethod.READ, resource, sectionLevel, parameters, namespace, resourceDoc);
412 outputOperation(CrestMethod.UPDATE, resource, sectionLevel, parameters, namespace, resourceDoc);
413 outputOperation(CrestMethod.DELETE, resource, sectionLevel, parameters, namespace, resourceDoc);
414 outputOperation(CrestMethod.PATCH, resource, sectionLevel, parameters, namespace, resourceDoc);
415
416 outputActionOperations(resource, sectionLevel, parameters, namespace, resourceDoc);
417 outputQueryOperations(resource, sectionLevel, parameters, namespace, resourceDoc);
418
419
420 outputItems(version, resource, parameters, pathName, parentNamespace);
421
422
423 outputSubResources(version, resource.getSubresources(), parameters, pathName, parentNamespace);
424
425
426 final String resourceFilename = namespace + ADOC_EXTENSION;
427 if (inMemoryMode) {
428 adocMap.put(resourceFilename, resourceDoc.toString());
429 } else {
430 resourceDoc.toFile(outputDirPath, resourceFilename);
431 }
432 return resourceFilename;
433 }
434
435 private void outputItems(final Version version, final Resource resource, final List<Parameter> parameters,
436 final String parentPathName, final String parentNamespace) throws IOException {
437 Items items = resource.getItems();
438 if (items != null) {
439 Parameter pathParameter = items.getPathParameter();
440 final String itemsPathName = parentPathName + "/{" + pathParameter.getName() + "}";
441 final String itemsPathDocNamespace = normalizeName(parentNamespace, itemsPathName);
442
443 final Resource itemsResource = items.asResource(resource.isMvccSupported(),
444 resource.getResourceSchema(), resource.getTitle(), resource.getDescription());
445
446 final List<Parameter> itemsParameters = mergeParameters(mergeParameters(new ArrayList<>(parameters),
447 resource.getParameters()), pathParameter);
448
449 outputSubResources(version, items.getSubresources(), itemsParameters, itemsPathName, itemsPathDocNamespace);
450
451 final String resourceFilename = outputResource(parentPathName, version,
452 itemsResource, itemsParameters, itemsPathDocNamespace);
453 addPathResource(itemsPathName, version, resourceFilename);
454 }
455 }
456
457 private void outputSubResources(final Version version, final SubResources subResources,
458 final List<Parameter> parameters, final String parentPathName, final String parentNamespace)
459 throws IOException {
460 if (subResources != null) {
461 final List<String> subPathNames = new ArrayList<>(subResources.getNames());
462 Collections.sort(subPathNames);
463
464 for (final String name : subPathNames) {
465 final String subPathName = buildPath(parentPathName, name);
466
467
468 final List<Parameter> subresourcesParameters = unmodifiableList(mergeParameters(
469 new ArrayList<>(parameters), buildPathParameters(subPathName)));
470
471 final String subPathDocNamespace = normalizeName(parentNamespace, subPathName);
472
473 Resource subResource = subResources.get(name);
474 if (subResource.getReference() != null) {
475 subResource = referenceResolver.getService(subResource.getReference());
476 referencedServices.add(subResource);
477 }
478
479 final String resourceFilename = outputResource(subPathName, version,
480 subResource, subresourcesParameters, subPathDocNamespace);
481 addPathResource(subPathName, version, resourceFilename);
482 }
483 }
484 }
485
486
487
488
489
490
491
492
493
494
495
496
497 private void outputOperation(final CrestMethod crestMethod, final Resource resource, final int sectionLevel,
498 final List<Parameter> parameters, final String parentNamespace, final AsciiDoc parentDoc)
499 throws IOException {
500
501 final Operation operation;
502 final boolean responseOnly;
503 final String displayName;
504 switch (crestMethod) {
505 case CREATE:
506 displayName = "Create";
507 responseOnly = false;
508 operation = resource.getCreate();
509 break;
510 case READ:
511 displayName = "Read";
512 responseOnly = true;
513 operation = resource.getRead();
514 break;
515 case UPDATE:
516 displayName = "Update";
517 responseOnly = false;
518 operation = resource.getUpdate();
519 break;
520 case DELETE:
521 displayName = "Delete";
522 responseOnly = true;
523 operation = resource.getDelete();
524 break;
525 case PATCH:
526 displayName = "Patch";
527 responseOnly = true;
528 operation = resource.getPatch();
529 break;
530 default:
531
532 throw new ApiDocGeneratorException("Unsupported CREST method: " + crestMethod);
533 }
534
535 if (operation != null) {
536 final String namespace = normalizeName(parentNamespace, displayName);
537 final AsciiDoc operationDoc = asciiDoc()
538 .sectionTitle(displayName, sectionLevel);
539 final String description = toTranslatedString(operation.getDescription());
540 final String descriptionFilename = outputDescriptionBlock(description, namespace);
541 operationDoc.include(descriptionFilename);
542
543 final List<String> headers = new ArrayList<>();
544 final List<Integer> columnWidths = new ArrayList<>();
545 final AsciiDocTable table = operationDoc.tableStart();
546 outputStability(operation.getStability(), table, headers, columnWidths);
547 outputMvccSupport(resource.isMvccSupported(), table, headers, columnWidths);
548 if (crestMethod == CrestMethod.CREATE) {
549 final Create create = resource.getCreate();
550 outputCreateMode(create.getMode(), table, headers, columnWidths);
551 outputSingletonStatus(create.isSingleton(), table, headers, columnWidths);
552 } else if (crestMethod == CrestMethod.PATCH) {
553 final Patch patch = resource.getPatch();
554 outputSupportedPatchOperations(patch.getOperations(), table, headers, columnWidths);
555 }
556 table.headers(headers)
557 .columnWidths(columnWidths)
558 .tableEnd();
559
560 outputParameters(operation.getParameters(), parameters, namespace, operationDoc);
561 outputResourceEntity(resource, responseOnly, operationDoc);
562 outputErrors(operation.getApiErrors(), operationDoc);
563
564 parentDoc.horizontalRule();
565
566 if (inMemoryMode) {
567 parentDoc.rawText(operationDoc.toString());
568 } else {
569 final String filename = namespace + ADOC_EXTENSION;
570 operationDoc.toFile(outputDirPath, filename);
571 parentDoc.include(filename);
572 }
573 }
574 }
575
576
577
578
579
580
581
582
583
584
585
586 private void outputActionOperations(final Resource resource, final int sectionLevel,
587 final List<Parameter> parameters, final String parentNamespace, final AsciiDoc parentDoc)
588 throws IOException {
589 if (!isEmpty(resource.getActions())) {
590 final String namespace = normalizeName(parentNamespace, "action");
591 final AsciiDoc operationDoc = asciiDoc();
592
593 for (final Action action : resource.getActions()) {
594 final String filename = outputActionOperation(resource, action, sectionLevel, parameters, namespace);
595 operationDoc.include(filename);
596 }
597
598 if (inMemoryMode) {
599 parentDoc.rawText(operationDoc.toString());
600 } else {
601 final String filename = namespace + ADOC_EXTENSION;
602 operationDoc.toFile(outputDirPath, filename);
603 parentDoc.include(filename);
604 }
605 }
606 }
607
608
609
610
611
612
613
614
615
616
617
618
619 private String outputActionOperation(final Resource resource, final Action action, final int sectionLevel,
620 final List<Parameter> parameters, final String parentNamespace) throws IOException {
621 final String namespace = normalizeName(parentNamespace, action.getName());
622 final AsciiDoc operationDoc = asciiDoc()
623 .horizontalRule()
624 .sectionTitle(asciiDoc().rawText("Action: ").mono(action.getName()).toString(), sectionLevel);
625 final String description = toTranslatedString(action.getDescription());
626 final String descriptionFilename = outputDescriptionBlock(description, namespace);
627 operationDoc.include(descriptionFilename);
628
629 final List<String> headers = new ArrayList<>();
630 final List<Integer> columnWidths = new ArrayList<>();
631 final AsciiDocTable table = operationDoc.tableStart();
632 outputStability(action.getStability(), table, headers, columnWidths);
633 outputMvccSupport(resource.isMvccSupported(), table, headers, columnWidths);
634 table.headers(headers)
635 .columnWidths(columnWidths)
636 .tableEnd();
637
638 outputParameters(action.getParameters(), parameters, namespace, operationDoc);
639
640 if (action.getRequest() != null) {
641 final Schema schema = action.getRequest().getReference() == null
642 ? action.getRequest()
643 : referenceResolver.getDefinition(action.getRequest().getReference());
644
645 final AsciiDoc blockDoc = asciiDoc()
646 .blockTitle("Request Entity")
647 .rawParagraph("This operation takes a request resource that conforms to the following schema:")
648 .listingBlock(OBJECT_MAPPER.writeValueAsString(schema.getSchema().getObject()), "json");
649 operationDoc.rawText(blockDoc.toString());
650 }
651
652 if (action.getResponse() != null) {
653 final Schema schema = action.getResponse().getReference() == null
654 ? action.getResponse()
655 : referenceResolver.getDefinition(action.getResponse().getReference());
656
657 final AsciiDoc blockDoc = asciiDoc()
658 .blockTitle("Response Entity")
659 .rawParagraph("This operation returns a response resource that conforms to the following schema:")
660 .listingBlock(OBJECT_MAPPER.writeValueAsString(schema.getSchema().getObject()), "json");
661 operationDoc.rawText(blockDoc.toString());
662 }
663
664 outputErrors(action.getApiErrors(), operationDoc);
665
666 final String filename = namespace + ADOC_EXTENSION;
667 if (inMemoryMode) {
668 adocMap.put(filename, operationDoc.toString());
669 } else {
670 operationDoc.toFile(outputDirPath, filename);
671 }
672 return filename;
673 }
674
675
676
677
678
679
680
681
682
683
684
685 private void outputQueryOperations(final Resource resource, final int sectionLevel,
686 final List<Parameter> parameters, final String parentNamespace,
687 final AsciiDoc parentDoc) throws IOException {
688 if (!isEmpty(resource.getQueries())) {
689 final String namespace = normalizeName(parentNamespace, "query");
690 final AsciiDoc operationDoc = asciiDoc();
691
692 for (final Query query : resource.getQueries()) {
693 final String filename = outputQueryOperation(resource, query, sectionLevel, parameters,
694 namespace);
695 operationDoc.include(filename);
696 }
697
698 final String filename = namespace + ADOC_EXTENSION;
699 if (inMemoryMode) {
700 adocMap.put(filename, operationDoc.toString());
701 } else {
702 operationDoc.toFile(outputDirPath, filename);
703 }
704 parentDoc.include(filename);
705 }
706 }
707
708
709
710
711
712
713
714
715
716
717
718
719 private String outputQueryOperation(final Resource resource, final Query query, final int sectionLevel,
720 final List<Parameter> parameters, final String parentNamespace) throws IOException {
721 final String namespace;
722 final AsciiDoc operationDoc = asciiDoc()
723 .horizontalRule();
724
725 switch (query.getType()) {
726 case ID:
727 namespace = normalizeName(parentNamespace, "id", query.getQueryId());
728 operationDoc.sectionTitle(asciiDoc().rawText("Query by ID: ").mono(query.getQueryId()).toString(),
729 sectionLevel);
730 break;
731 case FILTER:
732 namespace = normalizeName(parentNamespace, "filter");
733 operationDoc.sectionTitle("Query by Filter", sectionLevel);
734 break;
735 case EXPRESSION:
736 namespace = normalizeName(parentNamespace, "expression");
737 operationDoc.sectionTitle("Query by Expression", sectionLevel);
738 break;
739 default:
740 throw new ApiDocGeneratorException("Unsupported QueryType: " + query.getType());
741 }
742
743 final String descriptionFilename = outputDescriptionBlock(
744 toTranslatedString(query.getDescription()), namespace);
745 operationDoc.include(descriptionFilename);
746
747 final List<String> headers = new ArrayList<>();
748 final List<Integer> columnWidths = new ArrayList<>();
749 final AsciiDocTable table = operationDoc.tableStart();
750 outputStability(query.getStability(), table, headers, columnWidths);
751 outputMvccSupport(resource.isMvccSupported(), table, headers, columnWidths);
752
753 if (!isEmpty(query.getQueryableFields())) {
754 headers.add(asciiDoc().link("query-queryable-fields", "Queryable Fields").toString());
755 columnWidths.add(COLUMN_WIDTH_MEDIUM);
756
757 final AsciiDoc blockDoc = asciiDoc();
758 for (final String field : query.getQueryableFields()) {
759 blockDoc.unorderedList1(asciiDoc().mono(field).toString());
760 }
761 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
762 }
763
764 if (!isEmpty(query.getPagingModes())) {
765 headers.add(asciiDoc().link("query-paging-modes", "Paging Modes").toString());
766 columnWidths.add(COLUMN_WIDTH_MEDIUM);
767
768 final AsciiDoc blockDoc = asciiDoc();
769 for (final PagingMode pagingMode : query.getPagingModes()) {
770 blockDoc.unorderedList1(asciiDoc().mono(pagingMode.toString()).toString());
771 }
772 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
773 }
774
775 if (!isEmpty(query.getCountPolicies())) {
776 headers.add(asciiDoc().link("query-page-count-policies", "Page Count Policies").toString());
777 columnWidths.add(COLUMN_WIDTH_MEDIUM);
778
779 final AsciiDoc blockDoc = asciiDoc();
780 for (final CountPolicy countPolicy : query.getCountPolicies()) {
781 blockDoc.unorderedList1(asciiDoc().mono(countPolicy.toString()).toString());
782 }
783 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
784 }
785
786 if (!isEmpty(query.getSupportedSortKeys())) {
787 headers.add(asciiDoc().link("query-sort-keys", "Supported Sort Keys").toString());
788 columnWidths.add(COLUMN_WIDTH_MEDIUM);
789
790 final AsciiDoc blockDoc = asciiDoc();
791 for (final String sortKey : query.getSupportedSortKeys()) {
792 blockDoc.unorderedList1(asciiDoc().mono(sortKey).toString());
793 }
794 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
795 }
796
797 table.headers(headers)
798 .columnWidths(columnWidths)
799 .tableEnd();
800
801 outputParameters(query.getParameters(), parameters, namespace, operationDoc);
802
803
804 outputResourceEntity(resource, true, operationDoc);
805
806 outputErrors(query.getApiErrors(), operationDoc);
807
808 final String filename = namespace + ADOC_EXTENSION;
809 if (inMemoryMode) {
810 adocMap.put(filename, operationDoc.toString());
811 } else {
812 operationDoc.toFile(outputDirPath, filename);
813 }
814 return filename;
815 }
816
817
818
819
820
821
822
823
824
825 private static void outputStability(Stability stability, final AsciiDocTable table, final List<String> headers,
826 final List<Integer> columnWidths) {
827 if (stability == null) {
828 stability = Stability.STABLE;
829 }
830
831 headers.add(asciiDoc().link("interface-stability-definitions", "Stability").toString());
832 columnWidths.add(COLUMN_WIDTH_SMALL);
833
834 table.columnCell(stability.name());
835 }
836
837
838
839
840
841
842
843
844
845 private static void outputMvccSupport(final boolean mvccSupported, final AsciiDocTable table,
846 final List<String> headers, final List<Integer> columnWidths) {
847 headers.add(asciiDoc().link("MVCC", "MVCC").toString());
848 columnWidths.add(COLUMN_WIDTH_SMALL);
849
850
851 table.columnCell(mvccSupported ? "✓" : "⃠");
852 }
853
854
855
856
857
858
859
860
861 private void outputResourceEntity(final Resource resource, final boolean responseOnly, final AsciiDoc doc)
862 throws IOException {
863 if (resource.getResourceSchema() != null) {
864 final Schema schema = resource.getResourceSchema().getReference() == null
865 ? resource.getResourceSchema()
866 : referenceResolver.getDefinition(resource.getResourceSchema().getReference());
867 final AsciiDoc blockDoc = asciiDoc()
868 .blockTitle("Resource Entity");
869 if (responseOnly) {
870 blockDoc.rawParagraph(
871 "This operation returns a response resource that conforms to the following schema:");
872 } else {
873 blockDoc.rawParagraph("This operation takes a request body and returns a response resource that "
874 + "conforms to the following schema:");
875 }
876 blockDoc.listingBlock(OBJECT_MAPPER.writeValueAsString(schema.getSchema().getObject()), "json");
877
878 doc.rawText(blockDoc.toString());
879 }
880 }
881
882
883
884
885
886
887
888
889
890 private static void outputSingletonStatus(final boolean isSingleton, final AsciiDocTable table,
891 final List<String> headers, final List<Integer> columnWidths) {
892 headers.add(asciiDoc().link("create-singleton", "Singleton").toString());
893 columnWidths.add(COLUMN_WIDTH_SMALL);
894
895
896 table.columnCell(isSingleton ? "✓" : "⃠");
897 }
898
899
900
901
902
903
904
905
906
907 private void outputParameters(final Parameter[] operationParameters, final List<Parameter> inheritedParameters,
908 final String parentNamespace, final AsciiDoc doc)
909 throws IOException {
910 final List<Parameter> parameters = mergeParameters(new ArrayList<>(inheritedParameters), operationParameters);
911 if (parameters.isEmpty()) {
912 return;
913 }
914 final String parametersNamespace = normalizeName(parentNamespace, "parameters");
915 final AsciiDocTable table = doc.tableStart()
916 .title("Parameters")
917 .headers("Name", "Type", "Description", "Required", "In", "Values")
918 .columnWidths(2, 1, 4, 1, 1, 2);
919 for (final Parameter parameter : parameters) {
920
921 String enumValuesContent = null;
922 if (!isEmpty(parameter.getEnumValues()) || !isEmpty(parameter.getDefaultValue())) {
923 final AsciiDoc enumValuesDoc = asciiDoc();
924 if (!isEmpty(parameter.getDefaultValue())) {
925 enumValuesDoc.italic("Default:")
926 .rawText(" ")
927 .mono(parameter.getDefaultValue())
928 .newline();
929 }
930 if (!isEmpty(parameter.getEnumValues())) {
931 final String[] enumValues = parameter.getEnumValues();
932 final String[] enumTitles = parameter.getEnumTitles();
933 for (int i = 0; i < enumValues.length; ++i) {
934 final AsciiDoc enumDoc = asciiDoc()
935 .mono(enumValues[i]);
936 if (enumTitles != null) {
937 enumDoc.rawText(": " + enumTitles[i]);
938 }
939 enumValuesDoc.unorderedList1(enumDoc.toString());
940 }
941 }
942 enumValuesContent = enumValuesDoc.toString();
943 }
944
945 final String namespace = normalizeName(parametersNamespace, parameter.getName());
946 final String description = toTranslatedString(parameter.getDescription());
947 final String descriptionFilename = outputDescriptionBlock(description, namespace);
948
949
950 table.columnCell(parameter.getName(), MONO_CELL)
951 .columnCell(parameter.getType(), MONO_CELL)
952 .columnCell(asciiDoc().include(descriptionFilename).toString(), ASCII_DOC_CELL)
953 .columnCell(ValidationUtil.nullToFalse(parameter.isRequired()) ? "✓" : null)
954 .columnCell(parameter.getSource().name(), MONO_CELL)
955 .columnCell(enumValuesContent, ASCII_DOC_CELL)
956 .rowEnd();
957 }
958 table.tableEnd();
959 }
960
961
962
963
964
965
966
967 private String toTranslatedString(LocalizableString localizableString) {
968 return localizableString == null
969 ? null
970 : localizableString.toTranslatedString(PREFERRED_LOCALES);
971 }
972
973
974
975
976
977
978
979
980
981 private static void outputCreateMode(final CreateMode createMode, final AsciiDocTable table,
982 final List<String> headers, final List<Integer> columnWidths) {
983 headers.add("Resource ID");
984 columnWidths.add(COLUMN_WIDTH_MEDIUM);
985
986 final AsciiDoc doc = asciiDoc();
987
988 switch (createMode) {
989 case ID_FROM_CLIENT:
990 doc.rawText("Assigned by client");
991 break;
992 case ID_FROM_SERVER:
993 doc.rawText("Assigned by server (do not supply)");
994 break;
995 default:
996 throw new ApiDocGeneratorException("Unsupported CreateMode: " + createMode);
997 }
998
999 table.columnCell(doc.toString());
1000 }
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010 private static void outputSupportedPatchOperations(final PatchOperation[] patchOperations,
1011 final AsciiDocTable table, final List<String> headers, final List<Integer> columnWidths) {
1012 headers.add(asciiDoc().link("patch-operations", "Patch Operations").toString());
1013 columnWidths.add(COLUMN_WIDTH_MEDIUM);
1014
1015 final AsciiDoc blockDoc = asciiDoc();
1016 for (final PatchOperation patchOperation : patchOperations) {
1017 blockDoc.unorderedList1(patchOperation.name());
1018 }
1019 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
1020 }
1021
1022
1023
1024
1025
1026
1027
1028 private void outputErrors(final ApiError[] apiErrors, final AsciiDoc doc) throws IOException {
1029 if (!isEmpty(apiErrors)) {
1030 doc.blockTitle("Errors");
1031 final AsciiDocTable table = doc.tableStart()
1032 .headers("Code", "Description")
1033 .columnWidths(1, 10);
1034
1035
1036 final List<ApiError> resolvedErrors = new ArrayList<>(apiErrors.length);
1037 for (final ApiError error : apiErrors) {
1038 if (error.getReference() != null) {
1039 final ApiError resolved = referenceResolver.getError(error.getReference());
1040 if (resolved != null && resolved.getReference() == null) {
1041 resolvedErrors.add(resolved);
1042 }
1043 } else {
1044 resolvedErrors.add(error);
1045 }
1046 }
1047 Collections.sort(resolvedErrors, ApiError.ERROR_COMPARATOR);
1048
1049 for (final ApiError error : resolvedErrors) {
1050 table.columnCell(String.valueOf(error.getCode()), MONO_CELL);
1051 if (error.getSchema() == null) {
1052 table.columnCell(toTranslatedString(error.getDescription()));
1053 } else {
1054 final Schema schema = error.getSchema().getReference() == null
1055 ? error.getSchema()
1056 : referenceResolver.getDefinition(error.getSchema().getReference());
1057
1058 final AsciiDoc blockDoc = asciiDoc()
1059 .rawParagraph(toTranslatedString(error.getDescription()))
1060 .rawParagraph("This error may contain an underlying `cause` that conforms to the following "
1061 + "schema:")
1062 .listingBlock(OBJECT_MAPPER.writeValueAsString(schema.getSchema().getObject()), "json");
1063 table.columnCell(blockDoc.toString(), ASCII_DOC_CELL);
1064 }
1065 table.rowEnd();
1066 }
1067 table.tableEnd();
1068 }
1069 }
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079 private boolean copyReplacementFile(final String filename) throws IOException {
1080 if (inputDirPath != null) {
1081 final Path inputFilePath = inputDirPath.resolve(filename);
1082 final Path outputFilePath = outputDirPath.resolve(filename);
1083 if (Files.exists(inputFilePath)) {
1084
1085 Files.copy(inputFilePath, outputFilePath, StandardCopyOption.REPLACE_EXISTING);
1086 return true;
1087 }
1088 }
1089 return false;
1090 }
1091
1092
1093
1094
1095
1096
1097
1098
1099 private void addPathResource(final String pathName, final Version version, final String resourceFilename) {
1100 if (!pathTree.containsKey(pathName)) {
1101 pathTree.put(pathName, new LinkedHashMap<Version, String>());
1102 }
1103 final Map<Version, String> versionTree = pathTree.get(pathName);
1104 if (!versionTree.containsKey(version)) {
1105 versionTree.put(version, resourceFilename);
1106 }
1107 }
1108
1109
1110
1111
1112
1113
1114
1115
1116 private String toString(final String rootFilename) {
1117 if (!inMemoryMode) {
1118 throw new ApiDocGeneratorException("Expected inMemoryMode");
1119 }
1120 String s = adocMap.get(checkNotNull(rootFilename));
1121 final Matcher m = AsciiDoc.INCLUDE_PATTERN.matcher(s);
1122 while (m.find()) {
1123 final String content = adocMap.get(m.group(1));
1124 s = m.replaceFirst(isEmpty(content) ? "" : Matcher.quoteReplacement(content));
1125 m.reset(s);
1126 }
1127 return s;
1128 }
1129
1130 }