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} &mdash; content-negotiated (HTML view via {@link RedocUI} swap, plus all serializers
039 *       registered on the host).
040 *    <li>{@code /openapi.json} &mdash; <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} &mdash; <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=&#123;YamlSerializer.class&#125;)} 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 &mdash; 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}} &mdash; 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/*}) &mdash; 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] &mdash; 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] &mdash; 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] &mdash; 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}