001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
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 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
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 */
016package io.jboot.support.swagger;
017
018import com.fasterxml.jackson.databind.JavaType;
019import com.fasterxml.jackson.databind.type.TypeFactory;
020import com.google.common.base.Function;
021import com.google.common.base.Predicate;
022import com.google.common.base.Splitter;
023import com.google.common.collect.Collections2;
024import com.jfinal.log.Log;
025import io.swagger.annotations.*;
026import io.swagger.converter.ModelConverters;
027import io.swagger.models.*;
028import io.swagger.models.parameters.*;
029import io.swagger.models.properties.ArrayProperty;
030import io.swagger.models.properties.MapProperty;
031import io.swagger.models.properties.Property;
032import io.swagger.models.properties.RefProperty;
033import io.swagger.util.BaseReaderUtils;
034import io.swagger.util.ParameterProcessor;
035import io.swagger.util.PathUtils;
036import io.swagger.util.ReflectionUtils;
037import org.apache.commons.lang3.StringUtils;
038
039import java.lang.annotation.Annotation;
040import java.lang.reflect.Method;
041import java.lang.reflect.Type;
042import java.util.*;
043
044public class ControllerReaderExtension {
045
046    private static final Log LOGGER = Log.getLog(ControllerReaderExtension.class);
047    private static final String SUCCESSFUL_OPERATION = "successful operation";
048
049    private static <T> List<T> parseAnnotationValues(String str, Function<String, T> processor) {
050        final List<T> result = new ArrayList<T>();
051        for (String item : Splitter.on(",").trimResults().omitEmptyStrings().split(str)) {
052            result.add(processor.apply(item));
053        }
054        return result;
055    }
056
057    private static List<String> parseStringValues(String str) {
058        return parseAnnotationValues(str, new Function<String, String>() {
059
060            @Override
061            public String apply(String value) {
062                return value;
063            }
064        });
065    }
066
067    private static List<Scheme> parseSchemes(String schemes) {
068        final List<Scheme> result = new ArrayList<Scheme>();
069        for (String item : StringUtils.trimToEmpty(schemes).split(",")) {
070            final Scheme scheme = Scheme.forValue(StringUtils.trimToNull(item));
071            if (scheme != null && !result.contains(scheme)) {
072                result.add(scheme);
073            }
074        }
075        return result;
076    }
077
078    private static List<SecurityRequirement> parseAuthorizations(Authorization[] authorizations) {
079        final List<SecurityRequirement> result = new ArrayList<SecurityRequirement>();
080        for (Authorization auth : authorizations) {
081            if (StringUtils.isNotEmpty(auth.value())) {
082                final SecurityRequirement security = new SecurityRequirement();
083                security.setName(auth.value());
084                for (AuthorizationScope scope : auth.scopes()) {
085                    if (StringUtils.isNotEmpty(scope.scope())) {
086                        security.addScope(scope.scope());
087                    }
088                }
089                result.add(security);
090            }
091        }
092        return result;
093    }
094
095    private static Map<String, Property> parseResponseHeaders(Swagger swagger, ReaderContext context, ResponseHeader[] headers) {
096        Map<String, Property> responseHeaders = null;
097        for (ResponseHeader header : headers) {
098            final String name = header.name();
099            if (StringUtils.isNotEmpty(name)) {
100                if (responseHeaders == null) {
101                    responseHeaders = new HashMap<String, Property>();
102                }
103                final Class<?> cls = header.response();
104                if (!ReflectionUtils.isVoid(cls)) {
105                    final Property property = ModelConverters.getInstance().readAsProperty(cls);
106                    if (property != null) {
107                        final Property responseProperty = ContainerWrapper.wrapContainer(header.responseContainer(),
108                                property, ContainerWrapper.ARRAY, ContainerWrapper.LIST, ContainerWrapper.SET);
109                        responseProperty.setDescription(header.description());
110                        responseHeaders.put(name, responseProperty);
111                        appendModels(swagger, cls);
112                    }
113                }
114            }
115        }
116        return responseHeaders;
117    }
118
119    private static void appendModels(Swagger swagger, Type type) {
120        final Map<String, Model> models = ModelConverters.getInstance().readAll(type);
121        for (Map.Entry<String, Model> entry : models.entrySet()) {
122            swagger.model(entry.getKey(), entry.getValue());
123        }
124    }
125
126    private static boolean isValidResponse(Type type) {
127        final JavaType javaType = TypeFactory.defaultInstance().constructType(type);
128        return !ReflectionUtils.isVoid(javaType);
129    }
130
131    private static Type getResponseType(Method method) {
132        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
133        if (apiOperation != null && !ReflectionUtils.isVoid(apiOperation.response())) {
134            return apiOperation.response();
135        } else {
136            return method.getGenericReturnType();
137        }
138    }
139
140    private static String getResponseContainer(ApiOperation apiOperation) {
141        return apiOperation == null ? null : StringUtils.defaultIfBlank(apiOperation.responseContainer(), null);
142    }
143
144
145    public int getPriority() {
146        return 0;
147    }
148
149
150    public boolean isReadable(ReaderContext context) {
151        final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
152        return apiAnnotation != null && (context.isReadHidden() || !apiAnnotation.hidden());
153    }
154
155
156    public void applyConsumes(ReaderContext context, Operation operation, Method method) {
157        final List<String> consumes = new ArrayList<String>();
158        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
159
160        if (apiOperation != null) {
161            consumes.addAll(parseStringValues(apiOperation.consumes()));
162        }
163
164        if (consumes.isEmpty()) {
165            final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
166            if (apiAnnotation != null) {
167                consumes.addAll(parseStringValues(apiAnnotation.consumes()));
168            }
169            consumes.addAll(context.getParentConsumes());
170        }
171
172        for (String consume : consumes) {
173            operation.consumes(consume);
174        }
175    }
176
177
178    public void applyProduces(ReaderContext context, Operation operation, Method method) {
179        final List<String> produces = new ArrayList<String>();
180        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
181
182        if (apiOperation != null) {
183            produces.addAll(parseStringValues(apiOperation.produces()));
184        }
185
186        if (produces.isEmpty()) {
187            final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
188            if (apiAnnotation != null) {
189                produces.addAll(parseStringValues(apiAnnotation.produces()));
190            }
191            produces.addAll(context.getParentProduces());
192        }
193
194        for (String produce : produces) {
195            operation.produces(produce);
196        }
197    }
198
199
200    public String getHttpMethod(ReaderContext context, Method method) {
201        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
202        return apiOperation == null || StringUtils.isEmpty(apiOperation.httpMethod()) ?
203                context.getParentHttpMethod() : apiOperation.httpMethod();
204    }
205
206
207    public String getPath(ReaderContext context, Method method) {
208        final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
209        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
210        final String operationPath = apiOperation == null ? null : apiOperation.nickname();
211        return PathUtils.collectPath(context.getParentPath(),
212                apiAnnotation == null ? null : apiAnnotation.value(),
213                StringUtils.isBlank(operationPath) ? method.getName() : operationPath);
214    }
215
216
217    public void applyOperationId(Operation operation, Method method) {
218        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
219        if (apiOperation != null && StringUtils.isNotBlank(apiOperation.nickname())) {
220            operation.operationId(apiOperation.nickname());
221        } else {
222            operation.operationId(method.getName());
223        }
224    }
225
226
227    public void applySummary(Operation operation, Method method) {
228        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
229        if (apiOperation != null && StringUtils.isNotBlank(apiOperation.value())) {
230            operation.summary(apiOperation.value());
231        }
232    }
233
234
235    public void applyDescription(Operation operation, Method method) {
236        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
237        if (apiOperation != null && StringUtils.isNotBlank(apiOperation.notes())) {
238            operation.description(apiOperation.notes());
239        }
240    }
241
242
243    public void applySchemes(ReaderContext context, Operation operation, Method method) {
244        final List<Scheme> schemes = new ArrayList<Scheme>();
245        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
246        final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
247
248        if (apiOperation != null) {
249            schemes.addAll(parseSchemes(apiOperation.protocols()));
250        }
251
252        if (schemes.isEmpty() && apiAnnotation != null) {
253            schemes.addAll(parseSchemes(apiAnnotation.protocols()));
254        }
255
256        for (Scheme scheme : schemes) {
257            operation.scheme(scheme);
258        }
259    }
260
261
262    public void setDeprecated(Operation operation, Method method) {
263        operation.deprecated(ReflectionUtils.getAnnotation(method, Deprecated.class) != null);
264    }
265
266
267    public void applySecurityRequirements(ReaderContext context, Operation operation, Method method) {
268        final List<SecurityRequirement> securityRequirements = new ArrayList<SecurityRequirement>();
269        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
270        final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
271
272        if (apiOperation != null) {
273            securityRequirements.addAll(parseAuthorizations(apiOperation.authorizations()));
274        }
275
276        if (securityRequirements.isEmpty() && apiAnnotation != null) {
277            securityRequirements.addAll(parseAuthorizations(apiAnnotation.authorizations()));
278        }
279
280        for (SecurityRequirement securityRequirement : securityRequirements) {
281            operation.security(securityRequirement);
282        }
283    }
284
285
286    public void applyTags(ReaderContext context, Operation operation, Method method) {
287        final List<String> tags = new ArrayList<String>();
288
289        final Api apiAnnotation = context.getCls().getAnnotation(Api.class);
290        if (apiAnnotation != null) {
291            tags.addAll(Collections2.filter(Arrays.asList(apiAnnotation.tags()), new Predicate<String>() {
292
293                @Override
294                public boolean apply(String input) {
295                    return StringUtils.isNotBlank(input);
296                }
297            }));
298        }
299        tags.addAll(context.getParentTags());
300
301        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
302        if (apiOperation != null) {
303            tags.addAll(Collections2.filter(Arrays.asList(apiOperation.tags()), new Predicate<String>() {
304
305                @Override
306                public boolean apply(String input) {
307                    return StringUtils.isNotBlank(input);
308                }
309            }));
310        }
311
312        for (String tag : tags) {
313            operation.tag(tag);
314        }
315    }
316
317
318    public void applyResponses(Swagger swagger, ReaderContext context, Operation operation, Method method) {
319        final Map<Integer, Response> result = new HashMap<Integer, Response>();
320
321        final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
322        if (apiOperation != null && StringUtils.isNotBlank(apiOperation.responseReference())) {
323            final Response response = new Response().description(SUCCESSFUL_OPERATION);
324            response.schema(new RefProperty(apiOperation.responseReference()));
325            result.put(apiOperation.code(), response);
326        }
327
328        final Type responseType = getResponseType(method);
329        if (isValidResponse(responseType)) {
330            final Property property = ModelConverters.getInstance().readAsProperty(responseType);
331            if (property != null) {
332                final Property responseProperty = ContainerWrapper.wrapContainer(getResponseContainer(apiOperation), property);
333                final int responseCode = apiOperation == null ? 200 : apiOperation.code();
334                final Map<String, Property> defaultResponseHeaders = apiOperation == null ?
335                        Collections.<String, Property>emptyMap() :
336                        parseResponseHeaders(swagger, context, apiOperation.responseHeaders());
337                final Response response = new Response()
338                        .description(SUCCESSFUL_OPERATION)
339                        .schema(responseProperty)
340                        .headers(defaultResponseHeaders);
341                result.put(responseCode, response);
342                appendModels(swagger, responseType);
343            }
344        }
345
346        final ApiResponses responseAnnotation = ReflectionUtils.getAnnotation(method, ApiResponses.class);
347        if (responseAnnotation != null) {
348            for (ApiResponse apiResponse : responseAnnotation.value()) {
349                final Map<String, Property> responseHeaders = parseResponseHeaders(swagger, context, apiResponse.responseHeaders());
350
351                final Response response = new Response()
352                        .description(apiResponse.message())
353                        .headers(responseHeaders);
354
355                if (StringUtils.isNotEmpty(apiResponse.reference())) {
356                    response.schema(new RefProperty(apiResponse.reference()));
357                } else if (!ReflectionUtils.isVoid(apiResponse.response())) {
358                    final Type type = apiResponse.response();
359                    final Property property = ModelConverters.getInstance().readAsProperty(type);
360                    if (property != null) {
361                        response.schema(ContainerWrapper.wrapContainer(apiResponse.responseContainer(), property));
362                        appendModels(swagger, type);
363                    }
364                }
365                result.put(apiResponse.code(), response);
366            }
367        }
368
369        for (Map.Entry<Integer, Response> responseEntry : result.entrySet()) {
370            if (responseEntry.getKey() == 0) {
371                operation.defaultResponse(responseEntry.getValue());
372            } else {
373                operation.response(responseEntry.getKey(), responseEntry.getValue());
374            }
375        }
376    }
377
378
379    public void applyParameters(String httpMethod, ReaderContext context, Operation operation, Annotation[] annotations) {
380
381        for (Annotation annotation : annotations) {
382            if (annotation instanceof ApiParam) {
383                ApiParam apiParam = (ApiParam) annotation;
384
385                if ("get".equalsIgnoreCase(httpMethod)) {
386
387                    QueryParameter parameter = new QueryParameter();
388                    parameter.setAccess(apiParam.access());
389                    parameter.setName(apiParam.name());
390                    parameter.setAllowEmptyValue(apiParam.allowEmptyValue());
391                    parameter.setRequired(apiParam.required());
392                    parameter.setDefault(apiParam.value());
393                    parameter.setDefaultValue(apiParam.defaultValue());
394                    parameter.setExample(apiParam.example());
395                    parameter.setFormat(apiParam.format());
396                    parameter.setCollectionFormat(apiParam.collectionFormat());
397                    parameter.setDescription(apiParam.value());
398                    operation.addParameter(parameter);
399
400                }
401
402                /**
403                 * post 请求
404                 */
405
406                else if ("post".equalsIgnoreCase(httpMethod)) {
407                    BodyParameter parameter = new BodyParameter();
408                    parameter.setAccess(apiParam.access());
409                    parameter.setName(apiParam.name());
410//                    parameter.setAllowEmptyValue(apiParam.allowEmptyValue());
411                    parameter.setRequired(apiParam.required());
412                    parameter.setDescription(apiParam.value());
413
414                    Map<String, String> examples = new HashMap<>();
415                    Example example = apiParam.examples();
416                    if (example != null) {
417                        ExampleProperty[] exampleProperties = example.value();
418                        for (ExampleProperty ep : exampleProperties) {
419                            examples.put(ep.mediaType(), ep.value());
420                        }
421                    }
422                    parameter.setExamples(examples);
423                    operation.addParameter(parameter);
424                }
425
426            }
427        }
428    }
429
430
431    public void applyImplicitParameters(Swagger swagger, ReaderContext context, Operation operation, Method method) {
432        final ApiImplicitParams implicitParams = method.getAnnotation(ApiImplicitParams.class);
433        if (implicitParams != null && implicitParams.value().length > 0) {
434            for (ApiImplicitParam param : implicitParams.value()) {
435                final Parameter p = readImplicitParam(swagger, param);
436                if (p != null) {
437                    operation.parameter(p);
438                }
439            }
440        }
441    }
442
443
444    public void applyExtensions(ReaderContext context, Operation operation, Method method) {
445        final ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
446        if (apiOperation != null) {
447            operation.getVendorExtensions().putAll(BaseReaderUtils.parseExtensions(apiOperation.extensions()));
448        }
449    }
450
451    private Parameter readImplicitParam(Swagger swagger, ApiImplicitParam param) {
452        final Parameter p = ParameterFactory.createParam(param.paramType());
453        if (p == null) {
454            return null;
455        }
456        final Type type = ReflectionUtils.typeFromString(param.dataType());
457        return ParameterProcessor.applyAnnotations(swagger, p, type == null ? String.class : type,
458                Collections.<Annotation>singletonList(param));
459    }
460
461    enum ParameterFactory {
462        PATH("path") {
463            @Override
464            protected Parameter create() {
465                return new PathParameter();
466            }
467        },
468        QUERY("query") {
469            @Override
470            protected Parameter create() {
471                return new QueryParameter();
472            }
473        },
474        FORM("form") {
475            @Override
476            protected Parameter create() {
477                return new FormParameter();
478            }
479        },
480        FORM_DATA("formData") {
481            @Override
482            protected Parameter create() {
483                return new FormParameter();
484            }
485        },
486        HEADER("header") {
487            @Override
488            protected Parameter create() {
489                return new HeaderParameter();
490            }
491        },
492        BODY("body") {
493            @Override
494            protected Parameter create() {
495                return new BodyParameter();
496            }
497        };
498
499        private final String paramType;
500
501        ParameterFactory(String paramType) {
502            this.paramType = paramType;
503        }
504
505        public static Parameter createParam(String paramType) {
506            for (ParameterFactory item : values()) {
507                if (item.paramType.equalsIgnoreCase(paramType)) {
508                    return item.create();
509                }
510            }
511            LOGGER.warn("Unknown implicit parameter type: [" + paramType + "]");
512            return null;
513        }
514
515        protected abstract Parameter create();
516    }
517
518    enum ContainerWrapper {
519        LIST("list") {
520            @Override
521            protected Property doWrap(Property property) {
522                return new ArrayProperty(property);
523            }
524        },
525        ARRAY("array") {
526            @Override
527            protected Property doWrap(Property property) {
528                return new ArrayProperty(property);
529            }
530        },
531        MAP("map") {
532            @Override
533            protected Property doWrap(Property property) {
534                return new MapProperty(property);
535            }
536        },
537        SET("set") {
538            @Override
539            protected Property doWrap(Property property) {
540                ArrayProperty arrayProperty = new ArrayProperty(property);
541                arrayProperty.setUniqueItems(true);
542                return arrayProperty;
543            }
544        };
545
546        private final String container;
547
548        ContainerWrapper(String container) {
549            this.container = container;
550        }
551
552        public static Property wrapContainer(String container, Property property, ContainerWrapper... allowed) {
553            final Set<ContainerWrapper> tmp = allowed.length > 0 ? EnumSet.copyOf(Arrays.asList(allowed)) : EnumSet.allOf(ContainerWrapper.class);
554            for (ContainerWrapper wrapper : tmp) {
555                final Property prop = wrapper.wrap(container, property);
556                if (prop != null) {
557                    return prop;
558                }
559            }
560            return property;
561        }
562
563        public Property wrap(String container, Property property) {
564            if (this.container.equalsIgnoreCase(container)) {
565                return doWrap(property);
566            }
567            return null;
568        }
569
570        protected abstract Property doWrap(Property property);
571    }
572
573}