1616import java .nio .file .Paths ;
1717import java .util .*;
1818import java .util .function .BiConsumer ;
19- import java .util .stream .Collectors ;
2019import java .util .stream .Stream ;
2120
2221import javax .annotation .processing .*;
2322import javax .lang .model .SourceVersion ;
2423import javax .lang .model .element .*;
25- import javax .lang .model .type .DeclaredType ;
2624import javax .tools .Diagnostic ;
2725import javax .tools .JavaFileObject ;
2826import javax .tools .SimpleJavaFileObject ;
@@ -132,50 +130,55 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
132130 context .getRouters ().forEach (it -> context .debug (" %s" , it .getGeneratedType ()));
133131 return false ;
134132 } else {
135- // 1. Discover all unique Controller classes
136- Set < TypeElement > controllers = findControllers (annotations , roundEnv );
133+ // Discover all unique Controller classes
134+ var controllers = findControllers (annotations , roundEnv );
137135
138- // 2. Factory Pattern: Build specific routers for each class based on method annotations
136+ // Factory Pattern: Build specific routers for each class based on method annotations
139137 List <WebRouter <?>> activeRouters = new ArrayList <>();
140- for (TypeElement controller : controllers ) {
138+ for (var controller : controllers ) {
141139 if (controller .getModifiers ().contains (Modifier .ABSTRACT )) continue ;
142140
143141 // These factory methods will scan the class methods and return a populated router
144142 // if it finds relevant annotations (@GET for Rest, @McpTool for MCP, etc.)
145143 // We will implement these factories inside the respective Router classes.
146144
147- RestRouter restRouter = RestRouter .parse (context , controller );
148- if (!restRouter .isEmpty ()) activeRouters .add (restRouter );
145+ var restRouter = RestRouter .parse (context , controller );
146+ if (!restRouter .isEmpty ()) {
147+ activeRouters .add (restRouter );
148+ }
149149
150- JsonRpcRouter jsonRpcRouter = JsonRpcRouter .parse (context , controller );
151- if (!jsonRpcRouter .isEmpty ()) activeRouters .add (jsonRpcRouter );
150+ var jsonRpcRouter = JsonRpcRouter .parse (context , controller );
151+ if (!jsonRpcRouter .isEmpty ()) {
152+ activeRouters .add (jsonRpcRouter );
153+ }
152154
153- McpRouter mcpRouter = McpRouter .parse (context , controller );
154- if (!mcpRouter .isEmpty ()) activeRouters .add (mcpRouter );
155+ var mcpRouter = McpRouter .parse (context , controller );
156+ if (!mcpRouter .isEmpty ()) {
157+ activeRouters .add (mcpRouter );
158+ }
155159
156- TrpcRouter trpcRouter = TrpcRouter .parse (context , controller );
157- if (!trpcRouter .isEmpty ()) activeRouters .add (trpcRouter );
160+ var trpcRouter = TrpcRouter .parse (context , controller );
161+ if (!trpcRouter .isEmpty ()) {
162+ activeRouters .add (trpcRouter );
163+ }
158164 }
159165
160166 verifyBeanValidationDependency (activeRouters );
161167
162- // 3. Generate Code Iteratively!
163- for (WebRouter <?> router : activeRouters ) {
168+ // Generate Code Iteratively!
169+ for (var router : activeRouters ) {
164170 try {
165- context .add (router ); // Track for processingOver output
166-
167- String sourceCode = router .getSourceCode (null );
168- if (sourceCode != null ) {
169- String sourceLocation = router .getGeneratedFilename ();
170- String generatedType = router .getGeneratedType ();
171+ context .add (router );
171172
172- onGeneratedSource (generatedType , toJavaFileObject (sourceLocation , sourceCode ));
173- context .debug ("router %s: %s" , router .getTargetType (), generatedType );
173+ var sourceCode = router .toSourceCode (null );
174+ var sourceLocation = router .getGeneratedFilename ();
175+ var generatedType = router .getGeneratedType ();
174176
175- writeSource (
176- router .isKt (), generatedType , sourceLocation , sourceCode , router .getTargetType ());
177- }
177+ onGeneratedSource (generatedType , toJavaFileObject (sourceLocation , sourceCode ));
178+ context .debug ("router %s: %s" , router .getTargetType (), generatedType );
178179
180+ writeSource (
181+ router .isKt (), generatedType , sourceLocation , sourceCode , router .getTargetType ());
179182 } catch (IOException cause ) {
180183 throw new RuntimeException ("Unable to generate: " + router .getTargetType (), cause );
181184 }
@@ -194,7 +197,8 @@ private Set<TypeElement> findControllers(
194197 Set <TypeElement > controllers = new LinkedHashSet <>();
195198 for (var annotation : annotations ) {
196199 for (var element : roundEnv .getElementsAnnotatedWith (annotation )) {
197- if (element instanceof TypeElement typeElement ) {
200+ if (element instanceof TypeElement typeElement
201+ && !typeElement .getModifiers ().contains (Modifier .ABSTRACT )) {
198202 controllers .add (typeElement );
199203 } else if (element instanceof ExecutableElement method ) {
200204 controllers .add ((TypeElement ) method .getEnclosingElement ());
@@ -262,142 +266,16 @@ public String toString() {
262266
263267 protected void onGeneratedSource (String className , JavaFileObject source ) {}
264268
265- private Map <TypeElement , MvcRouter > buildRouteRegistry (
266- Set <? extends TypeElement > annotations , RoundEnvironment roundEnv ) {
267- Map <TypeElement , MvcRouter > registry = new LinkedHashMap <>();
268-
269- for (var annotation : annotations ) {
270- context .debug ("found annotation: %s" , annotation );
271- var elements = roundEnv .getElementsAnnotatedWith (annotation );
272- // Element could be Class or Method, bc @Path can be applied to both of them
273- // Also we need to expand lookup to external jars see #2486
274- for (var element : elements ) {
275- context .debug (" %s" , element );
276- if (element instanceof TypeElement typeElement ) {
277- // FORCE INIT: Ensures MvcRouter constructor executes our JsonRpc class-level rules
278- registry .computeIfAbsent (typeElement , type -> new MvcRouter (context , type ));
279- buildRouteRegistry (registry , typeElement );
280- } else if (element instanceof ExecutableElement method ) {
281- TypeElement typeElement = (TypeElement ) method .getEnclosingElement ();
282- // FORCE INIT
283- registry .computeIfAbsent (typeElement , type -> new MvcRouter (context , type ));
284- buildRouteRegistry (registry , typeElement );
285- }
286- }
287- }
288-
289- // Remove all abstract router
290- var abstractTypes =
291- registry .entrySet ().stream ()
292- .filter (it -> it .getValue ().isAbstract ())
293- .map (Map .Entry ::getKey )
294- .collect (Collectors .toSet ());
295- abstractTypes .forEach (registry ::remove );
296-
297- // Generate unique method name by router
298- for (var router : registry .values ()) {
299- // Split routes by their target generated classes to avoid false collisions
300- var restAndTrpcRoutes = router .getRoutes ().stream ().filter (r -> !r .isJsonRpc ()).toList ();
301-
302- var rpcRoutes = router .getRoutes ().stream ().filter (MvcRoute ::isJsonRpc ).toList ();
303-
304- resolveGeneratedNames (restAndTrpcRoutes );
305- resolveGeneratedNames (rpcRoutes );
306- }
307- return registry ;
308- }
309-
310- private void resolveGeneratedNames (List <MvcRoute > routes ) {
311- // Group by the actual target method name in the generated class
312- var grouped =
313- routes .stream ()
314- .collect (
315- Collectors .groupingBy (
316- route -> {
317- String baseName = route .getMethodName ();
318- return route .isTrpc ()
319- ? "trpc"
320- + Character .toUpperCase (baseName .charAt (0 ))
321- + baseName .substring (1 )
322- : baseName ;
323- }));
324-
325- for (var overloads : grouped .values ()) {
326- if (overloads .size () == 1 ) {
327- // No conflict in this specific output file, use the clean original name
328- overloads .get (0 ).setGeneratedName (overloads .get (0 ).getMethodName ());
329- } else {
330- // Conflict detected: generate names based on parameter types
331- for (var route : overloads ) {
332- var paramsString =
333- route .getRawParameterTypes (true ).stream ()
334- .map (it -> it .substring (Math .max (0 , it .lastIndexOf ("." ) + 1 )))
335- .map (it -> Character .toUpperCase (it .charAt (0 )) + it .substring (1 ))
336- .collect (Collectors .joining ());
337-
338- // A 0-arg method gets exactly the base name.
339- // Methods with args get the base name + their parameter types.
340- route .setGeneratedName (route .getMethodName () + paramsString );
341- }
342- }
343- }
344- }
345-
346- /**
347- * Scan routes from basType and any super class of it. It saves all route method found in current
348- * type or super (inherited). Routes method from super types are also saved.
349- *
350- * <p>Abstract route method are ignored.
351- *
352- * @param registry Route registry.
353- * @param currentType Base type.
354- */
355- private void buildRouteRegistry (Map <TypeElement , MvcRouter > registry , TypeElement currentType ) {
356- for (TypeElement superType : context .superTypes (currentType )) {
357- // collect all declared methods
358- superType .getEnclosedElements ().stream ()
359- .filter (ExecutableElement .class ::isInstance )
360- .map (ExecutableElement .class ::cast )
361- .forEach (
362- method -> {
363- if (method .getModifiers ().contains (Modifier .ABSTRACT )) {
364- context .debug ("ignoring abstract method: %s %s" , superType , method );
365- } else {
366- method .getAnnotationMirrors ().stream ()
367- .map (AnnotationMirror ::getAnnotationType )
368- .map (DeclaredType ::asElement )
369- .filter (TypeElement .class ::isInstance )
370- .map (TypeElement .class ::cast )
371- .filter (HttpMethod ::hasAnnotation )
372- .forEach (
373- annotation -> {
374- Stream .of (currentType , superType )
375- .distinct ()
376- .forEach (
377- routerClass ->
378- registry
379- .computeIfAbsent (
380- routerClass , type -> new MvcRouter (context , type ))
381- .put (annotation , method ));
382- });
383- }
384- });
385- if (!currentType .equals (superType )) {
386- // edge-case #1: when a controller has no method and extends another class which has.
387- // edge-case #2: some odd usage a controller could be empty.
388- // See https://github.com/jooby-project/jooby/issues/3656
389- if (registry .containsKey (superType )) {
390- registry .computeIfAbsent (currentType , key -> new MvcRouter (key , registry .get (superType )));
391- }
392- }
393- }
394- }
395-
396269 @ Override
397270 public Set <String > getSupportedAnnotationTypes () {
398271 var supportedTypes = new HashSet <String >();
399272 supportedTypes .addAll (HttpPath .PATH .getAnnotations ());
400273 supportedTypes .addAll (HttpMethod .annotations ());
274+ // Add Rcp annotations
275+ supportedTypes .add ("io.jooby.annotation.Trpc" );
276+ supportedTypes .add ("io.jooby.annotation.Trpc.Mutation" );
277+ supportedTypes .add ("io.jooby.annotation.Trpc.Query" );
278+ supportedTypes .add ("io.jooby.annotation.JsonRpc" );
401279 // Add MCP Annotations
402280 supportedTypes .add ("io.jooby.annotation.McpTool" );
403281 supportedTypes .add ("io.jooby.annotation.McpPrompt" );
@@ -428,37 +306,6 @@ public Set<String> getSupportedOptions() {
428306 return options ;
429307 }
430308
431- /**
432- * Throws any throwable 'sneakily' - you don't need to catch it, nor declare that you throw it
433- * onwards. The exception is still thrown - javac will just stop whining about it.
434- *
435- * <p>Example usage:
436- *
437- * <pre>public void run() {
438- * throw sneakyThrow(new IOException("You don't need to catch me!"));
439- * }</pre>
440- *
441- * <p>NB: The exception is not wrapped, ignored, swallowed, or redefined. The JVM actually does
442- * not know or care about the concept of a 'checked exception'. All this method does is hide the
443- * act of throwing a checked exception from the java compiler.
444- *
445- * <p>Note that this method has a return type of {@code RuntimeException}; it is advised you
446- * always call this method as argument to the {@code throw} statement to avoid compiler errors
447- * regarding no return statement and similar problems. This method won't of course return an
448- * actual {@code RuntimeException} - it never returns, it always throws the provided exception.
449- *
450- * @param x The throwable to throw without requiring you to catch its type.
451- * @return A dummy RuntimeException; this method never returns normally, it <em>always</em> throws
452- * an exception!
453- */
454- public static RuntimeException propagate (final Throwable x ) {
455- if (x == null ) {
456- throw new NullPointerException ("x" );
457- }
458-
459- return sneakyThrow0 (x );
460- }
461-
462309 /**
463310 * Make a checked exception un-checked and rethrow it.
464311 *
0 commit comments