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