001package com.mrivanplays.annotationconfig.core.internal;
002
003import com.mrivanplays.annotationconfig.core.AnnotationType;
004import com.mrivanplays.annotationconfig.core.Comment;
005import com.mrivanplays.annotationconfig.core.Comments;
006import com.mrivanplays.annotationconfig.core.CustomAnnotationRegistry;
007import com.mrivanplays.annotationconfig.core.CustomAnnotationRegistry.AnnotationResolver;
008import com.mrivanplays.annotationconfig.core.CustomAnnotationRegistry.AnnotationResolverContext;
009import com.mrivanplays.annotationconfig.core.CustomAnnotationRegistry.AnnotationWriter;
010import com.mrivanplays.annotationconfig.core.CustomAnnotationRegistry.AnnotationWriter.WriteFunction;
011import com.mrivanplays.annotationconfig.core.FieldTypeResolver;
012import com.mrivanplays.annotationconfig.core.Key;
013import com.mrivanplays.annotationconfig.core.TypeResolver;
014import java.io.File;
015import java.io.FileWriter;
016import java.io.IOException;
017import java.io.PrintWriter;
018import java.lang.annotation.Annotation;
019import java.lang.reflect.Field;
020import java.lang.reflect.InvocationTargetException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.stream.Collectors;
029
030public final class AnnotatedConfigResolver {
031
032  public static List<Map.Entry<AnnotationHolder, List<AnnotationType>>> resolveAnnotations(
033      Object annotatedClass, CustomAnnotationRegistry annoRegistry, boolean reverseFields) {
034    Map<AnnotationHolder, List<AnnotationType>> annotationData = new HashMap<>();
035    AnnotationHolder CLASS_ANNOTATION_HOLDER = new AnnotationHolder();
036    Class<?> theClass = annotatedClass.getClass();
037    for (Annotation annotation : theClass.getAnnotations()) {
038      AnnotationType annotationType =
039          AnnotationType.match(annotation.annotationType(), annoRegistry);
040      populate(CLASS_ANNOTATION_HOLDER, annotationType, annotationData);
041    }
042
043    List<Field> fields = Arrays.asList(theClass.getDeclaredFields());
044    if (reverseFields) {
045      Collections.reverse(fields);
046    }
047
048    int order = 1;
049    for (Field field : fields) {
050      if (field.getDeclaredAnnotations().length > 0) {
051        AnnotationHolder holder = new AnnotationHolder(field, order);
052        for (Annotation annotation : field.getDeclaredAnnotations()) {
053          AnnotationType annotationType =
054              AnnotationType.match(annotation.annotationType(), annoRegistry);
055          populate(holder, annotationType, annotationData);
056        }
057        order++;
058      }
059    }
060    return annotationData.entrySet().stream()
061        .sorted(Map.Entry.comparingByKey())
062        .collect(Collectors.toCollection(LinkedList::new));
063  }
064
065  public static void dump(
066      Object annotatedConfig,
067      List<Map.Entry<AnnotationHolder, List<AnnotationType>>> map,
068      File file,
069      String commentChar,
070      ValueWriter valueWriter,
071      CustomAnnotationRegistry annotationRegistry,
072      Class<?> configType,
073      boolean reverseFields) {
074    try {
075      file.createNewFile();
076      try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
077        toWriter(
078            annotatedConfig,
079            writer,
080            map,
081            commentChar,
082            annotationRegistry,
083            configType,
084            valueWriter,
085            false,
086            null,
087            reverseFields);
088      }
089    } catch (IOException e) {
090      throw new RuntimeException(e);
091    }
092  }
093
094  public interface ValueWriter {
095
096    void write(String key, Object value, PrintWriter writer) throws IOException;
097
098    void writeCustom(Object value, PrintWriter writer, String annoName) throws IOException;
099  }
100
101  private static void toWriter(
102      Object annotatedConfig,
103      PrintWriter writer,
104      List<Map.Entry<AnnotationHolder, List<AnnotationType>>> map,
105      String commentChar,
106      CustomAnnotationRegistry annotationRegistry,
107      Class<?> configType,
108      ValueWriter valueWriter,
109      boolean isSection,
110      String sectionKey,
111      boolean reverseFields)
112      throws IOException {
113    Map<String, Object> toWrite = null;
114    if (isSection) {
115      toWrite = new HashMap<>();
116    }
117    for (Map.Entry<AnnotationHolder, List<AnnotationType>> entry : map) {
118      AnnotationHolder holder = entry.getKey();
119      if (holder.isClass()) {
120        Class<?> theClass = annotatedConfig.getClass();
121        for (AnnotationType type : entry.getValue()) {
122          handleComments(type, null, theClass, commentChar, writer);
123        }
124        if (!isSection) {
125          writer.append('\n');
126        }
127      } else {
128        Field field = holder.getField();
129        field.setAccessible(true);
130        List<String> comments = new ArrayList<>();
131        String keyName = field.getName();
132        AnnotationResolver annoResolver = null;
133        Class<? extends Annotation> customAnnotationType = null;
134        boolean configObject = false;
135        for (AnnotationType type : entry.getValue()) {
136          if (type.is(AnnotationType.COMMENT)) {
137            comments.add(field.getDeclaredAnnotation(Comment.class).value());
138          }
139          if (type.is(AnnotationType.COMMENTS)) {
140            for (Comment comment : field.getDeclaredAnnotation(Comments.class).value()) {
141              comments.add(comment.value());
142            }
143          }
144          if (type.is(AnnotationType.KEY)) {
145            keyName = field.getDeclaredAnnotation(Key.class).value();
146          }
147          if (type.is(AnnotationType.CONFIG_OBJECT)) {
148            configObject = true;
149            try {
150              Object section = field.get(annotatedConfig);
151              if (section == null) {
152                throw new IllegalArgumentException(
153                    "Section not initialized for field '" + field.getName() + "'");
154              }
155              toWriter(
156                  section,
157                  writer,
158                  resolveAnnotations(section, annotationRegistry, reverseFields),
159                  commentChar,
160                  annotationRegistry,
161                  configType,
162                  valueWriter,
163                  true,
164                  keyName,
165                  reverseFields);
166            } catch (IllegalAccessException e) {
167              throw new IllegalArgumentException(
168                  "Could not config object value; field became inaccessible");
169            }
170          }
171          if (AnnotationType.isCustom(type)) {
172            if (annoResolver != null) {
173              throw new IllegalArgumentException("A field can only have 1 custom annotation.");
174            }
175            annoResolver = annotationRegistry.registry().get(type);
176            customAnnotationType = type.getRawType();
177          }
178        }
179        if (configObject) {
180          continue;
181        }
182        try {
183          Object defaultsToValueObject = field.get(annotatedConfig);
184          if (defaultsToValueObject == null) {
185            throw new IllegalArgumentException(
186                "No default value for field '" + field.getName() + "'");
187          }
188          if (!isSection) {
189            for (String comment : comments) {
190              writer.println(commentChar + comment);
191            }
192          }
193          if (annoResolver == null) {
194            if (!isSection) {
195              valueWriter.write(keyName, defaultsToValueObject, writer);
196            } else {
197              toWrite.put(keyName, defaultsToValueObject);
198            }
199          } else {
200            Annotation annotation = field.getDeclaredAnnotation(customAnnotationType);
201            AnnotationWriter annotationWriter = new AnnotationWriter();
202            AnnotationResolverContext context =
203                new AnnotationResolverContext(
204                    configType, field, annotatedConfig, defaultsToValueObject, keyName, isSection);
205            annoResolver.write(annotationWriter, customAnnotationType.cast(annotation), context);
206            for (Map.Entry<WriteFunction, Object> writeEntry :
207                annotationWriter.toWrite().entrySet()) {
208              handleCustomEntry(writeEntry, valueWriter, writer, annotation.annotationType().getSimpleName());
209            }
210          }
211        } catch (IllegalAccessException e) {
212          throw new IllegalArgumentException("lost access to field '" + field.getName() + "'");
213        }
214      }
215    }
216
217    if (isSection) {
218      valueWriter.write(sectionKey, toWrite, writer);
219    }
220  }
221
222  public static void setFields(
223      Object annotatedConfig,
224      Map<String, Object> values,
225      List<Map.Entry<AnnotationHolder, List<AnnotationType>>> map,
226      CustomAnnotationRegistry annoRegistry,
227      String commentChar,
228      ValueWriter valueWriter,
229      File file,
230      boolean shouldGenerateNonExistentFields,
231      boolean reverseFields,
232      Class<?> configType) {
233    for (Map.Entry<AnnotationHolder, List<AnnotationType>> entry : map) {
234      AnnotationHolder holder = entry.getKey();
235      if (holder.isClass()) {
236        continue;
237      }
238      Field field = holder.getField();
239      field.setAccessible(true);
240      Class<? extends FieldTypeResolver> typeResolver = null;
241      FieldTypeResolver constructedTypeResolver = null;
242      String keyName = field.getName();
243      boolean configObject = false;
244      Object section = null;
245      for (AnnotationType type : entry.getValue()) {
246        if (AnnotationType.isCustom(type)) {
247          if (constructedTypeResolver != null) {
248            throw new IllegalArgumentException("A field can only have 1 custom annotation");
249          }
250          constructedTypeResolver = getResolver(annoRegistry, type).typeResolver().get();
251        }
252        if (type.is(AnnotationType.TYPE_RESOLVER)) {
253          typeResolver = field.getDeclaredAnnotation(TypeResolver.class).value();
254        }
255        if (type.is(AnnotationType.KEY)) {
256          keyName = field.getDeclaredAnnotation(Key.class).value();
257        }
258        if (type.is(AnnotationType.CONFIG_OBJECT)) {
259          configObject = true;
260          try {
261            section = field.get(annotatedConfig);
262          } catch (IllegalAccessException e) {
263            throw new IllegalArgumentException(
264                "Could not get config object from annotated config '"
265                    + annotatedConfig.getClass().getSimpleName()
266                    + "'");
267          }
268        }
269      }
270      Class<?> fieldType = field.getType();
271      Object value = values.get(keyName);
272      if (value == null) {
273        if (shouldGenerateNonExistentFields) {
274          if (configObject) {
275            // config sections/objects we're not going to generate
276            continue;
277          }
278          AnnotationResolver annotationResolver = null;
279          Class<? extends Annotation> customAnnotationType = null;
280          try (PrintWriter writer = new PrintWriter(new FileWriter(file, true))) {
281            for (AnnotationType type : entry.getValue()) {
282              handleComments(type, field, null, commentChar, writer);
283              if (AnnotationType.isCustom(type)) {
284                if (annotationResolver != null) {
285                  throw new IllegalArgumentException("A field can only have 1 custom annotation");
286                }
287                annotationResolver = getResolver(annoRegistry, type);
288                customAnnotationType = type.getRawType();
289              }
290            }
291            Object def = field.get(annotatedConfig);
292            if (annotationResolver == null) {
293              valueWriter.write(keyName, def, writer);
294            } else {
295              Annotation annotation = field.getDeclaredAnnotation(customAnnotationType);
296              AnnotationWriter annotationWriter = new AnnotationWriter();
297              AnnotationResolverContext context =
298                  new AnnotationResolverContext(
299                      configType, field, annotatedConfig, def, keyName, false);
300              annotationResolver.write(
301                  annotationWriter, customAnnotationType.cast(annotation), context);
302              for (Map.Entry<WriteFunction, Object> writeEntry :
303                  annotationWriter.toWrite().entrySet()) {
304                handleCustomEntry(writeEntry, valueWriter, writer, annotation.annotationType().getSimpleName());
305              }
306            }
307          } catch (IOException e) {
308            throw new RuntimeException(e);
309          } catch (IllegalAccessException e) {
310            throw new IllegalArgumentException("Field became inaccessible");
311          }
312        }
313        continue;
314      }
315      if (configObject) {
316        if (section == null) {
317          throw new IllegalArgumentException(
318              "Non initialized config object found in annotated config '"
319                  + annotatedConfig.getClass().getSimpleName()
320                  + "'");
321        }
322        if (!(value instanceof Map)) {
323          // sections not supported, continue on
324          continue;
325        }
326        setFields(
327            section,
328            (Map<String, Object>) value,
329            resolveAnnotations(section, annoRegistry, reverseFields),
330            annoRegistry,
331            commentChar,
332            valueWriter,
333            file,
334            shouldGenerateNonExistentFields,
335            reverseFields,
336            configType);
337        continue;
338      }
339      if (typeResolver != null) {
340        try {
341          FieldTypeResolver resolver = typeResolver.getDeclaredConstructor().newInstance();
342          if (!resolver.shouldResolve(fieldType)) {
343            throw new IllegalArgumentException(
344                "Invalid type resolver found for \"" + field.getName() + "\"");
345          }
346          Object resolvedValue = resolver.toType(value, field);
347          field.set(annotatedConfig, resolvedValue);
348        } catch (InstantiationException e) {
349          throw new IllegalArgumentException("Could not construct a type resolver");
350        } catch (IllegalAccessException e) {
351          throw new IllegalArgumentException(
352              "Could not construct a type resolver/set a field's value; field/constructor not accessible anymore");
353        } catch (InvocationTargetException e) {
354          throw new IllegalArgumentException(
355              "Could not construct a type resolver; constructor rejected execution");
356        } catch (NoSuchMethodException e) {
357          throw new IllegalArgumentException(
358              "Cannot find a no args constructor for field type resolver \""
359                  + field.getName()
360                  + "\"");
361        } catch (Exception e) {
362          throw new IllegalArgumentException(
363              "Could not resolve to type argument \"" + field.getName() + "\": " + e.getMessage(),
364              e);
365        }
366      } else {
367        if (constructedTypeResolver != null) {
368          try {
369            if (!constructedTypeResolver.shouldResolve(fieldType)) {
370              throw new IllegalArgumentException(
371                  "Invalid type resolver found for \"" + field.getName() + "\"");
372            }
373            Object resolvedValue = constructedTypeResolver.toType(value, field);
374            field.set(annotatedConfig, resolvedValue);
375          } catch (IllegalAccessException e) {
376            throw new IllegalArgumentException(
377                "Could not set field \""
378                    + field.getName()
379                    + "\" value; field not accessible anymore");
380          } catch (Exception e) {
381            throw new IllegalArgumentException(
382                "Could not resolve to type argument \"" + field.getName() + "\": " + e.getMessage(),
383                e);
384          }
385        } else {
386          // either I'm really tired or the only way we can check if the type is primitive type is
387          // to do this spaghetti
388          try {
389            if (fieldType.isAssignableFrom(boolean.class)
390                || fieldType.isAssignableFrom(Boolean.class)) {
391              field.set(annotatedConfig, Boolean.parseBoolean(String.valueOf(value)));
392            }
393            if (fieldType.isAssignableFrom(String.class)) {
394              field.set(annotatedConfig, String.valueOf(value));
395            }
396            if (fieldType.isAssignableFrom(byte.class) || fieldType.isAssignableFrom(Byte.class)) {
397              field.set(annotatedConfig, Byte.valueOf(String.valueOf(value)));
398            }
399            if (fieldType.isAssignableFrom(int.class)
400                || fieldType.isAssignableFrom(Integer.class)) {
401              field.set(annotatedConfig, Integer.valueOf(String.valueOf(value)));
402            }
403            if (fieldType.isAssignableFrom(double.class)
404                || fieldType.isAssignableFrom(Double.class)) {
405              field.set(annotatedConfig, Double.valueOf(String.valueOf(value)));
406            }
407            if (fieldType.isAssignableFrom(float.class)
408                || fieldType.isAssignableFrom(Float.class)) {
409              field.set(annotatedConfig, Float.valueOf(String.valueOf(value)));
410            }
411          } catch (IllegalAccessException e) {
412            throw new IllegalArgumentException(
413                "Could not set field \""
414                    + field.getName()
415                    + "\" value; field not accessible anymore");
416          }
417        }
418      }
419    }
420  }
421
422  private static AnnotationResolver<? extends Annotation> getResolver(
423      CustomAnnotationRegistry annoRegistry, AnnotationType type) {
424    if (annoRegistry == null) {
425      throw new IllegalArgumentException(
426          "Could not resolve custom annotation type '"
427              + type.getRawType().getSimpleName()
428              + "' ; registry not available.");
429    }
430    AnnotationResolver<? extends Annotation> resolver = annoRegistry.registry().get(type);
431    if (resolver == null) {
432      throw new IllegalArgumentException(
433          "Could not resolve custom annotation type '"
434              + type.getRawType().getSimpleName()
435              + "' ; unregistered.");
436    }
437    return resolver;
438  }
439
440  private static void handleCustomEntry(
441      Map.Entry<WriteFunction, Object> writeEntry,
442      ValueWriter valueWriter,
443      PrintWriter writer,
444      String annotationName)
445      throws IOException {
446    WriteFunction func = writeEntry.getKey();
447    Object written = writeEntry.getValue();
448    switch (func) {
449      case WRITE:
450        valueWriter.writeCustom(written, writer, annotationName);
451        break;
452      case APPEND:
453        // but why this check when we have method only for appending character ?
454        // this is here to prevent people who use the non intended for api map
455        // I don't want to deal with abstracting the AnnotationWriter in order for this
456        // not
457        // to happen, but if I still do it then the people who are anti api would still
458        // find
459        // a way to bypass my little techniques. That's why this is here.
460        if (!(written instanceof Character)) {
461          throw new IllegalArgumentException(
462              "Cannot append other than char for config: annotation '" + annotationName + "'");
463        }
464        writer.append((char) written);
465    }
466  }
467
468  private static void handleComments(
469      AnnotationType type, Field field, Class<?> aClass, String commentChar, PrintWriter writer) {
470    if (type.is(AnnotationType.COMMENT)) {
471      writer.println(commentChar + getAnnotation(field, aClass, Comment.class).value());
472    }
473    if (type.is(AnnotationType.COMMENTS)) {
474      for (Comment comment : getAnnotation(field, aClass, Comments.class).value()) {
475        writer.println(commentChar + comment.value());
476      }
477    }
478  }
479
480  private static <T extends Annotation> T getAnnotation(
481      Field field, Class<?> theClass, Class<T> annotationType) {
482    if (field != null) {
483      return field.getDeclaredAnnotation(annotationType);
484    } else {
485      return theClass.getDeclaredAnnotation(annotationType);
486    }
487  }
488
489  private static void populate(
490      AnnotationHolder holder,
491      AnnotationType putData,
492      Map<AnnotationHolder, List<AnnotationType>> map) {
493    if (map.containsKey(holder)) {
494      List<AnnotationType> data = map.get(holder);
495      data.add(putData);
496      map.replace(holder, data);
497    } else {
498      List<AnnotationType> data = new ArrayList<>();
499      data.add(putData);
500      map.put(holder, data);
501    }
502  }
503}