Skip to content

Commit e76385e

Browse files
committed
OpenAPI: Swagger UI + Redoc
- Added UI clients - OpenAPIModule on core with classpath optional dependency for UI - Write a utitlity webjars AssetSource - Update generate openapi file names
1 parent 9995044 commit e76385e

File tree

9 files changed

+236
-3
lines changed

9 files changed

+236
-3
lines changed

jooby/src/main/java/io/jooby/AssetSource.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
import javax.annotation.Nonnull;
1313
import javax.annotation.Nullable;
1414
import java.io.FileNotFoundException;
15+
import java.io.IOException;
16+
import java.io.InputStream;
1517
import java.nio.file.Files;
1618
import java.nio.file.Path;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Properties;
22+
import java.util.stream.Stream;
1723

1824
/**
1925
* An asset source is a collection or provider of {@link Asset}. There are two implementations:
@@ -45,6 +51,25 @@ public interface AssetSource {
4551
return new ClassPathAssetSource(loader, location);
4652
}
4753

54+
static @Nonnull AssetSource webjars(@Nonnull ClassLoader loader, @Nonnull String name) {
55+
List<String> location = Arrays.asList(
56+
"/META-INF/maven/org.webjars/" + name + "/pom.properties",
57+
"/META-INF/maven/org.webjars.npm/" + name + "/pom.properties"
58+
);
59+
String versionPath = location.stream().filter(it -> loader.getResource(it) != null)
60+
.findFirst()
61+
.orElseThrow(() -> SneakyThrows.propagate(new FileNotFoundException(location.toString())));
62+
try (InputStream in = loader.getResourceAsStream(versionPath)) {
63+
Properties properties = new Properties();
64+
properties.load(in);
65+
String version = properties.getProperty("version");
66+
String source = "/META-INF/resources/webjars/" + name + "/" + version;
67+
return new ClassPathAssetSource(loader, source);
68+
} catch (IOException x) {
69+
throw SneakyThrows.propagate(x);
70+
}
71+
}
72+
4873
/**
4974
* Creates a source from given location. Assets are resolved from file system.
5075
*

jooby/src/main/java/io/jooby/MediaType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public final class MediaType implements Comparable<MediaType> {
6161
public static final String MULTIPART_FORMDATA = "multipart/form-data";
6262

6363
/** YAML. */
64-
public static final String YAML = "application/yaml";
64+
public static final String YAML = "text/yaml";
6565

6666
/** ALL. */
6767
public static final String ALL = "*/*";
@@ -748,6 +748,7 @@ static boolean matches(@Nonnull String expected, @Nonnull String contentType) {
748748
case "qt":
749749
return new MediaType("video/quicktime", null);
750750
case "yaml":
751+
case "yml":
751752
return yaml;
752753
case "pnm":
753754
return new MediaType("image/x-portable-anymap", null);
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import io.jooby.SneakyThrows.Consumer2;
9+
import org.apache.commons.io.IOUtils;
10+
11+
import javax.annotation.Nonnull;
12+
import java.io.IOException;
13+
import java.util.Arrays;
14+
import java.util.EnumSet;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.Optional;
18+
import java.util.function.BiConsumer;
19+
20+
public class OpenAPIModule implements Extension {
21+
22+
public enum Format {
23+
JSON,
24+
YAML
25+
}
26+
27+
private static final String REDOC = "<!DOCTYPE html>\n"
28+
+ "<html>\n"
29+
+ " <head>\n"
30+
+ " <title>ReDoc</title>\n"
31+
+ " <!-- needed for adaptive design -->\n"
32+
+ " <meta charset=\"utf-8\"/>\n"
33+
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
34+
+ " <link href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700\" rel=\"stylesheet\">\n"
35+
+ "\n"
36+
+ " <!--\n"
37+
+ " ReDoc doesn't change outer page styles\n"
38+
+ " -->\n"
39+
+ " <style>\n"
40+
+ " body {\n"
41+
+ " margin: 0;\n"
42+
+ " padding: 0;\n"
43+
+ " }\n"
44+
+ " </style>\n"
45+
+ " </head>\n"
46+
+ " <body>\n"
47+
+ " <redoc spec-url='${openAPIPath}'></redoc>\n"
48+
+ " <script src=\"${redocPath}/bundles/redoc.standalone.js\"> </script>\n"
49+
+ " </body>\n"
50+
+ "</html>";
51+
52+
private static final String OPENAPI_PATH = "https://petstore.swagger.io/v2/swagger.json";
53+
54+
private final String openAPIPath;
55+
private String swaggerUIPath = "/swagger";
56+
private String redocPath = "/redoc";
57+
private EnumSet<Format> format = EnumSet.of(Format.JSON, Format.YAML);
58+
59+
public OpenAPIModule(String path) {
60+
this.openAPIPath = Router.normalizePath(path);
61+
}
62+
63+
public OpenAPIModule() {
64+
this("/");
65+
}
66+
67+
public OpenAPIModule swaggerUI(String path) {
68+
this.swaggerUIPath = Router.normalizePath(path);
69+
return this;
70+
}
71+
72+
public OpenAPIModule redoc(String path) {
73+
this.redocPath = Router.normalizePath(path);
74+
return this;
75+
}
76+
77+
public OpenAPIModule format(Format... format) {
78+
this.format = EnumSet.copyOf(Arrays.asList(format));
79+
return this;
80+
}
81+
82+
@Override public void install(@Nonnull Jooby application) throws Exception {
83+
String dir = Optional.ofNullable(application.getClass().getPackage())
84+
.map(Package::getName)
85+
.orElse("/")
86+
.replace(".", "/");
87+
88+
for (Format ext : format) {
89+
String filename = "/openapi." + ext.name().toLowerCase();
90+
String openAPIFileLocation = Router.normalizePath(dir) + filename;
91+
application.assets(fullPath(openAPIPath, filename), openAPIFileLocation);
92+
}
93+
94+
/** Configure UI: */
95+
configureUI(application);
96+
}
97+
98+
private void configureUI(@Nonnull Jooby application) {
99+
Map<String, Consumer2<Jooby, AssetSource>> ui = new HashMap<>();
100+
ui.put("swagger-ui", this::swaggerUI);
101+
ui.put("redoc", this::redoc);
102+
for (Map.Entry<String, Consumer2<Jooby, AssetSource>> e : ui.entrySet()) {
103+
String name = e.getKey();
104+
Optional<AssetSource> source = assetSource(application.getClassLoader(), name);
105+
if (source.isPresent()) {
106+
if (format.contains(Format.JSON)) {
107+
Consumer2<Jooby, AssetSource> consumer = e.getValue();
108+
consumer.accept(application, source.get());
109+
} else {
110+
application.getLog().debug("{} is disabled when json format is not supported", name);
111+
}
112+
}
113+
}
114+
}
115+
116+
private Optional<AssetSource> assetSource(ClassLoader loader, String name) {
117+
try {
118+
return Optional.of(AssetSource.webjars(loader, name));
119+
} catch (Exception x) {
120+
return Optional.empty();
121+
}
122+
}
123+
124+
private void redoc(Jooby application, AssetSource source) throws IOException {
125+
application.assets(redocPath + "/*", source);
126+
String openAPIJSON = fullPath(
127+
fullPath(application.getContextPath(), openAPIPath), "/openapi.json");
128+
String template = REDOC
129+
.replace("${openAPIPath}", openAPIJSON)
130+
.replace("${redocPath}", fullPath(application.getContextPath(), redocPath));
131+
application
132+
.get(redocPath, ctx -> ctx.setResponseType(MediaType.html).send(template));
133+
}
134+
135+
private void swaggerUI(Jooby application, AssetSource source) throws IOException {
136+
String template = swaggerTemplate(source,
137+
fullPath(application.getContextPath(), swaggerUIPath),
138+
fullPath(application.getContextPath(), openAPIPath));
139+
140+
application.assets(swaggerUIPath + "/*", source);
141+
application.get(swaggerUIPath, ctx -> ctx.setResponseType(MediaType.html).send(template));
142+
}
143+
144+
static String swaggerTemplate(AssetSource source, String contextPath, String openAPIPath)
145+
throws IOException {
146+
String template = IOUtils
147+
.toString(source.resolve("index.html").stream(), "UTF-8")
148+
.replace("./", contextPath + "/");
149+
if (template.contains(OPENAPI_PATH)) {
150+
return template.replace(OPENAPI_PATH, fullPath(openAPIPath, "/openapi.json"));
151+
} else {
152+
throw new IllegalStateException("Unable to find openAPI path from template");
153+
}
154+
}
155+
156+
private static String fullPath(String contextPath, String path) {
157+
return Router.noTrailingSlash(Router.normalizePath(contextPath + path));
158+
}
159+
}

jooby/src/test/java/io/jooby/RoutePathTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class RoutePathTest {
88

99
@Test
1010
public void normalize() {
11+
assertEquals("/path", Router.normalizePath("//path"));
1112
assertEquals("/", Router.normalizePath(null));
1213
assertEquals("/", Router.normalizePath(""));
1314
assertEquals("/", Router.normalizePath("/"));

modules/jooby-bom/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@
9595
<plexus-utils.version>3.3.0</plexus-utils.version>
9696
<quartz.version>2.3.2</quartz.version>
9797
<reactor.version>3.3.2.RELEASE</reactor.version>
98+
<redoc.version>2.0.0-rc.20</redoc.version>
9899
<rest-assured.version>4.2.0</rest-assured.version>
99100
<rocker.version>1.2.3</rocker.version>
100101
<rxjava.version>2.2.17</rxjava.version>
101102
<slf4j.version>1.7.30</slf4j.version>
102103
<spring.version>5.2.3.RELEASE</spring.version>
103104
<stork-maven-plugin.version>3.0.0</stork-maven-plugin.version>
105+
<swagger-ui.version>3.25.0</swagger-ui.version>
104106
<truth.version>1.0.1</truth.version>
105107
<unbescape.version>1.1.6.RELEASE</unbescape.version>
106108
<undertow.version>2.0.28.Final</undertow.version>
@@ -470,6 +472,18 @@
470472
<version>${cron-utils.version}</version>
471473
<type>jar</type>
472474
</dependency>
475+
<dependency>
476+
<groupId>org.webjars</groupId>
477+
<artifactId>swagger-ui</artifactId>
478+
<version>${swagger-ui.version}</version>
479+
<type>jar</type>
480+
</dependency>
481+
<dependency>
482+
<groupId>org.webjars.npm</groupId>
483+
<artifactId>redoc</artifactId>
484+
<version>${redoc.version}</version>
485+
<type>jar</type>
486+
</dependency>
473487
<dependency>
474488
<groupId>org.unbescape</groupId>
475489
<artifactId>unbescape</artifactId>

modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public static List<Format> parse(String value) {
9393
Files.createDirectories(dir);
9494
}
9595

96-
String name = Optional.ofNullable(filename).orElse(names[names.length - 1]);
96+
String name = "openapi";
9797

9898
for (Format format : Format.parse(this.format)) {
9999
Path output = dir.resolve(name + "." + format.extension());

modules/jooby-openapi/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
<dependency>
6666
<groupId>io.swagger.parser.v3</groupId>
6767
<artifactId>swagger-parser</artifactId>
68-
<version>2.0.17</version>
68+
<version>2.0.18</version>
6969
</dependency>
7070

7171
<!-- Test dependencies -->

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/StatusCodeParser.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
16
package io.jooby.internal.openapi;
27

38
public class StatusCodeParser {

pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
<rxjava.version>2.2.17</rxjava.version>
6868
<reactor.version>3.3.2.RELEASE</reactor.version>
6969

70+
<!--OpenAPI-->
71+
<swagger-ui.version>3.25.0</swagger-ui.version>
72+
<redoc.version>2.0.0-rc.20</redoc.version>
73+
7074
<!-- javax -->
7175
<javax.inject.version>1</javax.inject.version>
7276
<jsr305.version>3.0.2</jsr305.version>
@@ -579,6 +583,30 @@
579583
<version>${cron-utils.version}</version>
580584
</dependency>
581585

586+
<!-- OpenAPI -->
587+
<dependency>
588+
<groupId>org.webjars</groupId>
589+
<artifactId>swagger-ui</artifactId>
590+
<version>${swagger-ui.version}</version>
591+
<exclusions>
592+
<exclusion>
593+
<groupId>*</groupId>
594+
<artifactId>*</artifactId>
595+
</exclusion>
596+
</exclusions>
597+
</dependency>
598+
599+
<dependency>
600+
<groupId>org.webjars.npm</groupId>
601+
<artifactId>redoc</artifactId>
602+
<version>${redoc.version}</version>
603+
<exclusions>
604+
<exclusion>
605+
<groupId>*</groupId>
606+
<artifactId>*</artifactId>
607+
</exclusion>
608+
</exclusions>
609+
</dependency>
582610

583611
<!-- https://mvnrepository.com/artifact/org.unbescape/unbescape -->
584612
<dependency>

0 commit comments

Comments
 (0)