diff --git a/backend/src/main/java/fr/inra/urgi/gpds/api/brapi/v1/CallsController.java b/backend/src/main/java/fr/inra/urgi/gpds/api/brapi/v1/CallsController.java index 433007fbf3356230dcfb4eb3af1c3935a639b4ba..9796212845c770a087eff1f25cfafb54faa097eb 100644 --- a/backend/src/main/java/fr/inra/urgi/gpds/api/brapi/v1/CallsController.java +++ b/backend/src/main/java/fr/inra/urgi/gpds/api/brapi/v1/CallsController.java @@ -1,7 +1,6 @@ package fr.inra.urgi.gpds.api.brapi.v1; -import com.google.common.collect.Lists; -import com.google.common.reflect.ClassPath; +import com.google.common.collect.ImmutableSet; import fr.inra.urgi.gpds.domain.brapi.v1.data.BrapiCall; import fr.inra.urgi.gpds.domain.brapi.v1.response.BrapiListResponse; import fr.inra.urgi.gpds.domain.criteria.base.PaginationCriteriaImpl; @@ -9,15 +8,19 @@ import fr.inra.urgi.gpds.domain.data.CallVO; import fr.inra.urgi.gpds.domain.response.ApiResponseFactory; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import org.springframework.web.bind.annotation.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.service.ApiDescription; +import springfox.documentation.service.Documentation; +import springfox.documentation.spring.web.DocumentationCache; +import springfox.documentation.spring.web.plugins.Docket; import javax.validation.Valid; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.*; - -import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * @author gcornut @@ -25,132 +28,81 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; @Api(tags = {"Breeding API"}, description = "BrAPI endpoint") @RestController public class CallsController { + private static final String BRAPI_PATH = "/brapi/v1/"; - private static final List<String> DATA_TYPES = Arrays.asList( + public static final Set<String> DEFAULT_DATA_TYPES = ImmutableSet.of( "json" ); - private static final List<String> BRAPI_VERSIONS = Arrays.asList( + public static final Set<String> DEFAULT_BRAPI_VERSIONS = ImmutableSet.of( "1.0", "1.1", "1.2" ); - private final List<BrapiCall> implementedCalls; + private List<BrapiCall> implementedCalls; + + private final DocumentationCache documentationCache; - public CallsController() { - this.implementedCalls = listImplementedCalls(); + @Autowired + public CallsController(DocumentationCache documentationCache) { + this.documentationCache = documentationCache; } /** * @link https://github.com/plantbreeding/API/blob/master/Specification/Calls/Calls.md */ @ApiOperation("List implemented Breeding API calls") - @GetMapping(value = {"/brapi/v1/calls", "/brapi/v1/"}) + @GetMapping("/brapi/v1/calls") public BrapiListResponse<BrapiCall> calls(@Valid PaginationCriteriaImpl criteria) { + if (implementedCalls == null) { + implementedCalls = swaggerToBrapiCalls(); + } + return ApiResponseFactory.createSubListResponse( criteria.getPageSize(), criteria.getPage(), implementedCalls ); } /** - * Generate {@link BrapiCall} by reflectively reading Spring REST - * annotations + * Get swagger API documentation and transform it to list of BrAPI calls + * + * This must be done after swagger has time to generate the API + * documentation and thus can't be done in this class constructor */ - private List<BrapiCall> listImplementedCalls() { - Map<String, BrapiCall> calls = new HashMap<>(); - - Class<?> aClass = getClass(); - ClassLoader classLoader = aClass.getClassLoader(); - ClassPath classPath; - try { - classPath = ClassPath.from(classLoader); - } catch (IOException e) { - throw new RuntimeException(e); - } - - String brapiControllerPackage = aClass.getPackage().getName(); - Set<ClassPath.ClassInfo> controllerClasses = classPath.getTopLevelClasses(brapiControllerPackage); - for (ClassPath.ClassInfo controllerClassInfo : controllerClasses) { - Class<?> controllerClass = controllerClassInfo.load(); - if (controllerClass.getAnnotation(RestController.class) == null) { - // Class is not a RestController - continue; - } - - for (Method method : controllerClass.getMethods()) { - ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); - if (apiOperation != null && apiOperation.hidden()) { - // Rest call is hidden = ignore - continue; - } - - String[] paths = getCallPath(method); - if (paths == null) { - // No rest path mapping => ignore - continue; - } - for (String path : paths) { - String[] pathSplit = path.split("/brapi/v1/"); - if (pathSplit.length != 2) { - continue; - } - path = pathSplit[1]; - - RequestMethod[] httpMethods = getCallMethods(method); - List<String> httpMethodNames = Lists.newArrayList(); - for (RequestMethod httpMethod : httpMethods) { - httpMethodNames.add(httpMethod.name()); - } - - BrapiCall call = calls.get(path); - if (call == null) { - call = new CallVO(path); - calls.put(path, call); - } - call.getMethods().addAll(httpMethodNames); - call.getDatatypes().addAll(DATA_TYPES); - call.getVersions().addAll(BRAPI_VERSIONS); - } - } - } - - ArrayList<BrapiCall> allCalls = new ArrayList<>(calls.values()); - // Sort by call name - Collections.sort(allCalls, Comparator.comparing(BrapiCall::getCall)); - return allCalls; - } - - private String[] getCallPath(Method method) { - RequestMapping annotation = method.getAnnotation(RequestMapping.class); - if (annotation != null) { - return annotation.value(); - } - GetMapping getAnnotation = method.getAnnotation(GetMapping.class); - if (getAnnotation != null) { - return getAnnotation.value(); - } - PostMapping postAnnotation = method.getAnnotation(PostMapping.class); - if (postAnnotation != null) { - return postAnnotation.value(); - } - return null; - } - - private RequestMethod[] getCallMethods(Method method) { - RequestMapping annotation = method.getAnnotation(RequestMapping.class); - if (annotation != null) { - return annotation.method(); - } - GetMapping getAnnotation = method.getAnnotation(GetMapping.class); - if (getAnnotation != null) { - return new RequestMethod[]{GET}; - } - PostMapping postAnnotation = method.getAnnotation(PostMapping.class); - if (postAnnotation != null) { - return new RequestMethod[]{POST}; - } - return null; + private List<BrapiCall> swaggerToBrapiCalls() { + Documentation apiDocumentation = this.documentationCache.documentationByGroup(Docket.DEFAULT_GROUP_NAME); + + // Get all endpoints + return apiDocumentation.getApiListings().values().stream() + .flatMap(endpointListing -> endpointListing.getApis().stream()) + // Only with BrAPI path + .filter(endpointDescription -> endpointDescription.getPath().startsWith(BRAPI_PATH)) + // Group by endpoint path (ex: /brapi/v1/phenotype => [GET, POST, ...]) + .collect(Collectors.groupingBy(ApiDescription::getPath)) + .entrySet().stream() + // Convert to BrAPI call + .map(endpointGroup -> { + String path = endpointGroup.getKey(); + List<ApiDescription> endpoints = endpointGroup.getValue(); + + // BrAPI call path should not include the base BrAPI path + CallVO call = new CallVO(path.replace(BRAPI_PATH, "")); + + // List every endpoint for current path + Set<String> methods = endpoints.stream() + // List all operations for each endpoint + .flatMap(endpointDescription -> endpointDescription.getOperations().stream()) + // List all methods + .map(operation -> operation.getMethod().toString()) + .collect(Collectors.toSet()); + call.setMethods(methods); + + return call; + }) + // Sort by call name + .sorted(Comparator.comparing(CallVO::getCall)) + .collect(Collectors.toList()); } } diff --git a/backend/src/main/java/fr/inra/urgi/gpds/domain/data/CallVO.java b/backend/src/main/java/fr/inra/urgi/gpds/domain/data/CallVO.java index eb071205e4ebcd0febd915386843c67a3cef20d5..9fbb1a3ee162df076dcf74e80d66c7649dd73233 100644 --- a/backend/src/main/java/fr/inra/urgi/gpds/domain/data/CallVO.java +++ b/backend/src/main/java/fr/inra/urgi/gpds/domain/data/CallVO.java @@ -2,24 +2,23 @@ package fr.inra.urgi.gpds.domain.data; import fr.inra.urgi.gpds.domain.brapi.v1.data.BrapiCall; -import java.util.HashSet; import java.util.Set; +import static fr.inra.urgi.gpds.api.brapi.v1.CallsController.DEFAULT_BRAPI_VERSIONS; +import static fr.inra.urgi.gpds.api.brapi.v1.CallsController.DEFAULT_DATA_TYPES; + /** * @author gcornut */ public class CallVO implements BrapiCall { - private String call; - private Set<String> datatypes; + private final String call; + private Set<String> datatypes = DEFAULT_DATA_TYPES; + private Set<String> versions = DEFAULT_BRAPI_VERSIONS; private Set<String> methods; - private Set<String> versions; public CallVO(String call) { this.call = call; - this.datatypes = new HashSet<>(); - this.methods = new HashSet<>(); - this.versions = new HashSet<>(); } @Override @@ -42,4 +41,15 @@ public class CallVO implements BrapiCall { return versions; } + public void setDatatypes(Set<String> datatypes) { + this.datatypes = datatypes; + } + + public void setMethods(Set<String> methods) { + this.methods = methods; + } + + public void setVersions(Set<String> versions) { + this.versions = versions; + } } diff --git a/backend/src/test/java/fr/inra/urgi/gpds/api/brapi/v1/CallsControllerTest.java b/backend/src/test/java/fr/inra/urgi/gpds/api/brapi/v1/CallsControllerTest.java index e31c71e1d8ab270a1d855244ad3a3cd1d8492d4c..9f7f1fc91ecd63b593afdc8cc6de530916242c65 100644 --- a/backend/src/test/java/fr/inra/urgi/gpds/api/brapi/v1/CallsControllerTest.java +++ b/backend/src/test/java/fr/inra/urgi/gpds/api/brapi/v1/CallsControllerTest.java @@ -3,7 +3,8 @@ package fr.inra.urgi.gpds.api.brapi.v1; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -18,7 +19,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author gcornut */ @ExtendWith(SpringExtension.class) -@WebMvcTest(controllers = CallsController.class) +@SpringBootTest +@AutoConfigureMockMvc class CallsControllerTest { @Autowired