001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.bean.openapi3.ui;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.bean.html5.HtmlBuilder.*;
021import static org.apache.juneau.bean.html5.HtmlBuilder.a;
022import static org.apache.juneau.common.utils.Utils.*;
023
024import java.util.*;
025
026import org.apache.juneau.*;
027import org.apache.juneau.bean.html5.*;
028import org.apache.juneau.bean.openapi3.*;
029import org.apache.juneau.collections.*;
030import org.apache.juneau.common.utils.*;
031import org.apache.juneau.cp.*;
032import org.apache.juneau.swap.*;
033
034/**
035 * Generates an OpenAPI-UI interface from an OpenAPI document.
036 *
037 * <h5 class='section'>See Also:</h5><ul>
038 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanOpenApi3">juneau-bean-openapi-v3</a>
039 * </ul>
040 */
041public class OpenApiUI extends ObjectSwap<OpenApi,Div> {
042
043   static final FileFinder RESOURCES = FileFinder
044      .create(BeanStore.INSTANCE)
045      .cp(OpenApiUI.class, null, true)
046      .dir(",")
047      .caching(Boolean.getBoolean("RestContext.disableClasspathResourceCaching.b") ? -1 : 1_000_000)
048      .build();
049
050   private static final Set<String> STANDARD_METHODS = set("get", "put", "post", "delete", "options", "head", "patch", "trace");
051
052   /**
053    * This UI applies to HTML requests only.
054    */
055   @Override
056   public org.apache.juneau.MediaType[] forMediaTypes() {
057      return new org.apache.juneau.MediaType[] {org.apache.juneau.MediaType.HTML};
058   }
059
060   private static class Session {
061      final int resolveRefsMaxDepth;
062      final OpenApi openApi;
063
064      Session(OpenApi openApi) {
065         this.openApi = openApi.copy();
066         this.resolveRefsMaxDepth = 1;
067      }
068   }
069
070   @Override
071   public Div swap(BeanSession beanSession, OpenApi openApi) throws Exception {
072
073      var s = new Session(openApi);
074
075      var css = RESOURCES.getString("files/htdocs/styles/OpenApiUI.css", null).orElse(null);
076      if (css == null)
077         css = RESOURCES.getString("OpenApiUI.css", null).orElse(null);
078
079      var outer = div(
080         style(css),
081         script("text/javascript", RESOURCES.getString("OpenApiUI.js", null).orElse(null)),
082         header(s)
083      )._class("openapi-ui");
084
085      // Operations without tags are rendered first.
086      outer.child(div()._class("tag-block tag-block-open").children(tagBlockContents(s, null)));
087
088      if (s.openApi.getTags() != null) {
089         s.openApi.getTags().forEach(x -> {
090            var tagBlock = div()._class("tag-block tag-block-open").children(
091               tagBlockSummary(x),
092               tagBlockContents(s, x)
093            );
094            outer.child(tagBlock);
095         });
096      }
097
098      if (s.openApi.getComponents() != null && s.openApi.getComponents().getSchemas() != null) {
099         var modelBlock = div()._class("tag-block").children(
100            modelsBlockSummary(),
101            modelsBlockContents(s)
102         );
103         outer.child(modelBlock);
104      }
105
106      return outer;
107   }
108
109   // Creates the informational summary before the ops.
110   private Table header(Session s) {
111      var table = table()._class("header");
112
113      var info = s.openApi.getInfo();
114      if (info != null) {
115
116         if (info.getDescription() != null)
117            table.child(tr(th("Description:"),td(toBRL(info.getDescription()))));
118
119         if (info.getVersion() != null)
120            table.child(tr(th("Version:"),td(info.getVersion())));
121
122         var c = info.getContact();
123         if (c != null) {
124            var t2 = table();
125
126            if (c.getName() != null)
127               t2.child(tr(th("Name:"),td(c.getName())));
128            if (c.getUrl() != null)
129               t2.child(tr(th("URL:"),td(a(c.getUrl(), c.getUrl()))));
130            if (c.getEmail() != null)
131               t2.child(tr(th("Email:"),td(a("mailto:"+ c.getEmail(), c.getEmail()))));
132
133            table.child(tr(th("Contact:"),td(t2)));
134         }
135
136         var l = info.getLicense();
137         if (l != null) {
138            var content = l.getName() != null ? l.getName() : l.getUrl();
139            var child = l.getUrl() != null ? a(l.getUrl(), content) : l.getName();
140            table.child(tr(th("License:"),td(child)));
141         }
142
143         ExternalDocumentation ed = s.openApi.getExternalDocs();
144         if (ed != null) {
145            var content = ed.getDescription() != null ? ed.getDescription() : ed.getUrl();
146            var child = ed.getUrl() != null ? a(ed.getUrl(), content) : ed.getDescription();
147            table.child(tr(th("Docs:"),td(child)));
148         }
149
150         if (info.getTermsOfService() != null) {
151            var tos = info.getTermsOfService();
152            var child = StringUtils.isUri(tos) ? a(tos, tos) : tos;
153            table.child(tr(th("Terms of Service:"),td(child)));
154         }
155      }
156
157      return table;
158   }
159
160   // Creates the "pet  Everything about your Pets  ext-link" header.
161   private HtmlElement tagBlockSummary(Tag t) {
162      var ed = t.getExternalDocs();
163
164      var content = ed != null && ed.getDescription() != null ? ed.getDescription() : (ed != null ? ed.getUrl() : null);
165      return div()._class("tag-block-summary").children(
166         span(t.getName())._class("name"),
167         span(toBRL(t.getDescription()))._class("description"),
168         ed != null && ed.getUrl() != null ? span(a(ed.getUrl(), content))._class("extdocs") : null
169      ).onclick("toggleTagBlock(this)");
170   }
171
172   // Creates the contents under the "pet  Everything about your Pets  ext-link" header.
173   private Div tagBlockContents(Session s, Tag t) {
174      var tagBlockContents = div()._class("tag-block-contents");
175
176      if (s.openApi.getPaths() != null) {
177         s.openApi.getPaths().forEach((path, pathItem) -> {
178            // Check each HTTP method in the path item
179            if (pathItem.getGet() != null)
180               addOperationIfTagMatches(tagBlockContents, s, path, "get", pathItem.getGet(), t);
181            if (pathItem.getPut() != null)
182               addOperationIfTagMatches(tagBlockContents, s, path, "put", pathItem.getPut(), t);
183            if (pathItem.getPost() != null)
184               addOperationIfTagMatches(tagBlockContents, s, path, "post", pathItem.getPost(), t);
185            if (pathItem.getDelete() != null)
186               addOperationIfTagMatches(tagBlockContents, s, path, "delete", pathItem.getDelete(), t);
187            if (pathItem.getOptions() != null)
188               addOperationIfTagMatches(tagBlockContents, s, path, "options", pathItem.getOptions(), t);
189            if (pathItem.getHead() != null)
190               addOperationIfTagMatches(tagBlockContents, s, path, "head", pathItem.getHead(), t);
191            if (pathItem.getPatch() != null)
192               addOperationIfTagMatches(tagBlockContents, s, path, "patch", pathItem.getPatch(), t);
193            if (pathItem.getTrace() != null)
194               addOperationIfTagMatches(tagBlockContents, s, path, "trace", pathItem.getTrace(), t);
195         });
196      }
197
198      return tagBlockContents;
199   }
200
201   private void addOperationIfTagMatches(Div tagBlockContents, Session s, String path, String method, Operation op, Tag t) {
202      if ((t == null && (op.getTags() == null || op.getTags().isEmpty())) ||
203         (t != null && op.getTags() != null && op.getTags().contains(t.getName()))) {
204         tagBlockContents.child(opBlock(s, path, method, op));
205      }
206   }
207
208   private Div opBlock(Session s, String path, String opName, Operation op) {
209
210      var opClass = op.getDeprecated() != null && op.getDeprecated() ? "deprecated" : opName.toLowerCase();
211      if (!(op.getDeprecated() != null && op.getDeprecated()) && !STANDARD_METHODS.contains(opClass))
212         opClass = "other";
213
214      return div()._class("op-block op-block-closed " + opClass).children(
215         opBlockSummary(path, opName, op),
216         div(tableContainer(s, op))._class("op-block-contents")
217      );
218   }
219
220   private HtmlElement opBlockSummary(String path, String opName, Operation op) {
221      return div()._class("op-block-summary").children(
222         span(opName.toUpperCase())._class("method-button"),
223         span(path)._class("path"),
224         op.getSummary() != null ? span(op.getSummary())._class("summary") : null
225      ).onclick("toggleOpBlock(this)");
226   }
227
228   private Div tableContainer(Session s, Operation op) {
229      var tableContainer = div()._class("table-container");
230
231      if (op.getDescription() != null)
232         tableContainer.child(div(toBRL(op.getDescription()))._class("op-block-description"));
233
234      if (op.getParameters() != null) {
235         tableContainer.child(div(h4("Parameters")._class("title"))._class("op-block-section-header"));
236
237         var parameters = table(tr(th("Name")._class("parameter-key"), th("Description")._class("parameter-key")))._class("parameters");
238
239         op.getParameters().forEach(x -> {
240            var piName = x.getName();
241            var required = x.getRequired() != null && x.getRequired();
242
243            var parameterKey = td(
244               div(piName)._class("name" + (required ? " required" : "")),
245               required ? div("required")._class("requiredlabel") : null,
246               x.getSchema() != null ? div(x.getSchema().getType())._class("type") : null,
247               div('(' + x.getIn() + ')')._class("in")
248            )._class("parameter-key");
249
250            var parameterValue = td(
251               div(toBRL(x.getDescription()))._class("description"),
252               examples(s, x)
253            )._class("parameter-value");
254
255            parameters.child(tr(parameterKey, parameterValue));
256         });
257
258         tableContainer.child(parameters);
259      }
260
261      if (op.getResponses() != null) {
262         tableContainer.child(div(h4("Responses")._class("title"))._class("op-block-section-header"));
263
264         var responses = table(tr(th("Code")._class("response-key"), th("Description")._class("response-key")))._class("responses");
265         tableContainer.child(responses);
266
267         op.getResponses().forEach((k,v) -> {
268            var code = td(k)._class("response-key");
269
270            var codeValue = td(
271               div(toBRL(v.getDescription()))._class("description"),
272               examples(s, v),
273               headers(v)
274            )._class("response-value");
275
276            responses.child(tr(code, codeValue));
277         });
278      }
279
280      return tableContainer;
281   }
282
283   private Div headers(Response ri) {
284      if (ri.getHeaders() == null)
285         return null;
286
287      var sectionTable = table(tr(th("Name"),th("Description"),th("Schema")))._class("section-table");
288
289      var headers = div(
290         div("Headers:")._class("section-name"),
291         sectionTable
292      )._class("headers");
293
294      ri.getHeaders().forEach((k,v) ->
295         sectionTable.child(
296            tr(
297               td(k)._class("name"),
298               td(toBRL(v.getDescription()))._class("description"),
299               td(v.asMap().keepAll("type","format","items","collectionFormat","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf"))
300            )
301         )
302      );
303
304      return headers;
305   }
306
307   private Div examples(Session s, Parameter pi) {
308      var m = new JsonMap();
309
310      try {
311         var si = pi.getSchema();
312         if (si != null)
313            m.put("model", si.copy().resolveRefs(s.openApi, new ArrayDeque<>(), s.resolveRefsMaxDepth));
314      } catch (Exception e) {
315         e.printStackTrace();
316      }
317
318      if (m.isEmpty())
319         return null;
320
321      return examplesDiv(m);
322   }
323
324   private Div examples(Session s, Response ri) {
325      var m = new JsonMap();
326      try {
327         var content = ri.getContent();
328         if (content != null) {
329            // For OpenAPI 3.0, content is a map of media types to MediaType objects
330            content.forEach((mediaType, mediaTypeObj) -> {
331               if (mediaTypeObj.getSchema() != null) {
332                  try {
333                     var schema = mediaTypeObj.getSchema().copy().resolveRefs(s.openApi, new ArrayDeque<>(), s.resolveRefsMaxDepth);
334                     m.put(mediaType, schema);
335                  } catch (Exception e) {
336                     e.printStackTrace();
337                  }
338               }
339            });
340         }
341      } catch (Exception e) {
342         e.printStackTrace();
343      }
344
345      if (m.isEmpty())
346         return null;
347
348      return examplesDiv(m);
349   }
350
351   private Div examplesDiv(JsonMap m) {
352      if (m.isEmpty())
353         return null;
354
355      Select select = null;
356      if (m.size() > 1) {
357         select = select().onchange("selectExample(this)")._class("example-select");
358      }
359
360      var div = div(select)._class("examples");
361
362      if (select != null)
363         select.child(option("model","model"));
364      div.child(div(m.remove("model"))._class("model active").attr("data-name", "model"));
365
366      var select2 = select;
367      m.forEach((k,v) -> {
368         if (select2 != null)
369            select2.child(option(k, k));
370         div.child(div(v.toString().replace("\\n", "\n"))._class("example").attr("data-name", k));
371      });
372
373      return div;
374   }
375
376   // Creates the "Model" header.
377   private HtmlElement modelsBlockSummary() {
378      return div()._class("tag-block-summary").children(span("Models")._class("name")).onclick("toggleTagBlock(this)");
379   }
380
381   // Creates the contents under the "Model" header.
382   private Div modelsBlockContents(Session s) {
383      var modelBlockContents = div()._class("tag-block-contents");
384      if (s.openApi.getComponents() != null && s.openApi.getComponents().getSchemas() != null) {
385         s.openApi.getComponents().getSchemas().forEach((k,v) -> modelBlockContents.child(modelBlock(k,v)));
386      }
387      return modelBlockContents;
388   }
389
390   private Div modelBlock(String modelName, SchemaInfo model) {
391      return div()._class("op-block op-block-closed model").children(
392         modelBlockSummary(modelName, model),
393         div(model)._class("op-block-contents")
394      );
395   }
396
397   private HtmlElement modelBlockSummary(String modelName, SchemaInfo model) {
398      return div()._class("op-block-summary").children(
399         span(modelName)._class("method-button"),
400         model.getDescription() != null ? span(toBRL(model.getDescription()))._class("summary") : null
401      ).onclick("toggleOpBlock(this)");
402   }
403
404   /**
405    * Replaces newlines with <br> elements.
406    */
407   private static List<Object> toBRL(String s) {
408      if (s == null)
409         return null;  // NOSONAR - Intentionally returning null.
410      if (s.indexOf(',') == -1)
411         return singletonList(s);
412      var l = Utils.list();
413      var sa = s.split("\n");
414      for (var i = 0; i < sa.length; i++) {
415         if (i > 0)
416            l.add(br());
417         l.add(sa[i]);
418      }
419      return l;
420   }
421}