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