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