001package io.avaje.http.generator.core.openapi;
002
003import com.fasterxml.jackson.annotation.JsonInclude;
004import com.fasterxml.jackson.databind.DeserializationFeature;
005import com.fasterxml.jackson.databind.ObjectMapper;
006import com.fasterxml.jackson.databind.SerializationFeature;
007import io.swagger.v3.oas.annotations.OpenAPIDefinition;
008import io.swagger.v3.oas.annotations.tags.Tag;
009import io.swagger.v3.oas.annotations.tags.Tags;
010import io.swagger.v3.oas.models.Components;
011import io.swagger.v3.oas.models.OpenAPI;
012import io.swagger.v3.oas.models.Operation;
013import io.swagger.v3.oas.models.PathItem;
014import io.swagger.v3.oas.models.Paths;
015import io.swagger.v3.oas.models.info.Info;
016import io.swagger.v3.oas.models.media.Content;
017import io.swagger.v3.oas.models.media.Schema;
018
019import javax.annotation.processing.Filer;
020import javax.annotation.processing.Messager;
021import javax.annotation.processing.ProcessingEnvironment;
022import javax.lang.model.element.Element;
023import javax.lang.model.element.TypeElement;
024import javax.lang.model.type.TypeMirror;
025import javax.lang.model.util.Elements;
026import javax.tools.Diagnostic;
027import javax.tools.FileObject;
028import javax.tools.StandardLocation;
029import java.io.IOException;
030import java.io.Writer;
031import java.util.Map;
032import java.util.TreeMap;
033
034/**
035 * Context for building the OpenAPI documentation.
036 */
037public class DocContext {
038
039  private final boolean openApiAvailable;
040
041  private final Elements elements;
042
043  private final Filer filer;
044
045  private final Messager messager;
046
047  private final Map<String, PathItem> pathMap = new TreeMap<>();
048
049  private final SchemaDocBuilder schemaBuilder;
050
051  private final OpenAPI openAPI;
052
053  public DocContext(ProcessingEnvironment env, boolean openApiAvailable) {
054    this.openApiAvailable = openApiAvailable;
055    this.elements = env.getElementUtils();
056    this.filer = env.getFiler();
057    this.messager = env.getMessager();
058    this.schemaBuilder = new SchemaDocBuilder(env.getTypeUtils(), env.getElementUtils());
059    this.openAPI = initOpenAPI();
060  }
061
062  public boolean isOpenApiAvailable() {
063    return openApiAvailable;
064  }
065
066  private OpenAPI initOpenAPI() {
067
068    OpenAPI openAPI = new OpenAPI();
069    openAPI.setPaths(new Paths());
070
071    Info info = new Info();
072    info.setTitle("");
073    info.setVersion("");
074    openAPI.setInfo(info);
075
076    return openAPI;
077  }
078
079  Schema toSchema(String rawType, Element element) {
080    TypeElement typeElement = elements.getTypeElement(rawType);
081    if (typeElement == null) {
082      // primitive types etc
083      return schemaBuilder.toSchema(element.asType());
084    } else {
085      return schemaBuilder.toSchema(typeElement.asType());
086    }
087  }
088
089  Content createContent(TypeMirror returnType, String mediaType) {
090    return schemaBuilder.createContent(returnType, mediaType);
091  }
092
093  PathItem pathItem(String fullPath) {
094    return pathMap.computeIfAbsent(fullPath, s -> new PathItem());
095  }
096
097  void addFormParam(Operation operation, String varName, Schema schema) {
098    schemaBuilder.addFormParam(operation, varName, schema);
099  }
100
101  void addRequestBody(Operation operation, Schema schema, boolean asForm, String description) {
102    schemaBuilder.addRequestBody(operation, schema, asForm, description);
103  }
104
105  /**
106   * Return the OpenAPI adding the paths and schemas.
107   */
108  private OpenAPI getApiForWriting() {
109
110    Paths paths = openAPI.getPaths();
111    if (paths == null) {
112      paths = new Paths();
113      openAPI.setPaths(paths);
114    }
115    // add paths by natural order
116    for (Map.Entry<String, PathItem> entry : pathMap.entrySet()) {
117      paths.addPathItem(entry.getKey(), entry.getValue());
118    }
119
120    components().setSchemas(schemaBuilder.getSchemas());
121    return openAPI;
122  }
123
124  /**
125   * Return the components creating if needed.
126   */
127  private Components components() {
128    Components components = openAPI.getComponents();
129    if (components == null) {
130      components = new Components();
131      openAPI.setComponents(components);
132    }
133    return components;
134  }
135
136  private io.swagger.v3.oas.models.tags.Tag createTagItem(Tag tag){
137    io.swagger.v3.oas.models.tags.Tag tagsItem = new io.swagger.v3.oas.models.tags.Tag();
138    tagsItem.setName(tag.name());
139    tagsItem.setDescription(tag.description());
140    // tagsItem.setExtensions(tag.extensions());  # Not sure about the extensions
141    // tagsItem.setExternalDocs(tag.externalDocs()); # Not sure about the external docs
142    return tagsItem;
143  }
144
145  public void addTagsDefinition(Element element) {
146    Tags tags = element.getAnnotation(Tags.class);
147    if(tags == null)
148      return;
149
150    for(Tag tag: tags.value()){
151      openAPI.addTagsItem(createTagItem(tag));
152    }
153  }
154
155  public void addTagDefinition(Element element){
156    Tag tag = element.getAnnotation(Tag.class);
157    if(tag == null)
158      return;
159
160    openAPI.addTagsItem(createTagItem(tag));
161  }
162
163  public void readApiDefinition(Element element) {
164
165    OpenAPIDefinition openApi = element.getAnnotation(OpenAPIDefinition.class);
166    io.swagger.v3.oas.annotations.info.Info info = openApi.info();
167    if (!info.title().isEmpty()) {
168      openAPI.getInfo().setTitle(info.title());
169    }
170    if (!info.description().isEmpty()) {
171      openAPI.getInfo().setDescription(info.description());
172    }
173    if (!info.version().isEmpty()) {
174      openAPI.getInfo().setVersion(info.version());
175    }
176
177  }
178
179  public void writeApi() {
180    try (Writer metaWriter = createMetaWriter()) {
181
182      OpenAPI openAPI = getApiForWriting();
183      ObjectMapper mapper = createObjectMapper();
184      mapper.writeValue(metaWriter, openAPI);
185
186    } catch (IOException e) {
187      logError(null, "Error writing openapi file" + e.getMessage());
188      e.printStackTrace();
189    }
190  }
191
192  private ObjectMapper createObjectMapper() {
193
194    ObjectMapper mapper = new ObjectMapper();
195    mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
196      .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
197      .enable(SerializationFeature.INDENT_OUTPUT);
198
199    return mapper;
200  }
201
202  private Writer createMetaWriter() throws IOException {
203    FileObject writer = filer.createResource(StandardLocation.CLASS_OUTPUT, "meta", "openapi.json", null);
204    return writer.openWriter();
205  }
206
207  private void logError(Element e, String msg, Object... args) {
208    messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
209  }
210
211}