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}