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