001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import com.soklet.annotation.DELETE; 020import com.soklet.annotation.GET; 021import com.soklet.annotation.HEAD; 022import com.soklet.annotation.OPTIONS; 023import com.soklet.annotation.PATCH; 024import com.soklet.annotation.POST; 025import com.soklet.annotation.PUT; 026import com.soklet.annotation.ServerSentEventSource; 027 028import javax.annotation.concurrent.NotThreadSafe; 029import javax.annotation.processing.AbstractProcessor; 030import javax.annotation.processing.Filer; 031import javax.annotation.processing.Messager; 032import javax.annotation.processing.ProcessingEnvironment; 033import javax.annotation.processing.RoundEnvironment; 034import javax.lang.model.SourceVersion; 035import javax.lang.model.element.AnnotationMirror; 036import javax.lang.model.element.AnnotationValue; 037import javax.lang.model.element.Element; 038import javax.lang.model.element.ElementKind; 039import javax.lang.model.element.ExecutableElement; 040import javax.lang.model.element.Modifier; 041import javax.lang.model.element.TypeElement; 042import javax.lang.model.element.VariableElement; 043import javax.lang.model.type.TypeMirror; 044import javax.lang.model.util.Elements; 045import javax.lang.model.util.Types; 046import javax.tools.Diagnostic; 047import javax.tools.FileObject; 048import javax.tools.StandardLocation; 049import java.io.BufferedReader; 050import java.io.IOException; 051import java.io.InputStreamReader; 052import java.io.UncheckedIOException; 053import java.io.Writer; 054import java.lang.annotation.Annotation; 055import java.lang.annotation.Repeatable; 056import java.nio.charset.StandardCharsets; 057import java.util.ArrayList; 058import java.util.Arrays; 059import java.util.Base64; 060import java.util.Collections; 061import java.util.Comparator; 062import java.util.LinkedHashMap; 063import java.util.LinkedHashSet; 064import java.util.List; 065import java.util.Map; 066import java.util.Set; 067import java.util.stream.Collectors; 068 069/** 070 * Soklet's standard Annotation Processor which is used to generate lookup tables of <em>Resource Method</em> definitions at compile time as well as prevent usage errors that are detectable by static analysis. 071 * <p> 072 * This Annotation Processor ensures <em>Resource Methods</em> annotated with {@link ServerSentEventSource} are declared as returning an instance of {@link HandshakeResult}. 073 * <p> 074 * Your build system should ensure this Annotation Processor is available at compile time. Follow the instructions below to make your application conformant: 075 * <p> 076 * Using {@code javac} directly: 077 * <pre>javac -parameters -processor com.soklet.SokletProcessor ...[rest of javac command elided]</pre> 078 * Using <a href="https://maven.apache.org" target="_blank">Maven</a>: 079 * <pre>{@code <plugin> 080 * <groupId>org.apache.maven.plugins</groupId> 081 * <artifactId>maven-compiler-plugin</artifactId> 082 * <version>...</version> 083 * <configuration> 084 * <release>...</release> 085 * <compilerArgs> 086 * <!-- Rest of args elided --> 087 * <arg>-parameters</arg> 088 * <arg>-processor</arg> 089 * <arg>com.soklet.SokletProcessor</arg> 090 * </compilerArgs> 091 * </configuration> 092 * </plugin>}</pre> 093 * Using <a href="https://gradle.org" target="_blank">Gradle</a>: 094 * <pre>{@code def sokletVersion = "2.0.0" // (use your actual version) 095 * 096 * dependencies { 097 * // Soklet used by your code at compile/run time 098 * implementation "com.soklet:soklet:${sokletVersion}" 099 * 100 * // Same artifact also provides the annotation processor 101 * annotationProcessor "com.soklet:soklet:${sokletVersion}" 102 * 103 * // If tests also need processing (optional) 104 * testAnnotationProcessor "com.soklet:soklet:${sokletVersion}" 105 * }}</pre> 106 * 107 * @author <a href="https://www.revetkn.com">Mark Allen</a> 108 */ 109@NotThreadSafe 110public final class SokletProcessor extends AbstractProcessor { 111 private Types types; 112 private Elements elements; 113 private Messager messager; 114 private Filer filer; 115 116 // Cached mirrors resolved in init() 117 private TypeMirror handshakeResultType; // com.soklet.HandshakeResult 118 private TypeElement pathParameterElement; // com.soklet.annotation.PathParameter 119 120 @Override 121 public synchronized void init(ProcessingEnvironment processingEnv) { 122 super.init(processingEnv); 123 this.types = processingEnv.getTypeUtils(); 124 this.elements = processingEnv.getElementUtils(); 125 this.messager = processingEnv.getMessager(); 126 this.filer = processingEnv.getFiler(); 127 128 TypeElement hr = elements.getTypeElement("com.soklet.HandshakeResult"); 129 this.handshakeResultType = (hr == null ? null : hr.asType()); 130 this.pathParameterElement = elements.getTypeElement("com.soklet.annotation.PathParameter"); 131 } 132 133 @Override 134 public Set<String> getSupportedAnnotationTypes() { 135 Set<String> out = new LinkedHashSet<>(); 136 for (Class<? extends Annotation> c : HTTP_AND_SSE_ANNOTATIONS) { 137 out.add(c.getCanonicalName()); 138 Class<? extends Annotation> container = findRepeatableContainer(c); 139 if (container != null) out.add(container.getCanonicalName()); 140 } 141 return out; 142 } 143 144 @Override 145 public SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();} 146 147 private final List<ResourceMethodDeclaration> collected = new ArrayList<>(); 148 private final Set<String> touchedTopLevelBinaries = new LinkedHashSet<>(); 149 150 @Override 151 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 152 for (Element root : roundEnv.getRootElements()) { 153 if (root instanceof TypeElement te) { 154 String bin = elements.getBinaryName(te).toString(); 155 touchedTopLevelBinaries.add(bin); 156 } 157 } 158 159 // SSE-specific return type check 160 enforceSseReturnTypes(roundEnv); 161 162 // Collect + validate 163 collect(roundEnv, HttpMethod.GET, GET.class, false); 164 collect(roundEnv, HttpMethod.POST, POST.class, false); 165 collect(roundEnv, HttpMethod.PUT, PUT.class, false); 166 collect(roundEnv, HttpMethod.PATCH, PATCH.class, false); 167 collect(roundEnv, HttpMethod.DELETE, DELETE.class, false); 168 collect(roundEnv, HttpMethod.HEAD, HEAD.class, false); 169 collect(roundEnv, HttpMethod.OPTIONS, OPTIONS.class, false); 170 collect(roundEnv, HttpMethod.GET, ServerSentEventSource.class, true); // SSE as GET + flag 171 172 if (roundEnv.processingOver()) { 173 mergeAndWriteIndex(collected, touchedTopLevelBinaries); 174 } 175 return false; 176 } 177 178 private static final List<Class<? extends Annotation>> HTTP_AND_SSE_ANNOTATIONS = List.of( 179 GET.class, POST.class, PUT.class, PATCH.class, DELETE.class, HEAD.class, OPTIONS.class, 180 ServerSentEventSource.class 181 ); 182 183 /** 184 * Collects and validates each annotated method occurrence (repeatable-aware). 185 */ 186 private void collect(RoundEnvironment roundEnv, 187 HttpMethod httpMethod, 188 Class<? extends Annotation> baseAnnotation, 189 boolean serverSentEventSource) { 190 191 Set<Element> candidates = new LinkedHashSet<>(); 192 TypeElement base = elements.getTypeElement(baseAnnotation.getCanonicalName()); 193 if (base != null) candidates.addAll(roundEnv.getElementsAnnotatedWith(base)); 194 Class<? extends Annotation> containerAnn = findRepeatableContainer(baseAnnotation); 195 if (containerAnn != null) { 196 TypeElement container = elements.getTypeElement(containerAnn.getCanonicalName()); 197 if (container != null) candidates.addAll(roundEnv.getElementsAnnotatedWith(container)); 198 } 199 200 for (Element e : candidates) { 201 if (e.getKind() != ElementKind.METHOD) { 202 error(e, "Soklet: @%s can only be applied to methods.", baseAnnotation.getSimpleName()); 203 continue; 204 } 205 ExecutableElement method = (ExecutableElement) e; 206 TypeElement owner = (TypeElement) method.getEnclosingElement(); 207 208 // -- Signature validations common to all HTTP annotations -- 209 // 1) no static 210 if (method.getModifiers().contains(Modifier.STATIC)) { 211 error(method, "Soklet: Resource Method must not be static"); 212 // keep validating path so we can show all issues in one compile, but we won't collect if any error 213 } 214 215 // Repeatable-aware: iterate each occurrence on the same method 216 Annotation[] anns = method.getAnnotationsByType(cast(baseAnnotation)); 217 for (Annotation a : anns) { 218 String rawPath = readAnnotationStringMember(a, "value"); 219 if (rawPath == null || rawPath.isBlank()) { 220 error(method, "Soklet: @%s must have a non-empty path value", baseAnnotation.getSimpleName()); 221 continue; 222 } 223 224 // 2) path normalization + validation 225 String path = normalizePath(rawPath); 226 227 ValidationResult vr = validatePathTemplate(method, path); 228 // If malformed, skip further param-name checks for this occurrence 229 if (!vr.ok) { 230 continue; 231 } 232 233 // 3) @PathParameter bindings 234 ParamBindings pb = readPathParameterBindings(method); 235 // a) placeholders must be bound 236 for (String placeholder : vr.placeholders) { // <-- already normalized 237 if (!pb.paramNames.contains(placeholder)) { 238 // use the original token if you want to echo "{cssPath*}" in the message 239 String shown = vr.original.getOrDefault(placeholder, placeholder); 240 error(method, "Resource Method path parameter {" + shown + "} not bound to a @PathParameter argument"); 241 } 242 } 243 244 // b) annotated params must exist in template 245 for (String annotated : pb.paramNames) { // annotated names are plain ("cssPath") 246 if (!vr.placeholders.contains(annotated)) { 247 error(method, "No placeholder {" + annotated + "} present in resource path declaration"); 248 } 249 } 250 251 // Only collect if no errors were reported on this method occurrence 252 if (!pb.hadError && vr.ok && !method.getModifiers().contains(Modifier.STATIC)) { 253 String className = elements.getBinaryName(owner).toString(); 254 String methodName = method.getSimpleName().toString(); 255 String[] paramTypes = method.getParameters().stream() 256 .map(p -> jvmTypeName(p.asType())) 257 .toArray(String[]::new); 258 259 collected.add(new ResourceMethodDeclaration( 260 httpMethod, path, className, methodName, paramTypes, serverSentEventSource 261 )); 262 } 263 } 264 } 265 } 266 267 // --- Helpers for parameter annotations ------------------------------------ 268 269 private static final class ParamBindings { 270 final Set<String> paramNames; 271 final boolean hadError; 272 273 ParamBindings(Set<String> names, boolean hadError) { 274 this.paramNames = names; 275 this.hadError = hadError; 276 } 277 } 278 279 private ParamBindings readPathParameterBindings(ExecutableElement method) { 280 boolean hadError = false; 281 Set<String> names = new LinkedHashSet<>(); 282 if (pathParameterElement == null) return new ParamBindings(names, false); 283 284 for (VariableElement p : method.getParameters()) { 285 for (AnnotationMirror am : p.getAnnotationMirrors()) { 286 if (isAnnotationType(am, pathParameterElement)) { 287 // 1) try explicit annotation member 288 String name = readAnnotationStringMember(am, "name"); 289 // 2) default to the parameter's source name if missing/blank 290 if (name == null || name.isBlank()) { 291 name = p.getSimpleName().toString(); 292 } 293 if (name != null && !name.isBlank()) { 294 names.add(name); 295 } 296 } 297 } 298 } 299 return new ParamBindings(names, hadError); 300 } 301 302 private static boolean isAnnotationType(AnnotationMirror am, TypeElement type) { 303 return am.getAnnotationType().asElement().equals(type); 304 } 305 306 // Overload: read member from AnnotationMirror (compile-safe, no reflection) 307 private static String readAnnotationStringMember(AnnotationMirror am, String member) { 308 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e : am.getElementValues().entrySet()) { 309 if (e.getKey().getSimpleName().contentEquals(member)) { 310 Object v = e.getValue().getValue(); 311 return (v == null) ? null : v.toString(); 312 } 313 } 314 return null; 315 } 316 317 // Existing reflective reader (used for repeatable occurrences) 318 private static String readAnnotationStringMember(Annotation a, String memberName) { 319 try { 320 Object v = a.annotationType().getMethod(memberName).invoke(a); 321 return (v == null) ? null : v.toString(); 322 } catch (ReflectiveOperationException ex) { 323 return null; 324 } 325 } 326 327 // --- Path parsing/validation ---------------------------------------------- 328 329 private static final class ValidationResult { 330 final boolean ok; 331 final Set<String> placeholders; // normalized names (no trailing '*') 332 final Map<String, String> original; // normalized -> original token (e.g., "cssPath" -> "cssPath*") 333 334 ValidationResult(boolean ok, Set<String> placeholders, Map<String, String> original) { 335 this.ok = ok; 336 this.placeholders = placeholders; 337 this.original = original; 338 } 339 } 340 341 /** 342 * Validates braces and duplicate placeholders (treating {name*} as a greedy/varargs 343 * placeholder whose logical name is "name"). Duplicate detection is done on the 344 * normalized name (without the trailing '*'). 345 * <p> 346 * Emits diagnostics on the element if malformed or duplicate. 347 */ 348 private ValidationResult validatePathTemplate(Element reportOn, String path) { 349 if (path == null || path.isEmpty()) { 350 return new ValidationResult(false, Collections.emptySet(), Collections.emptyMap()); 351 } 352 353 Set<String> names = new LinkedHashSet<>(); 354 Map<String, String> originalTokens = new LinkedHashMap<>(); 355 356 int i = 0; 357 while (i < path.length()) { 358 char c = path.charAt(i); 359 if (c == '{') { 360 int close = path.indexOf('}', i + 1); 361 if (close < 0) { 362 error(reportOn, "Soklet: Malformed resource path declaration (unbalanced braces)"); 363 return new ValidationResult(false, Collections.emptySet(), Collections.emptyMap()); 364 } 365 String token = path.substring(i + 1, close); // e.g., "id", "cssPath*" 366 if (token.isEmpty()) { 367 error(reportOn, "Soklet: Malformed resource path declaration (unbalanced braces)"); 368 return new ValidationResult(false, Collections.emptySet(), Collections.emptyMap()); 369 } 370 371 String normalized = normalizePlaceholder(token); // strip trailing '*' if present 372 if (normalized.isEmpty()) { 373 error(reportOn, "Soklet: Malformed resource path declaration (unbalanced braces)"); 374 return new ValidationResult(false, Collections.emptySet(), Collections.emptyMap()); 375 } 376 377 // Duplicate on normalized name 378 if (!names.add(normalized)) { 379 error(reportOn, "Soklet: Duplicate @PathParameter name: " + normalized); 380 } 381 // keep first-seen original token for any potential messaging 382 originalTokens.putIfAbsent(normalized, token); 383 384 i = close + 1; 385 } else if (c == '}') { 386 error(reportOn, "Soklet: Malformed resource path declaration (unbalanced braces)"); 387 return new ValidationResult(false, Collections.emptySet(), Collections.emptyMap()); 388 } else { 389 i++; 390 } 391 } 392 return new ValidationResult(true, names, originalTokens); 393 } 394 395 /** 396 * Accepts vararg placeholders like "cssPath*" and returns the logical name "cssPath". 397 */ 398 private static String normalizePlaceholder(String token) { 399 if (token.endsWith("*")) return token.substring(0, token.length() - 1); 400 return token; 401 } 402 403 // --- Existing utilities ---------------------------------------------------- 404 405 @SuppressWarnings({"rawtypes", "unchecked"}) 406 private static Class<? extends Annotation> cast(Class<? extends Annotation> c) {return (Class) c;} 407 408 private static String normalizePath(String p) { 409 if (p == null || p.isEmpty()) return "/"; 410 if (p.charAt(0) != '/') return "/" + p; 411 return p; 412 } 413 414 private static Class<? extends Annotation> findRepeatableContainer(Class<? extends Annotation> base) { 415 Repeatable repeatable = base.getAnnotation(Repeatable.class); 416 return (repeatable == null) ? null : repeatable.value(); 417 } 418 419 private String jvmTypeName(TypeMirror t) { 420 switch (t.getKind()) { 421 case BOOLEAN: 422 return "boolean"; 423 case BYTE: 424 return "byte"; 425 case SHORT: 426 return "short"; 427 case CHAR: 428 return "char"; 429 case INT: 430 return "int"; 431 case LONG: 432 return "long"; 433 case FLOAT: 434 return "float"; 435 case DOUBLE: 436 return "double"; 437 case VOID: 438 return "void"; 439 case ARRAY: 440 return "[" + jvmTypeDescriptor(((javax.lang.model.type.ArrayType) t).getComponentType()); 441 case DECLARED: 442 default: 443 TypeMirror erasure = processingEnv.getTypeUtils().erasure(t); 444 Element el = processingEnv.getTypeUtils().asElement(erasure); 445 if (el instanceof TypeElement te) { 446 return processingEnv.getElementUtils().getBinaryName(te).toString(); 447 } 448 return erasure.toString(); 449 } 450 } 451 452 private String jvmTypeDescriptor(TypeMirror t) { 453 switch (t.getKind()) { 454 case BOOLEAN: 455 return "Z"; 456 case BYTE: 457 return "B"; 458 case SHORT: 459 return "S"; 460 case CHAR: 461 return "C"; 462 case INT: 463 return "I"; 464 case LONG: 465 return "J"; 466 case FLOAT: 467 return "F"; 468 case DOUBLE: 469 return "D"; 470 case ARRAY: 471 return "[" + jvmTypeDescriptor(((javax.lang.model.type.ArrayType) t).getComponentType()); 472 case DECLARED: 473 default: 474 TypeMirror erasure = processingEnv.getTypeUtils().erasure(t); 475 Element el = processingEnv.getTypeUtils().asElement(erasure); 476 if (el instanceof TypeElement te) { 477 String bin = processingEnv.getElementUtils().getBinaryName(te).toString(); 478 return "L" + bin + ";"; 479 } 480 return "Ljava/lang/Object;"; 481 } 482 } 483 484 // ---- SSE return-type validation ------------------------------------------ 485 486 private void enforceSseReturnTypes(RoundEnvironment roundEnv) { 487 if (handshakeResultType == null) return; 488 TypeElement sseAnn = elements.getTypeElement(ServerSentEventSource.class.getCanonicalName()); 489 if (sseAnn == null) return; 490 491 for (Element e : roundEnv.getElementsAnnotatedWith(sseAnn)) { 492 if (e.getKind() != ElementKind.METHOD) { 493 error(e, "@%s can only be applied to methods.", ServerSentEventSource.class.getSimpleName()); 494 continue; 495 } 496 ExecutableElement method = (ExecutableElement) e; 497 TypeMirror returnType = method.getReturnType(); 498 boolean assignable = types.isAssignable(returnType, handshakeResultType); 499 if (!assignable) { 500 error(e, 501 "Soklet: Resource Methods annotated with @%s must specify a return type of %s (found: %s).", 502 ServerSentEventSource.class.getSimpleName(), "HandshakeResult", prettyType(returnType)); 503 } 504 } 505 } 506 507 private static String prettyType(TypeMirror t) {return (t == null ? "null" : t.toString());} 508 509 // ---- Index read/merge/write ---------------------------------------------- 510 511 static String RESOURCE_METHOD_LOOKUP_TABLE_PATH = "META-INF/soklet/resource-method-lookup-table"; 512 513 private void mergeAndWriteIndex(List<ResourceMethodDeclaration> newlyCollected, 514 Set<String> touchedTopLevelBinaries) { 515 Map<String, ResourceMethodDeclaration> merged = readExistingIndex(); 516 517 if (!touchedTopLevelBinaries.isEmpty()) { 518 merged.values().removeIf(r -> { 519 String ownerBin = r.className(); 520 for (String top : touchedTopLevelBinaries) { 521 if (ownerBin.equals(top) || ownerBin.startsWith(top + "$")) return true; 522 } 523 return false; 524 }); 525 } 526 527 merged.values().removeIf(r -> { 528 String canonical = binaryToCanonical(r.className()); 529 TypeElement te = elements.getTypeElement(canonical); 530 return te == null; 531 }); 532 533 for (ResourceMethodDeclaration r : dedupeAndOrder(newlyCollected)) { 534 merged.put(generateKey(r), r); 535 } 536 537 List<ResourceMethodDeclaration> toWrite = new ArrayList<>(merged.values()); 538 toWrite.sort(Comparator 539 .comparing((ResourceMethodDeclaration r) -> r.httpMethod().name()) 540 .thenComparing(ResourceMethodDeclaration::path) 541 .thenComparing(ResourceMethodDeclaration::className) 542 .thenComparing(ResourceMethodDeclaration::methodName)); 543 544 writeRoutesIndexResource(toWrite); 545 } 546 547 private static String binaryToCanonical(String binary) {return binary.replace('$', '.');} 548 549 private Map<String, ResourceMethodDeclaration> readExistingIndex() { 550 Map<String, ResourceMethodDeclaration> out = new LinkedHashMap<>(); 551 BufferedReader reader = null; 552 try { 553 FileObject fo = filer.getResource(StandardLocation.CLASS_OUTPUT, "", RESOURCE_METHOD_LOOKUP_TABLE_PATH); 554 reader = new BufferedReader(new InputStreamReader(fo.openInputStream(), StandardCharsets.UTF_8)); 555 String line; 556 while ((line = reader.readLine()) != null) { 557 line = line.trim(); 558 if (line.isEmpty()) continue; 559 ResourceMethodDeclaration r = parseIndexLine(line); 560 if (r != null) out.put(generateKey(r), r); 561 } 562 } catch (IOException ignored) { 563 } finally { 564 if (reader != null) try { 565 reader.close(); 566 } catch (IOException ignored) { 567 } 568 } 569 return out; 570 } 571 572 private ResourceMethodDeclaration parseIndexLine(String line) { 573 try { 574 String[] parts = line.split("\\|", -1); 575 if (parts.length < 6) return null; 576 577 HttpMethod httpMethod = HttpMethod.valueOf(parts[0]); 578 Base64.Decoder dec = Base64.getDecoder(); 579 580 String path = new String(dec.decode(parts[1]), StandardCharsets.UTF_8); 581 String className = new String(dec.decode(parts[2]), StandardCharsets.UTF_8); 582 String methodName = new String(dec.decode(parts[3]), StandardCharsets.UTF_8); 583 String paramsJoined = new String(dec.decode(parts[4]), StandardCharsets.UTF_8); 584 boolean sse = Boolean.parseBoolean(parts[5]); 585 586 String[] paramTypes; 587 if (paramsJoined.isEmpty()) { 588 paramTypes = new String[0]; 589 } else { 590 List<String> tmp = Arrays.stream(paramsJoined.split(";")) 591 .filter(s -> !s.isEmpty()) 592 .collect(Collectors.toList()); 593 paramTypes = tmp.toArray(String[]::new); 594 } 595 return new ResourceMethodDeclaration(httpMethod, path, className, methodName, paramTypes, sse); 596 } catch (Throwable t) { 597 return null; 598 } 599 } 600 601 private void writeRoutesIndexResource(List<ResourceMethodDeclaration> routes) { 602 try { 603 FileObject fo = filer.createResource(StandardLocation.CLASS_OUTPUT, "", RESOURCE_METHOD_LOOKUP_TABLE_PATH); 604 try (Writer w = fo.openWriter()) { 605 Base64.Encoder b64 = Base64.getEncoder(); 606 for (ResourceMethodDeclaration r : routes) { 607 String params = String.join(";", r.parameterTypes()); 608 String line = String.join("|", 609 r.httpMethod().name(), 610 b64encode(b64, r.path()), 611 b64encode(b64, r.className()), 612 b64encode(b64, r.methodName()), 613 b64encode(b64, params), 614 Boolean.toString(r.serverSentEventSource()) 615 ); 616 w.write(line); 617 w.write('\n'); 618 } 619 } 620 } catch (IOException e) { 621 throw new UncheckedIOException("Failed to write " + RESOURCE_METHOD_LOOKUP_TABLE_PATH, e); 622 } 623 } 624 625 private static String b64encode(Base64.Encoder enc, String s) { 626 byte[] bytes = (s == null ? new byte[0] : s.getBytes(StandardCharsets.UTF_8)); 627 return enc.encodeToString(bytes); 628 } 629 630 // ---- Messaging ------------------------------------------------------------ 631 632 private void error(Element e, String fmt, Object... args) { 633 messager.printMessage(Diagnostic.Kind.ERROR, String.format(fmt, args), e); 634 } 635 636 private static String generateKey(ResourceMethodDeclaration r) { 637 return r.httpMethod().name() + "|" + r.path() + "|" + r.className() + "|" + 638 r.methodName() + "|" + String.join(";", r.parameterTypes()) + "|" + 639 r.serverSentEventSource(); 640 } 641 642 private static List<ResourceMethodDeclaration> dedupeAndOrder(List<ResourceMethodDeclaration> in) { 643 Map<String, ResourceMethodDeclaration> byKey = new LinkedHashMap<>(); 644 for (ResourceMethodDeclaration r : in) byKey.putIfAbsent(generateKey(r), r); 645 List<ResourceMethodDeclaration> out = new ArrayList<>(byKey.values()); 646 out.sort(Comparator 647 .comparing((ResourceMethodDeclaration r) -> r.httpMethod().name()) 648 .thenComparing(ResourceMethodDeclaration::path) 649 .thenComparing(ResourceMethodDeclaration::className) 650 .thenComparing(ResourceMethodDeclaration::methodName)); 651 return out; 652 } 653}