6565import java .lang .reflect .ParameterizedType ;
6666import java .lang .reflect .Type ;
6767import java .util .AbstractMap ;
68+ import java .util .ArrayList ;
6869import java .util .Arrays ;
6970import java .util .Collection ;
7071import java .util .Collections ;
7475import java .util .Map ;
7576import java .util .Optional ;
7677import java .util .Set ;
78+ import java .util .regex .Pattern ;
7779import java .util .stream .Collectors ;
80+ import java .util .stream .Stream ;
7881
7982import static com .google .common .base .Strings .isNullOrEmpty ;
8083import static com .google .common .base .Strings .nullToEmpty ;
8790 * and too big for what we want to do with it.
8891 */
8992public class Generator {
93+ private static final String INNER_CLASSES_SEPARATOR = "__" ;
9094
9195 private static final Logger LOG = LoggerFactory .getLogger (Generator .class );
9296
9397 public static final String EMULATED_SWAGGER_VERSION = "1.2" ;
9498 public static final String CLOUD_VISIBLE = "cloud" ;
9599
96100 private static final Map <String , Object > overviewResult = Maps .newHashMap ();
101+ private static final String PROPERTIES = "properties" ;
102+ private static final String ADDITIONAL_PROPERTIES = "additional_properties" ;
103+ private static final String ITEMS = "items" ;
104+ private static final String REF = "$ref" ;
105+ private static final String TYPE = "type" ;
106+ private static final String GENERIC_CLASSES_SEPARATOR = "_" ;
107+ private static final String ROUTE_SEPARATOR = "/" ;
108+ private static final String PATH = "path" ;
97109
98110 private final Set <Class <?>> resourceClasses ;
99111 private final Map <Class <?>, String > pluginMapping ;
@@ -126,12 +138,12 @@ private String prefixedPath(Class<?> resourceClass, @Nullable String resourceAnn
126138
127139 if (pluginMapping .containsKey (resourceClass )) {
128140 prefixedPath .append (pluginPathPrefix )
129- .append ("/" )
141+ .append (ROUTE_SEPARATOR )
130142 .append (pluginMapping .get (resourceClass ));
131143 }
132144
133- if (!resourcePath .startsWith ("/" )) {
134- prefixedPath .append ("/" );
145+ if (!resourcePath .startsWith (ROUTE_SEPARATOR )) {
146+ prefixedPath .append (ROUTE_SEPARATOR );
135147 }
136148
137149 return prefixedPath .append (resourcePath ).toString ();
@@ -160,7 +172,7 @@ public synchronized Map<String, Object> generateOverview() {
160172
161173 final Map <String , Object > apiDescription = Maps .newHashMap ();
162174 apiDescription .put ("name" , (prefixPlugins && prefixedPath .startsWith (pluginPathPrefix )) ? "Plugins/" + info .value () : info .value ());
163- apiDescription .put ("path" , prefixedPath );
175+ apiDescription .put (PATH , prefixedPath );
164176 apiDescription .put ("description" , info .description ());
165177
166178 apis .add (apiDescription );
@@ -243,7 +255,7 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
243255 produces = method .getAnnotation (Produces .class );
244256 }
245257 }
246- api .put ("path" , methodPath );
258+ api .put (PATH , methodPath );
247259
248260 Map <String , Object > operation = Maps .newHashMap ();
249261 operation .put ("method" , determineHttpMethod (method ));
@@ -262,13 +274,13 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
262274 if (responseType != null ) {
263275 models .putAll (responseType .models ());
264276 if (responseType .name () != null && isObjectSchema (responseType .type ())) {
265- operation .put ("type" , responseType .name ());
277+ operation .put (TYPE , responseType .name ());
266278 models .put (responseType .name (), responseType .type ());
267279 } else {
268280 if (responseType .type () != null ) {
269281 operation .putAll (responseType .type ());
270282 } else {
271- operation .put ("type" , responseType .name ());
283+ operation .put (TYPE , responseType .name ());
272284 }
273285 }
274286 }
@@ -296,12 +308,12 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
296308 }
297309 }
298310
299- if (basePath .endsWith ("/" )) {
311+ if (basePath .endsWith (ROUTE_SEPARATOR )) {
300312 basePath = basePath .substring (0 , basePath .length () - 1 );
301313 }
302314
303315 Collections .sort (apis , (o1 , o2 ) -> ComparisonChain .start ()
304- .compare (o1 .get ("path" ).toString (), o2 .get ("path" ).toString ())
316+ .compare (o1 .get (PATH ).toString (), o2 .get (PATH ).toString ())
305317 .result ());
306318
307319 // generate the json schema for the auto-mapped return types
@@ -401,15 +413,15 @@ private TypeSchema typeSchema(Type genericType) {
401413 final Map <String , Object > modelItemsDefinition ;
402414 if (valueType instanceof Class && isPrimitive ((Class <?>) valueType )) {
403415 valueName = mapPrimitives (((Class <?>) valueType ).getSimpleName ());
404- modelItemsDefinition = Collections .singletonMap ("additional_properties" , valueName );
416+ modelItemsDefinition = Collections .singletonMap (ADDITIONAL_PROPERTIES , valueName );
405417 } else {
406418 final TypeSchema valueSchema = typeSchema (valueType );
407419 if (valueSchema == null ) {
408420 return null ;
409421 }
410422 valueName = valueSchema .name ();
411423 models .putAll (valueSchema .models ());
412- modelItemsDefinition = Collections .singletonMap ("additional_properties" , Collections .singletonMap ("$ref" , valueName ));
424+ modelItemsDefinition = Collections .singletonMap (ADDITIONAL_PROPERTIES , Collections .singletonMap (REF , valueName ));
413425 if (valueSchema .type () != null ) {
414426 models .put (valueName , valueSchema .type ());
415427 }
@@ -418,13 +430,13 @@ private TypeSchema typeSchema(Type genericType) {
418430
419431 final String modelName = valueName + "Map" ;
420432 final Map <String , Object > model = ImmutableMap .<String , Object >builder ()
421- .put ("type" , "object" )
433+ .put (TYPE , "object" )
422434 .put ("id" , modelName )
423- .put ("properties" , Collections .emptyMap ())
435+ .put (PROPERTIES , Collections .emptyMap ())
424436 .putAll (modelItemsDefinition )
425437 .build ();
426438 models .put (modelName , model );
427- return createTypeSchema (modelName , Collections .singletonMap ("type" , modelName ), models );
439+ return createTypeSchema (modelName , Collections .singletonMap (TYPE , modelName ), models );
428440 }
429441 if (returnType .isAssignableFrom (Optional .class )) {
430442 final Type valueType = typeParameters (genericType )[0 ];
@@ -437,7 +449,7 @@ private TypeSchema typeSchema(Type genericType) {
437449 final Map <String , Object > modelItemsDefinition ;
438450 if (valueType instanceof Class && isPrimitive ((Class <?>) valueType )) {
439451 valueName = mapPrimitives (((Class <?>) valueType ).getSimpleName ());
440- modelItemsDefinition = Collections .singletonMap ("items" , valueName );
452+ modelItemsDefinition = Collections .singletonMap (ITEMS , valueName );
441453 } else {
442454 final TypeSchema valueSchema = typeSchema (valueType );
443455 if (valueSchema == null ) {
@@ -449,17 +461,17 @@ private TypeSchema typeSchema(Type genericType) {
449461 }
450462 models .putAll (valueSchema .models ());
451463 //final String valueModelId = (String)((Map<String, Object>)models.get(valueName)).get("id");
452- modelItemsDefinition = Collections .singletonMap ("items" , Collections .singletonMap ("$ref" , valueName ));
464+ modelItemsDefinition = Collections .singletonMap (ITEMS , Collections .singletonMap (REF , valueName ));
453465 }
454466 final String modelName = valueName + "Array" ;
455467 final Map <String , Object > model = ImmutableMap .<String , Object >builder ()
456- .put ("type" , "array" )
468+ .put (TYPE , "array" )
457469 .put ("id" , modelName )
458- .put ("properties" , Collections .emptyMap ())
470+ .put (PROPERTIES , Collections .emptyMap ())
459471 .putAll (modelItemsDefinition )
460472 .build ();
461473 models .put (modelName , model );
462- return createTypeSchema (modelName , Collections .singletonMap ("type" , modelName ), models );
474+ return createTypeSchema (modelName , Collections .singletonMap (TYPE , modelName ), models );
463475 }
464476
465477 final String modelName = uniqueModelName (genericType , returnType );
@@ -473,93 +485,141 @@ private TypeSchema typeSchema(Type genericType) {
473485 }
474486
475487 private String uniqueModelName (Type genericType , Class <?> returnType ) {
476- final var simpleName = returnType . getSimpleName ( );
488+ final var simpleName = nestedNames ( returnType ). collect ( Collectors . joining ( INNER_CLASSES_SEPARATOR ) );
477489 if (genericType instanceof ParameterizedType parameterizedType ) {
478490 final var classNames = Arrays .stream (parameterizedType .getActualTypeArguments ())
479491 .map (type -> uniqueModelName (type , classForType (type )))
480492 .toList ();
481- return simpleName + "_" + Joiner .on ("_" ).join (classNames );
493+ return simpleName + GENERIC_CLASSES_SEPARATOR + Joiner .on (GENERIC_CLASSES_SEPARATOR ).join (classNames );
482494 }
483495 return simpleName ;
484496 }
485497
498+ private Stream <String > nestedNames (Class <?> returnType ) {
499+ if (returnType .getEnclosingClass () == null ) {
500+ return Stream .of (returnType .getSimpleName ());
501+ }
502+ return Stream .concat (nestedNames (returnType .getEnclosingClass ()), Stream .of (returnType .getSimpleName ()));
503+ }
504+
486505 private TypeSchema extractInlineModels (Map <String , Object > genericTypeSchema ) {
487506 if (isObjectSchema (genericTypeSchema )) {
488507 final Map <String , Object > newGenericTypeSchema = new HashMap <>(genericTypeSchema );
489508 final Map <String , Object > models = new HashMap <>();
490- if (genericTypeSchema .get ("properties" ) instanceof Map ) {
491- final Map <String , Object > properties = (Map <String , Object >) genericTypeSchema .get ("properties" );
509+ if (genericTypeSchema .get (PROPERTIES ) instanceof Map ) {
510+ final Map <String , Object > properties = (Map <String , Object >) genericTypeSchema .get (PROPERTIES );
492511 final Map <String , Object > newProperties = properties .entrySet ().stream ().map (entry -> {
493512 final Map <String , Object > property = (Map <String , Object >) entry .getValue ();
494513 final TypeSchema propertySchema = extractInlineModels (property );
495514 models .putAll (propertySchema .models ());
515+ final Map <String , Object > type = reuseTypeRef (propertySchema .type ());
496516 if (propertySchema .name () == null ) {
497- return new AbstractMap .SimpleEntry <String , Object >(entry .getKey (), propertySchema . type () );
517+ return new AbstractMap .SimpleEntry <>(entry .getKey (), type );
498518 }
499519 if (propertySchema .type () != null ) {
500- models .put (propertySchema .name (), propertySchema . type () );
520+ models .put (propertySchema .name (), type );
501521 }
502- return new AbstractMap .SimpleEntry <String , Object >(entry .getKey (), Collections .singletonMap ("$ref" , propertySchema .name ()));
522+ return new AbstractMap .SimpleEntry <String , Object >(entry .getKey (), Collections .singletonMap (REF , propertySchema .name ()));
503523 })
504524 .collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
505- newGenericTypeSchema .put ("properties" , newProperties );
525+ newGenericTypeSchema .put (PROPERTIES , newProperties );
506526 }
507- if (genericTypeSchema .get ("additional_properties" ) instanceof Map ) {
508- final Map <String , Object > additionalProperties = (Map <String , Object >) genericTypeSchema .get ("additional_properties" );
527+ if (genericTypeSchema .get (ADDITIONAL_PROPERTIES ) instanceof Map ) {
528+ final Map <String , Object > additionalProperties = (Map <String , Object >) genericTypeSchema .get (ADDITIONAL_PROPERTIES );
509529 final TypeSchema itemsSchema = extractInlineModels (additionalProperties );
510530 models .putAll (itemsSchema .models ());
511531 if (itemsSchema .name () != null ) {
512532 if (itemsSchema .type () != null ) {
513533 models .put (itemsSchema .name (), itemsSchema .type ());
514534 }
515- newGenericTypeSchema .put ("additional_properties" , Collections .singletonMap ("$ref" , itemsSchema .name ()));
535+ newGenericTypeSchema .put (ADDITIONAL_PROPERTIES , Collections .singletonMap (REF , itemsSchema .name ()));
516536 } else {
517537 if (itemsSchema .type () != null ) {
518- newGenericTypeSchema .put ("additional_properties" , itemsSchema .type ());
538+ final Map <String , Object > type = reuseTypeRef (itemsSchema .type ());
539+ newGenericTypeSchema .put (ADDITIONAL_PROPERTIES , itemsSchema .type ());
519540 }
520541 }
521542 }
522543
523- if (!genericTypeSchema .containsKey ("properties" )) {
524- newGenericTypeSchema .put ("properties" , Collections .emptyMap ());
544+ if (!genericTypeSchema .containsKey (PROPERTIES )) {
545+ newGenericTypeSchema .put (PROPERTIES , Collections .emptyMap ());
525546 }
526- final String id = shortenJsonSchemaURN ((String ) genericTypeSchema .get ("id" ));
547+ final String id = shortenJsonSchemaURNs ((String ) genericTypeSchema .get ("id" ));
527548 return createTypeSchema (id , newGenericTypeSchema , models );
528549 }
529550
530551 if (isArraySchema (genericTypeSchema )) {
531552 final Map <String , Object > models = new HashMap <>();
532553 final Map <String , Object > newGenericTypeSchema = new HashMap <>(genericTypeSchema );
533- if (genericTypeSchema .get ("items" ) instanceof Map ) {
534- final Map <String , Object > items = (Map <String , Object >) genericTypeSchema .get ("items" );
554+ if (genericTypeSchema .get (ITEMS ) instanceof Map ) {
555+ final Map <String , Object > items = (Map <String , Object >) genericTypeSchema .get (ITEMS );
535556 final TypeSchema itemsSchema = extractInlineModels (items );
536557 models .putAll (itemsSchema .models ());
537558 if (itemsSchema .name () != null ) {
538559 if (itemsSchema .type () != null ) {
539560 models .put (itemsSchema .name (), itemsSchema .type ());
540561 }
541- newGenericTypeSchema .put ("items" , Collections .singletonMap ("$ref" , itemsSchema .name ()));
562+ newGenericTypeSchema .put (ITEMS , Collections .singletonMap (REF , itemsSchema .name ()));
563+ } else {
564+ final Map <String , Object > type = reuseTypeRef (itemsSchema .type ());
565+ newGenericTypeSchema .put (ITEMS , type );
542566 }
543567 }
544568 return createTypeSchema (null , newGenericTypeSchema , models );
545569 }
546570 return createTypeSchema (null , genericTypeSchema , Collections .emptyMap ());
547571 }
548572
573+ private Map <String , Object > reuseTypeRef (Map <String , Object > type ) {
574+ if (type .get (REF ) != null ) {
575+ type .put (REF , shortenJsonSchemaURNs ((String ) type .get (REF )));
576+ }
577+
578+ return type ;
579+ }
580+
581+ private static final Pattern IDENT =
582+ Pattern .compile ("[A-Za-z_][A-Za-z0-9_$:]*" );
583+
584+ private static final Set <String > KEYWORDS =
585+ Set .of ("extends" , "super" );
586+
587+ private List <String > splitIfGeneric (String genericFqcn ) {
588+ if (genericFqcn == null ) {
589+ return null ;
590+ }
591+ final List <String > result = new ArrayList <>();
592+ final var m = IDENT .matcher (genericFqcn );
593+ while (m .find ()) {
594+ final var token = m .group ();
595+ if (!KEYWORDS .contains (token ) && !token .equals ("?" )) {
596+ result .add (token );
597+ }
598+ }
599+ return result ;
600+ }
601+
602+ private String shortenJsonSchemaURNs (@ Nullable String id ) {
603+ final var genericParts = splitIfGeneric (id );
604+ return genericParts != null ? genericParts .stream ().map (this ::shortenJsonSchemaURN ).collect (Collectors .joining (GENERIC_CLASSES_SEPARATOR )) : null ;
605+ }
549606 private String shortenJsonSchemaURN (@ Nullable String id ) {
550607 if (id == null ) {
551608 return null ;
552609 }
553610 final Splitter splitter = Splitter .on (":" );
554611 final List <String > segments = splitter .splitToList (id );
555- return segments .size () > 0
556- ? segments .get (segments .size () - 1 )
557- : id ;
612+ if (segments .isEmpty ()) {
613+ return id ;
614+ }
615+ return segments .stream ()
616+ .filter (segment -> Character .isUpperCase (segment .codePointAt (0 )))
617+ .collect (Collectors .joining (INNER_CLASSES_SEPARATOR ));
558618 }
559619
560620 private static Optional <String > typeOfSchema (@ Nullable Map <String , Object > typeSchema ) {
561621 return Optional .ofNullable (typeSchema )
562- .map (schema -> Strings .emptyToNull ((String ) schema .get ("type" )));
622+ .map (schema -> Strings .emptyToNull ((String ) schema .get (TYPE )));
563623 }
564624
565625 private static boolean isArraySchema (Map <String , Object > genericTypeSchema ) {
@@ -576,13 +636,13 @@ private Map<String, Object> schemaForType(Type valueType) {
576636 try {
577637 final JsonSchema schema = schemaGenerator .generateSchema (mapper .getTypeFactory ().constructType (valueType ));
578638 final Map <String , Object > schemaMap = mapper .readValue (mapper .writeValueAsBytes (schema ), Map .class );
579- if (schemaMap .containsKey ("additional_properties" ) && !schemaMap .containsKey ("properties" )) {
580- schemaMap .put ("properties" , Collections .emptyMap ());
639+ if (schemaMap .containsKey (ADDITIONAL_PROPERTIES ) && !schemaMap .containsKey (PROPERTIES )) {
640+ schemaMap .put (PROPERTIES , Collections .emptyMap ());
581641 }
582- if (schemaMap .equals (Collections .singletonMap ("type" , "any" ))) {
642+ if (schemaMap .equals (Collections .singletonMap (TYPE , "any" ))) {
583643 return ImmutableMap .of (
584- "type" , "object" ,
585- "properties" , Collections .emptyMap ()
644+ TYPE , "object" ,
645+ PROPERTIES , Collections .emptyMap ()
586646 );
587647 }
588648 return schemaMap ;
@@ -706,11 +766,11 @@ private List<Map<String, Object>> determineResponses(Method method) {
706766
707767 // Leading slash but no trailing.
708768 private String cleanRoute (String route ) {
709- if (!route .startsWith ("/" )) {
710- route = "/" + route ;
769+ if (!route .startsWith (ROUTE_SEPARATOR )) {
770+ route = ROUTE_SEPARATOR + route ;
711771 }
712772
713- if (route .endsWith ("/" )) {
773+ if (route .endsWith (ROUTE_SEPARATOR )) {
714774 route = route .substring (0 , route .length () - 1 );
715775 }
716776
@@ -876,7 +936,7 @@ public Map<String, Object> jsonValue() {
876936 }
877937
878938 if (typeSchema .type () == null || isObjectSchema (typeSchema .type ())) {
879- result .put ("type" , typeSchema .name ());
939+ result .put (TYPE , typeSchema .name ());
880940 } else {
881941 result .putAll (typeSchema .type ());
882942 }
0 commit comments