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.rest.server.docs; 018 019import java.io.*; 020 021import org.apache.juneau.bean.openapi3.*; 022import org.apache.juneau.bean.openapi3.ui.*; 023import org.apache.juneau.http.response.*; 024import org.apache.juneau.marshall.*; 025import org.apache.juneau.marshall.html.*; 026import org.apache.juneau.marshall.json.*; 027import org.apache.juneau.marshall.jsonschema.*; 028import org.apache.juneau.marshall.yaml.*; 029import org.apache.juneau.rest.server.*; 030 031/** 032 * Mixin that publishes the OpenAPI 3.1 spec endpoints for a resource. 033 * 034 * <p> 035 * Three URL paths are mounted: 036 * </p> 037 * <ul class='spaced-list'> 038 * <li>{@code /openapi} — content-negotiated (HTML view via {@link RedocUI} swap, plus all serializers 039 * registered on the host). 040 * <li>{@code /openapi.json} — <em>format-pinned</em>: always emits JSON regardless of the request 041 * {@code Accept} header. Implemented via {@link RestResponse#getDirectWriter(String)} so the pin is 042 * robust against {@code Accept: text/html} or any other media type. 043 * <li>{@code /openapi.yaml} — <em>format-pinned</em>: always emits YAML regardless of the request 044 * {@code Accept} header. 045 * </ul> 046 * 047 * <p> 048 * Sibling of {@link SwaggerMixin} (Swagger v2 spec at {@code /api}), 049 * {@link SwaggerUiMixin} (HTML-first mount at {@code /swagger}), and {@link RedocMixin} 050 * (HTML-first mount at {@code /redoc}). All four classes live in the 051 * {@code org.apache.juneau.rest.server.docs} api-docs mixin pack. 052 * 053 * <p> 054 * The mixin declares {@link Rest#serializers() @Rest(serializers={YamlSerializer.class})} so the 055 * {@code /openapi} content-negotiated endpoint can serve {@code application/yaml} when explicitly 056 * requested via {@code Accept}. Sub-context inheritance (per the mixin sub-context model) confines this 057 * contribution to this mixin's endpoints — the host's other endpoints retain their own serializer set. 058 * 059 * <h5 class='section'>Configurable mount path:</h5> 060 * 061 * <p> 062 * The default mount prefix {@code /openapi} can be overridden via the SVL variable 063 * {@code ${juneau.openapi.path:openapi}} — the same variable is reused for all three mounts 064 * ({@code /openapi/*}, {@code /openapi.json}, {@code /openapi.yaml}) so a single override 065 * relocates the whole surface. Set via system property 066 * ({@code -Djuneau.openapi.path=v1/openapi}), environment variable 067 * ({@code JUNEAU_OPENAPI_PATH=v1/openapi}), or {@code Config} key 068 * ({@code juneau.openapi.path = v1/openapi}) to change the runtime mount without subclassing. 069 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}). 070 * 071 * <p> 072 * Override accepts bare token ({@code openapi}), leading slash ({@code /openapi}), trailing slash 073 * ({@code openapi/}), or wildcard suffix ({@code /openapi/*}) — all resolve to the same mount. 074 * 075 * <h5 class='section'>Mixin-only deployment:</h5> 076 * 077 * <p> 078 * This resource is designed for composition via {@code @Rest(mixins=...)}. The three mount paths 079 * ({@code /openapi/*}, {@code /openapi.json}, {@code /openapi.yaml}) are pinned at the op level 080 * by {@link RestGet @RestGet(path=...)} on the handler methods; a class-level 081 * {@code @Rest(paths=...)} declaration would be silently ignored under the mixin pattern (see 082 * {@link Rest#paths() @Rest(paths)} Javadoc). 083 * 084 * <h5 class='figure'>Composition example:</h5> 085 * 086 * <p class='bjava'> 087 * <ja>@Rest</ja>(path=<js>"/myresource"</js>, mixins=OpenApiMixin.<jk>class</jk>) 088 * <jk>public class</jk> MyResource <jk>extends</jk> RestServlet { 089 * <jc>// Now also serves /myresource/openapi, /myresource/openapi.json, /myresource/openapi.yaml.</jc> 090 * } 091 * </p> 092 * 093 * <h5 class='section'>See Also:</h5><ul> 094 * <li class='jc'>{@link SwaggerMixin} 095 * <li class='jc'>{@link SwaggerUiMixin} 096 * <li class='jc'>{@link RedocMixin} 097 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 098 * </ul> 099 * 100 * @since 10.0.0 101 */ 102// @formatter:off 103@Rest( 104 serializers={YamlSerializer.class} 105) 106@JsonSchemaConfig( 107 addDescriptionsTo="bean,collection,array,map,enum", 108 addExamplesTo="bean,collection,array,map", 109 ignoreTypes="OpenApi,org.apache.juneau.bean.html5.*", 110 useBeanDefs="true" 111) 112public class OpenApiMixin { 113 114 /** 115 * [GET /openapi] — emit the OpenAPI 3.1 document for this resource. 116 * 117 * <p> 118 * Standard Juneau content negotiation. {@code Accept: text/html} renders the Redoc HTML view via the 119 * {@link RedocUI} per-media-type swap; other accept types serialize the {@link OpenApi} bean using the 120 * host's serializer set (plus the {@link YamlSerializer} contributed by this mixin). 121 * 122 * @param req The current REST request. 123 * @return The OpenAPI document. 124 * @throws NotFound If no OpenAPI document is available for this resource. 125 */ 126 @RestGet( 127 path="/#{pathToken(${juneau.openapi.path:openapi})}/*", 128 summary="OpenAPI 3.1 documentation", 129 description="OpenAPI 3.1 documentation for this resource." 130 ) 131 @HtmlDocConfig( 132 rank=10, 133 navlinks={ 134 "back: servlet:/", 135 "json: servlet:/openapi.json", 136 "yaml: servlet:/openapi.yaml" 137 }, 138 aside="NONE" 139 ) 140 @MarshalledConfig( 141 swaps={ 142 RedocUI.class 143 } 144 ) 145 public OpenApi getOpenApi(RestRequest req) { 146 return req.getOpenApi().orElseThrow(NotFound::new); 147 } 148 149 /** 150 * [GET /openapi.json] — emit the OpenAPI 3.1 document as JSON, ignoring the {@code Accept} header. 151 * 152 * <p> 153 * Format-pinned: always returns {@code application/json}. Implemented via 154 * {@link RestResponse#getDirectWriter(String)} so the pin is not subject to content negotiation. 155 * 156 * @param req The current REST request. 157 * @param res The current REST response. 158 * @throws NotFound If no OpenAPI document is available for this resource. 159 * @throws IOException If an I/O error occurs while writing the response. 160 */ 161 @RestGet( 162 path="/#{pathToken(${juneau.openapi.path:openapi})}.json", 163 summary="OpenAPI 3.1 documentation (JSON)", 164 description="OpenAPI 3.1 documentation for this resource as JSON." 165 ) 166 public void getOpenApiJson(RestRequest req, RestResponse res) throws IOException { 167 var doc = req.getOpenApi().orElseThrow(NotFound::new); 168 try (var w = res.getDirectWriter("application/json")) { 169 JsonSerializer.DEFAULT_READABLE.write(doc, w); 170 } 171 } 172 173 /** 174 * [GET /openapi.yaml] — emit the OpenAPI 3.1 document as YAML, ignoring the {@code Accept} header. 175 * 176 * <p> 177 * Format-pinned: always returns {@code application/yaml}. 178 * 179 * @param req The current REST request. 180 * @param res The current REST response. 181 * @throws NotFound If no OpenAPI document is available for this resource. 182 * @throws IOException If an I/O error occurs while writing the response. 183 */ 184 @RestGet( 185 path="/#{pathToken(${juneau.openapi.path:openapi})}.yaml", 186 summary="OpenAPI 3.1 documentation (YAML)", 187 description="OpenAPI 3.1 documentation for this resource as YAML." 188 ) 189 public void getOpenApiYaml(RestRequest req, RestResponse res) throws IOException { 190 var doc = req.getOpenApi().orElseThrow(NotFound::new); 191 try (var w = res.getDirectWriter("application/yaml")) { 192 YamlSerializer.DEFAULT_READABLE.write(doc, w); 193 } 194 } 195}