Commit 9c8ec0f0 authored by Raphaël Flores's avatar Raphaël Flores
Browse files

Merge branch 'fix/empty_brapi_calls' into 'master'

Fix unreliable BrAPI calls using the swagger API instead.

See merge request urgi-is/gpds!17
parents e37b3d7e fe2bdd8f
package fr.inra.urgi.gpds.api.brapi.v1; package fr.inra.urgi.gpds.api.brapi.v1;
import com.google.common.collect.Lists; import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import fr.inra.urgi.gpds.domain.brapi.v1.data.BrapiCall; 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.brapi.v1.response.BrapiListResponse;
import fr.inra.urgi.gpds.domain.criteria.base.PaginationCriteriaImpl; import fr.inra.urgi.gpds.domain.criteria.base.PaginationCriteriaImpl;
...@@ -9,15 +8,19 @@ import fr.inra.urgi.gpds.domain.data.CallVO; ...@@ -9,15 +8,19 @@ import fr.inra.urgi.gpds.domain.data.CallVO;
import fr.inra.urgi.gpds.domain.response.ApiResponseFactory; import fr.inra.urgi.gpds.domain.response.ApiResponseFactory;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; 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 javax.validation.Valid;
import java.io.IOException; import java.util.Comparator;
import java.lang.reflect.Method; import java.util.List;
import java.util.*; import java.util.Set;
import java.util.stream.Collectors;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/** /**
* @author gcornut * @author gcornut
...@@ -25,132 +28,81 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; ...@@ -25,132 +28,81 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
@Api(tags = {"Breeding API"}, description = "BrAPI endpoint") @Api(tags = {"Breeding API"}, description = "BrAPI endpoint")
@RestController @RestController
public class CallsController { 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" "json"
); );
private static final List<String> BRAPI_VERSIONS = Arrays.asList( public static final Set<String> DEFAULT_BRAPI_VERSIONS = ImmutableSet.of(
"1.0", "1.0",
"1.1", "1.1",
"1.2" "1.2"
); );
private final List<BrapiCall> implementedCalls; private List<BrapiCall> implementedCalls;
private final DocumentationCache documentationCache;
public CallsController() { @Autowired
this.implementedCalls = listImplementedCalls(); public CallsController(DocumentationCache documentationCache) {
this.documentationCache = documentationCache;
} }
/** /**
* @link https://github.com/plantbreeding/API/blob/master/Specification/Calls/Calls.md * @link https://github.com/plantbreeding/API/blob/master/Specification/Calls/Calls.md
*/ */
@ApiOperation("List implemented Breeding API calls") @ApiOperation("List implemented Breeding API calls")
@GetMapping(value = {"/brapi/v1/calls", "/brapi/v1/"}) @GetMapping("/brapi/v1/calls")
public BrapiListResponse<BrapiCall> calls(@Valid PaginationCriteriaImpl criteria) { public BrapiListResponse<BrapiCall> calls(@Valid PaginationCriteriaImpl criteria) {
if (implementedCalls == null) {
implementedCalls = swaggerToBrapiCalls();
}
return ApiResponseFactory.createSubListResponse( return ApiResponseFactory.createSubListResponse(
criteria.getPageSize(), criteria.getPage(), implementedCalls criteria.getPageSize(), criteria.getPage(), implementedCalls
); );
} }
/** /**
* Generate {@link BrapiCall} by reflectively reading Spring REST * Get swagger API documentation and transform it to list of BrAPI calls
* annotations *
* 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() { private List<BrapiCall> swaggerToBrapiCalls() {
Map<String, BrapiCall> calls = new HashMap<>(); Documentation apiDocumentation = this.documentationCache.documentationByGroup(Docket.DEFAULT_GROUP_NAME);
Class<?> aClass = getClass(); // Get all endpoints
ClassLoader classLoader = aClass.getClassLoader(); return apiDocumentation.getApiListings().values().stream()
ClassPath classPath; .flatMap(endpointListing -> endpointListing.getApis().stream())
try { // Only with BrAPI path
classPath = ClassPath.from(classLoader); .filter(endpointDescription -> endpointDescription.getPath().startsWith(BRAPI_PATH))
} catch (IOException e) { // Group by endpoint path (ex: /brapi/v1/phenotype => [GET, POST, ...])
throw new RuntimeException(e); .collect(Collectors.groupingBy(ApiDescription::getPath))
} .entrySet().stream()
// Convert to BrAPI call
String brapiControllerPackage = aClass.getPackage().getName(); .map(endpointGroup -> {
Set<ClassPath.ClassInfo> controllerClasses = classPath.getTopLevelClasses(brapiControllerPackage); String path = endpointGroup.getKey();
for (ClassPath.ClassInfo controllerClassInfo : controllerClasses) { List<ApiDescription> endpoints = endpointGroup.getValue();
Class<?> controllerClass = controllerClassInfo.load();
if (controllerClass.getAnnotation(RestController.class) == null) { // BrAPI call path should not include the base BrAPI path
// Class is not a RestController CallVO call = new CallVO(path.replace(BRAPI_PATH, ""));
continue;
} // List every endpoint for current path
Set<String> methods = endpoints.stream()
for (Method method : controllerClass.getMethods()) { // List all operations for each endpoint
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); .flatMap(endpointDescription -> endpointDescription.getOperations().stream())
if (apiOperation != null && apiOperation.hidden()) { // List all methods
// Rest call is hidden = ignore .map(operation -> operation.getMethod().toString())
continue; .collect(Collectors.toSet());
} call.setMethods(methods);
String[] paths = getCallPath(method); return call;
if (paths == null) { })
// No rest path mapping => ignore // Sort by call name
continue; .sorted(Comparator.comparing(CallVO::getCall))
} .collect(Collectors.toList());
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;
} }
} }
...@@ -2,24 +2,23 @@ package fr.inra.urgi.gpds.domain.data; ...@@ -2,24 +2,23 @@ package fr.inra.urgi.gpds.domain.data;
import fr.inra.urgi.gpds.domain.brapi.v1.data.BrapiCall; import fr.inra.urgi.gpds.domain.brapi.v1.data.BrapiCall;
import java.util.HashSet;
import java.util.Set; 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 * @author gcornut
*/ */
public class CallVO implements BrapiCall { public class CallVO implements BrapiCall {
private String call; private final String call;
private Set<String> datatypes; private Set<String> datatypes = DEFAULT_DATA_TYPES;
private Set<String> versions = DEFAULT_BRAPI_VERSIONS;
private Set<String> methods; private Set<String> methods;
private Set<String> versions;
public CallVO(String call) { public CallVO(String call) {
this.call = call; this.call = call;
this.datatypes = new HashSet<>();
this.methods = new HashSet<>();
this.versions = new HashSet<>();
} }
@Override @Override
...@@ -42,4 +41,15 @@ public class CallVO implements BrapiCall { ...@@ -42,4 +41,15 @@ public class CallVO implements BrapiCall {
return versions; 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;
}
} }
...@@ -3,7 +3,8 @@ package fr.inra.urgi.gpds.api.brapi.v1; ...@@ -3,7 +3,8 @@ package fr.inra.urgi.gpds.api.brapi.v1;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; 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.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
...@@ -18,7 +19,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. ...@@ -18,7 +19,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* @author gcornut * @author gcornut
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = CallsController.class) @SpringBootTest
@AutoConfigureMockMvc
class CallsControllerTest { class CallsControllerTest {
@Autowired @Autowired
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment