001package io.avaje.http.generator.core;
002
003import io.avaje.http.api.Path;
004import io.avaje.http.api.Produces;
005import io.swagger.v3.oas.annotations.Hidden;
006
007import javax.lang.model.element.Element;
008import javax.lang.model.element.ElementKind;
009import javax.lang.model.element.ExecutableElement;
010import javax.lang.model.element.TypeElement;
011import javax.lang.model.type.DeclaredType;
012import javax.lang.model.type.ExecutableType;
013import javax.lang.model.type.TypeKind;
014import javax.lang.model.type.TypeMirror;
015import javax.lang.model.util.ElementFilter;
016import javax.validation.Valid;
017import java.lang.annotation.Annotation;
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Set;
021import java.util.TreeSet;
022
023/**
024 * Reads the type information for the Controller (bean).
025 */
026public class ControllerReader {
027
028  private final ProcessingContext ctx;
029
030  private final TypeElement beanType;
031
032  private final List<Element> interfaces;
033
034  private final List<ExecutableElement> interfaceMethods;
035
036  private final List<String> roles;
037
038  private final List<MethodReader> methods = new ArrayList<>();
039
040  private final Set<String> staticImportTypes = new TreeSet<>();
041
042  private final Set<String> importTypes = new TreeSet<>();
043
044  /**
045   * The produces media type for the controller. Null implies JSON.
046   */
047  private final String produces;
048
049  private final boolean includeValidator;
050
051  /**
052   * Flag set when the controller is dependant on a request scope type.
053   */
054  private boolean requestScope;
055
056  private boolean docHidden;
057
058  ControllerReader(TypeElement beanType, ProcessingContext ctx) {
059    this.beanType = beanType;
060    this.ctx = ctx;
061    this.interfaces = initInterfaces();
062    this.interfaceMethods = initInterfaceMethods();
063    this.roles = Util.findRoles(beanType);
064    importTypes.add(Constants.GENERATED);
065    if (ctx.isOpenApiAvailable()) {
066      docHidden = initDocHidden();
067    }
068    includeValidator = initIncludeValidator();
069    importTypes.add(Constants.SINGLETON);
070    importTypes.add(Constants.IMPORT_CONTROLLER);
071    importTypes.add(beanType.getQualifiedName().toString());
072    if (includeValidator) {
073      importTypes.add(Constants.VALIDATOR);
074    }
075    this.produces = initProduces();
076  }
077
078  private List<Element> initInterfaces() {
079    List<Element> interfaces = new ArrayList<>();
080    for (TypeMirror anInterface : beanType.getInterfaces()) {
081      final Element ifaceElement = ctx.asElement(anInterface);
082      if (ifaceElement.getAnnotation(Path.class) != null) {
083        interfaces.add(ifaceElement);
084      }
085    }
086    return interfaces;
087  }
088
089  private List<ExecutableElement> initInterfaceMethods() {
090    List<ExecutableElement> ifaceMethods = new ArrayList<>();
091    for (Element anInterface : interfaces) {
092      ifaceMethods.addAll(ElementFilter.methodsIn(anInterface.getEnclosedElements()));
093    }
094    return ifaceMethods;
095  }
096
097  private <A extends Annotation> A findAnnotation(Class<A> type) {
098    A annotation = beanType.getAnnotation(type);
099    if (annotation != null) {
100      return annotation;
101    }
102    for (Element anInterface : interfaces) {
103      annotation = anInterface.getAnnotation(type);
104      if (annotation != null) {
105        return annotation;
106      }
107    }
108    return null;
109  }
110
111  <A extends Annotation> A findMethodAnnotation(Class<A> type, ExecutableElement element) {
112    for (ExecutableElement interfaceMethod : interfaceMethods) {
113      if (matchMethod(interfaceMethod, element)) {
114        final A annotation = interfaceMethod.getAnnotation(type);
115        if (annotation != null) {
116          return annotation;
117        }
118      }
119    }
120    return null;
121  }
122
123  private boolean matchMethod(ExecutableElement interfaceMethod, ExecutableElement element) {
124    return interfaceMethod.toString().equals(element.toString());
125  }
126
127  private String initProduces() {
128    final Produces produces = findAnnotation(Produces.class);
129    return (produces == null) ? null : produces.value();
130  }
131
132  private boolean initDocHidden() {
133    return findAnnotation(Hidden.class) != null;
134  }
135
136  private boolean initIncludeValidator() {
137    return findAnnotation(Valid.class) != null;
138  }
139
140  String getProduces() {
141    return produces;
142  }
143
144  TypeElement getBeanType() {
145    return beanType;
146  }
147
148  public boolean isDocHidden() {
149    return docHidden;
150  }
151
152  public boolean isIncludeValidator() {
153    return includeValidator;
154  }
155
156  /**
157   * Return true if the controller has request scoped dependencies.
158   * In that case a BeanFactory will have been generated.
159   */
160  boolean isRequestScoped() {
161    return requestScope;
162  }
163
164  void read() {
165    if (!roles.isEmpty()) {
166      ctx.platform().controllerRoles(roles, this);
167    }
168    for (Element element : beanType.getEnclosedElements()) {
169      if (element.getKind() == ElementKind.METHOD) {
170        readMethod((ExecutableElement) element);
171      } else if (element.getKind() == ElementKind.FIELD) {
172        readField(element);
173      }
174    }
175    readSuper(beanType);
176  }
177
178  private void readField(Element element) {
179    if (!requestScope) {
180      final String rawType = element.asType().toString();
181      requestScope = RequestScopeTypes.isRequestType(rawType);
182    }
183  }
184
185  /**
186   * Read methods from superclasses taking into account generics.
187   */
188  private void readSuper(TypeElement beanType) {
189    TypeMirror superclass = beanType.getSuperclass();
190    if (superclass.getKind() != TypeKind.NONE) {
191      DeclaredType declaredType = (DeclaredType) superclass;
192      final Element superElement = ctx.asElement(superclass);
193      if (!"java.lang.Object".equals(superElement.toString())) {
194        for (Element element : superElement.getEnclosedElements()) {
195          if (element.getKind() == ElementKind.METHOD) {
196            readMethod((ExecutableElement) element, declaredType);
197          } else if (element.getKind() == ElementKind.FIELD) {
198            readField(element);
199          }
200        }
201        if (superElement instanceof TypeElement) {
202          readSuper((TypeElement) superElement);
203        }
204      }
205    }
206  }
207
208  private void readMethod(ExecutableElement element) {
209    readMethod(element, null);
210  }
211
212  private void readMethod(ExecutableElement method, DeclaredType declaredType) {
213
214    ExecutableType actualExecutable = null;
215    if (declaredType != null) {
216      // actual taking into account generics
217      actualExecutable = (ExecutableType) ctx.asMemberOf(declaredType, method);
218    }
219
220    MethodReader methodReader = new MethodReader(this, method, actualExecutable, ctx);
221    if (methodReader.isWebMethod()) {
222      methodReader.read();
223      methods.add(methodReader);
224    }
225  }
226
227  public List<String> getRoles() {
228    return roles;
229  }
230
231  public List<MethodReader> getMethods() {
232    return methods;
233  }
234
235  public String getPath() {
236    Path path = findAnnotation(Path.class);
237    if (path == null) {
238      return null;
239    }
240    return Util.trimPath(path.value());
241  }
242
243  public void addImportType(String rawType) {
244    importTypes.add(rawType);
245  }
246
247  public void addStaticImportType(String rawType) {
248    staticImportTypes.add(rawType);
249  }
250
251  public Set<String> getStaticImportTypes() {
252    return staticImportTypes;
253  }
254
255  public Set<String> getImportTypes() {
256    return importTypes;
257  }
258
259}