1010import lombok .Getter ;
1111import lombok .Setter ;
1212
13- import java .util .ArrayList ;
14- import java .util .HashMap ;
15- import java .util .List ;
16- import java .util .Map ;
17- import java .util .stream .Collectors ;
13+ import java .util .*;
14+ import java .util .concurrent .atomic .AtomicBoolean ;
1815
19- import static de .sonallux .spotify .core .model .SpotifyWebApiEndpoint .ParameterLocation .*;
16+ import static java .util .stream .Collectors .joining ;
17+ import static java .util .stream .Collectors .toList ;
2018
2119public class CategoryTemplate extends AbstractTemplate <SpotifyWebApiCategory > {
2220
@@ -41,7 +39,7 @@ Map<String, Object> buildContext(SpotifyWebApiCategory category, Map<String, Obj
4139 context .put ("name" , category .getName ());
4240 context .put ("className" , JavaUtils .getClassName (category ));
4341 context .put ("documentationLink" , category .getLink ());
44- context .put ("endpoints" , category .getEndpointList ().stream ().flatMap (e -> buildEndpointContext (e ).stream ()).collect (Collectors . toList ()));
42+ context .put ("endpoints" , category .getEndpointList ().stream ().flatMap (e -> buildEndpointContext (e ).stream ()).collect (toList ()));
4543 return context ;
4644 }
4745
@@ -66,106 +64,143 @@ private List<Map<String, Object>> buildEndpointContext(SpotifyWebApiEndpoint end
6664 baseContext .put ("responseType" , getResponseType (endpoint ));
6765 baseContext .put ("documentationLink" , endpoint .getLink ());
6866
69- List <Map <String , Object >> contexts = new ArrayList <>();
70- var arguments = getArguments (endpoint );
71- for (var args : arguments ) {
72- var context = new HashMap <>(baseContext );
73- context .put ("arguments" , args .stream ().map (Argument ::asMethodArgument ).collect (Collectors .joining (", " )));
74- context .put ("javaDocParams" , args .stream ().map (Argument ::asJavaDoc ).collect (Collectors .toList ()));
75- if (endpoint .getHttpMethod ().equals ("DELETE" ) && args .stream ().anyMatch (a -> a .getAnnotation ().startsWith ("@Body" ))) {
76- // Officially DELETE does not allow a request body, but Spotify uses it.
77- // This adjusts the http method annotation, so retrofit does not throw an error.
78- context .put ("deleteWithBody" , true );
79- }
80- contexts .add (context );
81- }
82- return contexts ;
83- }
84-
85- private List <List <Argument >> getArguments (SpotifyWebApiEndpoint endpoint ) {
8667 EndpointRequestBodyHelper .fixDuplicateEndpointParameters (endpoint );
8768
88- List <Argument > requiredArgs = new ArrayList <>();
89- endpoint .getParameters ().stream ()
90- .filter (p -> p .getLocation () == PATH )
91- .map (p -> new Argument ("@Path(\" " + p .getName () + "\" )" , JavaUtils .mapToPrimitiveJavaType (p .getType ()), p .getName (), p .getDescription ()))
92- .forEach (requiredArgs ::add );
93-
94- endpoint .getParameters ().stream ()
95- .filter (p -> p .getLocation () == QUERY && p .isRequired ())
96- .map (p -> new Argument ("@Query(\" " + p .getName () + "\" )" , JavaUtils .mapToPrimitiveJavaType (p .getType ()), p .getName (), p .getDescription ()))
97- .forEach (requiredArgs ::add );
98-
99- boolean requestBodyArgAdded = false ;
100- if (endpoint .getParameters ().stream ().anyMatch (p -> p .getLocation () == BODY && p .isRequired ())) {
101- requestBodyArgAdded = true ;
102- requiredArgs .add (new Argument (
103- "@Body" , EndpointRequestBodyHelper .getEndpointRequestBodyName (endpoint ), "requestBody" , "the request body" ));
69+ var rawArguments = argumentsFromEndpoint (endpoint );
70+ List <Parameter > requiredParameters = new ArrayList <>();
71+ List <Parameter > optionalParameters = new ArrayList <>();
72+ for (var rawArgument : rawArguments ) {
73+ if (rawArgument .isRequired ()) {
74+ requiredParameters .add (rawArgument );
75+ } else {
76+ optionalParameters .add (rawArgument );
77+ }
10478 }
10579
106- List <List <Argument >> args = new ArrayList <>();
107- args .add (new ArrayList <>(requiredArgs ));
108-
109- var optionalQueryArgs = endpoint .getParameters ().stream ().filter (p -> p .getLocation () == QUERY && !p .isRequired ()).collect (Collectors .toList ());
110- if (optionalQueryArgs .size () == 1 ) {
111- var p = optionalQueryArgs .get (0 );
112- requiredArgs .add (new Argument (
113- "@Query(\" " + p .getName () + "\" )" , JavaUtils .mapToPrimitiveJavaType (p .getType ()), p .getName (), p .getDescription ()));
114- args .add (requiredArgs );
115- } else if (optionalQueryArgs .size () > 1 ) {
116- requiredArgs .add (new Argument (
117- "@QueryMap" , "java.util.Map<String, Object>" , "queryParameters" , "A map of optional query parameters" ));
118- args .add (requiredArgs );
80+ requiredParameters .sort (Comparator .comparing (Parameter ::getOrder ));
81+ optionalParameters .sort (Comparator .comparing (Parameter ::getOrder ));
82+
83+ if (endpoint .getHttpMethod ().equals ("DELETE" ) && rawArguments .stream ().anyMatch (a -> a .getAnnotation ().startsWith ("@Body" ))) {
84+ // Officially DELETE does not allow a request body, but Spotify uses it.
85+ // This adjusts the http method annotation, so retrofit does not throw an error.
86+ baseContext .put ("deleteWithBody" , true );
11987 }
12088
121- if (!requestBodyArgAdded && endpoint .getParameters ().stream ().anyMatch (p -> p .getLocation () == BODY )) {
122- List <List <Argument >> endpointArguments = new ArrayList <>();
123- for (var arguments : args ) {
124- var duplicateArguments = new ArrayList <>(arguments );
125- duplicateArguments .add (new Argument (
126- "@Body" , EndpointRequestBodyHelper .getEndpointRequestBodyName (endpoint ), "requestBody" , "The request body" ));
127- endpointArguments .add (arguments );
128- endpointArguments .add (duplicateArguments );
89+ List <Parameter > allParameters ;
90+ if (optionalParameters .size () == 0 ) {
91+ baseContext .put ("requiredParametersMethod" , false );
92+ allParameters = requiredParameters ;
93+ } else {
94+ allParameters = new ArrayList <>(requiredParameters );
95+ var arguments = requiredParameters .stream ().map (Parameter ::getFieldName ).collect (toList ());
96+
97+ // If multiple query parameters are optional, wrap them together in one @QueryMap parameter
98+ var optionalArgumentsWithoutQuery = optionalParameters .stream ().filter (a -> !a .getAnnotation ().startsWith ("@Query" )).collect (toList ());
99+ if (optionalParameters .size () - optionalArgumentsWithoutQuery .size () > 1 ) {
100+ allParameters .add (new Parameter ("@QueryMap" , "java.util.Map<String, Object>" , "queryParameters" , "A map of optional query parameters" , false , 4 ));
101+ arguments .add ("java.util.Map.of()" );
102+ optionalParameters = optionalArgumentsWithoutQuery ;
129103 }
130- return endpointArguments ;
104+
105+ optionalParameters .forEach (optionalArg -> {
106+ allParameters .add (optionalArg );
107+ arguments .add (getDefaultArgumentValue (endpoint , optionalArg ));
108+ });
109+ baseContext .put ("requiredParametersMethod" , true );
110+ baseContext .put ("requiredParameters" , requiredParameters .stream ().map (Parameter ::asMethodArgumentWithoutAnnotation ).collect (joining (", " )));
111+ baseContext .put ("requiredJavaDocParameters" , requiredParameters .stream ().map (Parameter ::asJavaDoc ).collect (toList ()));
112+ baseContext .put ("arguments" , String .join (", " , arguments ));
131113 }
132114
133- return args ;
115+ baseContext .put ("parameters" , allParameters .stream ().map (Parameter ::asMethodArgument ).collect (joining (", " )));
116+ baseContext .put ("javaDocParameters" , allParameters .stream ().map (Parameter ::asJavaDoc ).collect (toList ()));
117+ return List .of (baseContext );
118+ }
119+
120+ private String getDefaultArgumentValue (SpotifyWebApiEndpoint endpoint , Parameter parameter ) {
121+ if (parameter .getAnnotation ().startsWith ("@Body" )) {
122+ return "new " + EndpointRequestBodyHelper .getEndpointRequestBodyName (endpoint ) + "()" ;
123+ } else {
124+ return "null" ;
125+ }
134126 }
135127
136128 private String getResponseType (SpotifyWebApiEndpoint endpoint ) {
137- if (endpoint .getResponseTypes ().size () == 1
138- || 1 == endpoint . getResponseTypes (). stream ( )
139- . map ( SpotifyWebApiEndpoint . ResponseType :: getType ). distinct ().count ()) {
129+ if (endpoint .getResponseTypes ().stream ()
130+ . map ( SpotifyWebApiEndpoint . ResponseType :: getType )
131+ . distinct ().count () == 1 ) {
140132 return JavaUtils .mapToPrimitiveJavaType (endpoint .getResponseTypes ().get (0 ).getType ());
141133 }
142134 var nonVoidResponseTypes = endpoint .getResponseTypes ().stream ()
143- .map (SpotifyWebApiEndpoint .ResponseType ::getType ).filter (t -> !"Void" .equals (t )).distinct ().collect (Collectors . toList ());
135+ .map (SpotifyWebApiEndpoint .ResponseType ::getType ).filter (t -> !"Void" .equals (t )).distinct ().collect (toList ());
144136 if (nonVoidResponseTypes .size () == 1 ) {
145137 return JavaUtils .mapToPrimitiveJavaType (endpoint .getResponseTypes ().get (0 ).getType ());
146138 }
147139 return "" ;
148140 }
149141
142+ private List <Parameter > argumentsFromEndpoint (SpotifyWebApiEndpoint endpoint ) {
143+ var hasBodyParameter = new AtomicBoolean (false );
144+ var hasRequiredBodyParameter = new AtomicBoolean (false );
145+
146+ var arguments = endpoint .getParameters ().stream ().map (parameter -> {
147+ switch (parameter .getLocation ()) {
148+ case PATH : {
149+ return new Parameter ("@Path(\" " + parameter .getName () + "\" )" , parameter , 1 );
150+ }
151+ case QUERY : {
152+ return new Parameter ("@Query(\" " + parameter .getName () + "\" )" , parameter , 2 );
153+ }
154+ case BODY : {
155+ hasBodyParameter .set (true );
156+ if (parameter .isRequired ()) {
157+ hasRequiredBodyParameter .set (true );
158+ }
159+ return null ;
160+ }
161+ case HEADER : // Ignore header parameters because they are only Authorization and Content-Type header
162+ default : return null ;
163+ }
164+ }).filter (Objects ::nonNull ).collect (toList ());
165+
166+ if (hasBodyParameter .get ()) {
167+ var requestBodyType = EndpointRequestBodyHelper .getEndpointRequestBodyName (endpoint );
168+ arguments .add (new Parameter ("@Body" , requestBodyType , "requestBody" , "The request body" , hasRequiredBodyParameter .get (), 3 ));
169+ }
170+ return arguments ;
171+ }
172+
150173 @ Getter
151174 @ Setter
152- private static class Argument {
175+ private static class Parameter {
153176 private String annotation ;
154177 private String type ;
155178 private String fieldName ;
156179 private String description ;
180+ private boolean isRequired ;
181+ private int order ;
157182
158- public Argument (String annotation , String type , String fieldName , String description ) {
183+ public Parameter (String annotation , String type , String fieldName , String description , boolean isRequired , int order ) {
159184 this .annotation = annotation ;
160185 this .type = type ;
161186 this .fieldName = JavaUtils .escapeFieldName (fieldName );
162187 this .description = Markdown2Html .convertToSingleLine (description );
188+ this .isRequired = isRequired ;
189+ this .order = order ;
190+ }
191+
192+ private Parameter (String annotation , SpotifyWebApiEndpoint .Parameter parameter , int order ) {
193+ // Do not map type to primitive type, so we can pass null to it if it is optional
194+ this (annotation , JavaUtils .mapToJavaType (parameter .getType ()), parameter .getName (), parameter .getDescription (), parameter .isRequired (), order );
163195 }
164196
165197 public String asMethodArgument () {
166198 return annotation + " " + type + " " + fieldName ;
167199 }
168200
201+ public String asMethodArgumentWithoutAnnotation () {
202+ return type + " " + fieldName ;
203+ }
169204 public String asJavaDoc () {
170205 return "@param " + fieldName + " " + description ;
171206 }
0 commit comments