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}