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.bean.openapi3;
018
019import static org.apache.juneau.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023import static org.apache.juneau.internal.ConverterUtils.*;
024
025import java.util.*;
026
027import org.apache.juneau.commons.collections.*;
028import org.apache.juneau.json.*;
029import org.apache.juneau.objecttools.*;
030
031/**
032 * This is the root document object for the OpenAPI specification.
033 *
034 * <p>
035 * The OpenAPI Object is the root document that describes an entire API. It contains metadata about the API,
036 * available paths and operations, parameters, responses, authentication methods, and other information.
037 *
038 * <h5 class='section'>OpenAPI Specification:</h5>
039 * <p>
040 * The OpenAPI Object is composed of the following fields:
041 * <ul class='spaced-list'>
042 *    <li><c>openapi</c> (string, REQUIRED) - The OpenAPI Specification version (e.g., "3.0.0")
043 *    <li><c>info</c> ({@link Info}, REQUIRED) - Provides metadata about the API
044 *    <li><c>servers</c> (array of {@link Server}) - An array of Server Objects providing connectivity information
045 *    <li><c>paths</c> (map of {@link PathItem}) - The available paths and operations for the API
046 *    <li><c>components</c> ({@link Components}) - An element to hold various schemas for reuse
047 *    <li><c>security</c> (array of {@link SecurityRequirement}) - Security mechanisms applied to all operations
048 *    <li><c>tags</c> (array of {@link Tag}) - A list of tags for API documentation control
049 *    <li><c>externalDocs</c> ({@link ExternalDocumentation}) - Additional external documentation
050 * </ul>
051 *
052 * <h5 class='section'>Example:</h5>
053 * <p class='bjava'>
054 *    <jc>// Create an OpenAPI document</jc>
055 *    OpenApi <jv>doc</jv> = <jk>new</jk> OpenApi()
056 *       .setOpenapi(<js>"3.0.0"</js>)
057 *       .setInfo(
058 *          <jk>new</jk> Info()
059 *             .setTitle(<js>"My API"</js>)
060 *             .setVersion(<js>"1.0.0"</js>)
061 *       )
062 *       .setPaths(
063 *          JsonMap.<jsm>of</jsm>(
064 *             <js>"/pets"</js>, <jk>new</jk> PathItem()
065 *                .setGet(<jk>new</jk> Operation()
066 *                   .setSummary(<js>"List all pets"</js>)
067 *                )
068 *          )
069 *       );
070 * </p>
071 *
072 * <h5 class='section'>See Also:</h5><ul>
073 *    <li class='link'><a class="doclink" href="https://spec.openapis.org/oas/v3.0.0#openapi-object">OpenAPI Specification &gt; OpenAPI Object</a>
074 *    <li class='link'><a class="doclink" href="https://swagger.io/docs/specification/basic-structure/">OpenAPI Basic Structure</a>
075 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanOpenApi3">juneau-bean-openapi-v3</a>
076 * </ul>
077 */
078public class OpenApi extends OpenApiElement {
079
080   /** Represents a null OpenAPI document */
081   public static final OpenApi NULL = new OpenApi();
082
083   private static final Comparator<String> PATH_COMPARATOR = (o1, o2) -> o1.replace('{', '@').compareTo(o2.replace('{', '@'));
084
085   private String openapi = "3.0.0";
086   private Info info;
087   private List<Server> servers = list();
088   private Map<String,PathItem> paths;
089   private Components components;
090   private List<SecurityRequirement> security = list();
091   private List<Tag> tags = list();
092   private ExternalDocumentation externalDocs;
093
094   /**
095    * Default constructor.
096    */
097   public OpenApi() {}
098
099   /**
100    * Copy constructor.
101    *
102    * @param copyFrom The object to copy.
103    */
104   public OpenApi(OpenApi copyFrom) {
105      super(copyFrom);
106      this.openapi = copyFrom.openapi;
107      this.info = copyFrom.info;
108      if (nn(copyFrom.servers))
109         this.servers.addAll(copyOf(copyFrom.servers, Server::copy));
110      this.paths = copyOf(copyFrom.paths);
111      this.components = copyFrom.components;
112      if (nn(copyFrom.security))
113         this.security.addAll(copyOf(copyFrom.security, SecurityRequirement::copy));
114      if (nn(copyFrom.tags))
115         this.tags.addAll(copyOf(copyFrom.tags, Tag::copy));
116      this.externalDocs = copyFrom.externalDocs;
117   }
118
119   /**
120    * Adds a path to this OpenAPI document.
121    *
122    * @param path The path string.  Must not be <jk>null</jk>.
123    * @param pathItem The path item.  Must not be <jk>null</jk>.
124    * @return This object.
125    */
126   public OpenApi addPath(String path, PathItem pathItem) {
127      assertArgNotNull("path", path);
128      assertArgNotNull("pathItem", pathItem);
129      if (paths == null)
130         paths = new TreeMap<>(PATH_COMPARATOR);
131      getPaths().put(path, pathItem);
132      return this;
133   }
134
135   /**
136    * Bean property fluent setter:  <property>security</property>.
137    *
138    * <p>
139    * A declaration of which security mechanisms can be used across the API.
140    *
141    * @param values
142    *    The values to add to this property.
143    *    <br>Ignored if <jk>null</jk>.
144    * @return This object.
145    */
146   public OpenApi addSecurity(Collection<SecurityRequirement> values) {
147      security = listb(SecurityRequirement.class).sparse().addAny(security, values).build();
148      return this;
149   }
150
151   /**
152    * Bean property fluent setter:  <property>security</property>.
153    *
154    * <p>
155    * A declaration of which security mechanisms can be used across the API.
156    *
157    * @param values
158    *    The values to add to this property.
159    *    <br>Ignored if <jk>null</jk>.
160    * @return This object.
161    */
162   public OpenApi addSecurity(SecurityRequirement...values) {
163      security = listb(SecurityRequirement.class).sparse().addAll(security).addAny((Object)values).build();
164      return this;
165   }
166
167   /**
168    * Bean property fluent setter:  <property>servers</property>.
169    *
170    * <p>
171    * An array of Server Objects, which provide connectivity information to a target server.
172    *
173    * @param values
174    *    The values to add to this property.
175    *    <br>Ignored if <jk>null</jk>.
176    * @return This object.
177    */
178   public OpenApi addServers(Collection<Server> values) {
179      if (nn(values))
180         servers.addAll(values);
181      return this;
182   }
183
184   /**
185    * Bean property fluent setter:  <property>servers</property>.
186    *
187    * <p>
188    * An array of Server Objects, which provide connectivity information to a target server.
189    *
190    * @param values
191    *    The values to add to this property.
192    *    <br>Ignored if <jk>null</jk>.
193    * @return This object.
194    */
195   public OpenApi addServers(Server...values) {
196      if (nn(values))
197         for (var v : values)
198            if (nn(v))
199               servers.add(v);
200      return this;
201   }
202
203   /**
204    * Bean property appender:  <property>tags</property>.
205    *
206    * <p>
207    * A list of tags used by the specification with additional metadata.
208    *
209    * @param values
210    *    The values to add to this property.
211    *    <br>Ignored if <jk>null</jk>.
212    * @return This object.
213    */
214   public OpenApi addTags(Collection<Tag> values) {
215      if (nn(values))
216         tags.addAll(values);
217      return this;
218   }
219
220   /**
221    * Bean property appender:  <property>tags</property>.
222    *
223    * <p>
224    * A list of tags used by the specification with additional metadata.
225    *
226    * @param values
227    *    The values to add to this property.
228    *    <br>Ignored if <jk>null</jk>.
229    * @return This object.
230    */
231   public OpenApi addTags(Tag...values) {
232      if (nn(values))
233         for (var v : values)
234            if (nn(v))
235               tags.add(v);
236      return this;
237   }
238
239   /**
240    * Make a deep copy of this object.
241    *
242    * @return A deep copy of this object.
243    */
244   public OpenApi copy() {
245      return new OpenApi(this);
246   }
247
248   /**
249    * Finds a reference within this OpenAPI document.
250    *
251    * @param ref The reference string (e.g., <js>"#/components/schemas/User"</js>).  Must not be <jk>null</jk> or blank.
252    * @param c The expected class type.  Must not be <jk>null</jk>.
253    * @return The referenced node, or <jk>null</jk> if not found.
254    */
255   public <T> T findRef(String ref, Class<T> c) {
256      assertArgNotNullOrBlank("ref", ref);
257      assertArgNotNull("c", c);
258      if (! ref.startsWith("#/"))
259         throw rex("Unsupported reference:  ''{0}''", ref);
260      try {
261         return new ObjectRest(this).get(ref.substring(1), c);
262      } catch (Exception e) {
263         throw bex(e, c, "Reference ''{0}'' could not be converted to type ''{1}''.", ref, cn(c));
264      }
265   }
266
267   @Override /* Overridden from OpenApiElement */
268   public <T> T get(String property, Class<T> type) {
269      assertArgNotNull("property", property);
270      return switch (property) {
271         case "openapi" -> toType(getOpenapi(), type);
272         case "info" -> toType(getInfo(), type);
273         case "servers" -> toType(getServers(), type);
274         case "paths" -> toType(getPaths(), type);
275         case "components" -> toType(getComponents(), type);
276         case "security" -> toType(getSecurity(), type);
277         case "tags" -> toType(getTags(), type);
278         case "externalDocs" -> toType(getExternalDocs(), type);
279         default -> super.get(property, type);
280      };
281   }
282
283   /**
284    * Returns the components object.
285    *
286    * @return The components object.
287    */
288   public Components getComponents() { return components; }
289
290   /**
291    * Returns the external documentation.
292    *
293    * @return The external documentation.
294    */
295   public ExternalDocumentation getExternalDocs() { return externalDocs; }
296
297   /**
298    * Returns the info object.
299    *
300    * @return The info object.
301    */
302   public Info getInfo() { return info; }
303
304   /**
305    * Returns the OpenAPI version.
306    *
307    * @return The OpenAPI version.
308    */
309   public String getOpenapi() { return openapi; }
310
311   /**
312    * Returns the paths map.
313    *
314    * @return The paths map.
315    */
316   public Map<String,PathItem> getPaths() { return paths; }
317
318   /**
319    * Returns the security requirements list.
320    *
321    * @return The security requirements list.
322    */
323   public List<SecurityRequirement> getSecurity() { return nullIfEmpty(security); }
324
325   /**
326    * Returns the servers list.
327    *
328    * @return The servers list.
329    */
330   public List<Server> getServers() { return nullIfEmpty(servers); }
331
332   /**
333    * Returns the tags list.
334    *
335    * @return The tags list.
336    */
337   public List<Tag> getTags() { return nullIfEmpty(tags); }
338
339   @Override /* Overridden from OpenApiElement */
340   public Set<String> keySet() {
341      // @formatter:off
342      var s = setb(String.class)
343         .addIf(nn(components), "components")
344         .addIf(nn(externalDocs), "externalDocs")
345         .addIf(nn(info), "info")
346         .addIf(nn(openapi), "openapi")
347         .addIf(nn(paths), "paths")
348         .addIf(ne(security), "security")
349         .addIf(ne(servers), "servers")
350         .addIf(ne(tags), "tags")
351         .build();
352      // @formatter:on
353      return new MultiSet<>(s, super.keySet());
354   }
355
356   @Override /* Overridden from OpenApiElement */
357   public OpenApi set(String property, Object value) {
358      assertArgNotNull("property", property);
359      return switch (property) {
360         case "components" -> setComponents(toType(value, Components.class));
361         case "externalDocs" -> setExternalDocs(toType(value, ExternalDocumentation.class));
362         case "info" -> setInfo(toType(value, Info.class));
363         case "openapi" -> setOpenapi(s(value));
364         case "paths" -> setPaths(toMapBuilder(value, String.class, PathItem.class).sparse().build());
365         case "security" -> setSecurity(listb(SecurityRequirement.class).addAny(value).sparse().build());
366         case "servers" -> setServers(listb(Server.class).addAny(value).sparse().build());
367         case "tags" -> setTags(listb(Tag.class).addAny(value).sparse().build());
368         default -> {
369            super.set(property, value);
370            yield this;
371         }
372      };
373   }
374
375   /**
376    * Sets the components object.
377    *
378    * @param value The new value for this property.
379    * @return This object.
380    */
381   public OpenApi setComponents(Components value) {
382      components = value;
383      return this;
384   }
385
386   /**
387    * Sets the external documentation.
388    *
389    * @param value The new value for this property.
390    * @return This object.
391    */
392   public OpenApi setExternalDocs(ExternalDocumentation value) {
393      externalDocs = value;
394      return this;
395   }
396
397   /**
398    * Sets the info object.
399    *
400    * @param value The new value for this property.
401    * @return This object.
402    */
403   public OpenApi setInfo(Info value) {
404      info = value;
405      return this;
406   }
407
408   /**
409    * Sets the OpenAPI version.
410    *
411    * @param value The new value for this property.
412    * @return This object.
413    */
414   public OpenApi setOpenapi(String value) {
415      openapi = value;
416      return this;
417   }
418
419   /**
420    * Sets the paths map.
421    *
422    * @param value The new value for this property.
423    * @return This object.
424    */
425   public OpenApi setPaths(Map<String,PathItem> value) {
426      this.paths = toMapBuilder(value, String.class, PathItem.class).sparse().sorted(PATH_COMPARATOR).build();
427      return this;
428   }
429
430   /**
431    * Sets the security requirements list.
432    *
433    * @param value The new value for this property.
434    * @return This object.
435    */
436   public OpenApi setSecurity(List<SecurityRequirement> value) {
437      security.clear();
438      if (nn(value))
439         security.addAll(value);
440      return this;
441   }
442
443   /**
444    * Sets the servers list.
445    *
446    * @param value The new value for this property.
447    * @return This object.
448    */
449   public OpenApi setServers(List<Server> value) {
450      servers.clear();
451      if (nn(value))
452         servers.addAll(value);
453      return this;
454   }
455
456   /**
457    * Sets the tags list.
458    *
459    * @param value The new value for this property.
460    * @return This object.
461    */
462   public OpenApi setTags(List<Tag> value) {
463      tags.clear();
464      if (nn(value))
465         tags.addAll(value);
466      return this;
467   }
468
469   /**
470    * Bean property setter:  <property>tags</property>.
471    *
472    * <p>
473    * A list of tags used by the specification with additional metadata.
474    *
475    * @param value
476    *    The new value for this property.
477    *    <br>Ignored if <jk>null</jk>.
478    * @return This object.
479    */
480   public OpenApi setTags(Tag...value) {
481      setTags(listb(Tag.class).add(value).sparse().build());
482      return this;
483   }
484
485   @Override /* Overridden from OpenApiElement */
486   public OpenApi strict() {
487      super.strict();
488      return this;
489   }
490
491   @Override /* Overridden from OpenApiElement */
492   public OpenApi strict(Object value) {
493      super.strict(value);
494      return this;
495   }
496
497   @Override
498   public String toString() {
499      return JsonSerializer.DEFAULT.toString(this);
500   }
501}