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