Skip to content

Customising the displayed base URL for API endpoints #963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Luthien-in-edhil opened this issue Sep 10, 2015 · 12 comments
Closed

Customising the displayed base URL for API endpoints #963

Luthien-in-edhil opened this issue Sep 10, 2015 · 12 comments

Comments

@Luthien-in-edhil
Copy link

Hi,

I'm trying to figure out whether it is possible to customise how our API endpoints vs. the base URL (basePath in JSON output) appears? For instance (how it appears in the Swagger UI):

hierarchical_records:

GET /v2/record/{collectionId}/{recordId}/ancestor-self-siblings.json
GET /v2/record/{collectionId}/{recordId}/children.json
GET /v2/record/{collectionId}/{recordId}/parent.json
GET /v2/record/{collectionId}/{recordId}/self.json

(...)
[ base url: /api , api version: 1.0 ]

... while we would like to present it like this:

hierarchical_records:

GET /record/{collectionId}/{recordId}/ancestor-self-siblings.json
GET /record/{collectionId}/{recordId}/children.json
GET /record/{collectionId}/{recordId}/parent.json
GET /record/{collectionId}/{recordId}/self.json

(...)
[ base url: /api/v2 , api version: 1.0 ]

I understand that the base_url annotation has been deprecated since Swagger 2.0, and I assume that Swagger retrieves the endpoint urls from the Spring @RequestMapping annotations.
However, we have @RequestMapping annotations in our controllers with URL's that include the /v2 bit:
@RequestMapping(value = "/v2/user/saveditem.json" (...)
but also some without it:
@RequestMapping(value = "/{collectionId}/{recordId}/parent.json" (...)
according to how things are set in the *mvc.xml Spring configuration.

Swagger (Springfox?) seems to be smart enough to figure this out by itself and present all endpoints with the same unified cut between base url and endpoint url, which makes me think that it should be possible to somehow influence the choice where that cut is made.

But how?

Thanks in advance!

Using Springfox 2.2.2 Swagger, Java 7, Spring 2.2.0 (no JAX-RS),
Project: https://github.com/europeana/api2/ (develop)

@dilipkrish
Copy link
Member

Swagger (Springfox?) seems to be smart enough to figure this out by itself and present all endpoints with the same unified cut between base url and endpoint url, which makes me think that it should be possible to somehow influence the choice where that cut is made.

It is not :) it uses the context path as the starting point.

What you really need to is to define a dynamic servlet registration and create 2 dockets .. one for api and one for api/v2. This SO post might help

...
  Dynamic servlet = servletContext.addServlet("v1Dispatcher", new DispatcherServlet(ctx1));  
        servlet.addMapping("/api");  
        servlet.setLoadOnStartup(1);  

  Dynamic servlet = servletContext.addServlet("v2Dispatcher", new DispatcherServlet(ctx2));  
        servlet.addMapping("/api/v2");  
        servlet.setLoadOnStartup(1);  

@Luthien-in-edhil
Copy link
Author

My apologies: it turns out I overlooked something ... the problem with this API is that it's a load of legacy code with hardly anyone remembering the why and how of design decisions made by developers who left two years ago ... :( .

I overlooked the class-level RequestMapping annotations that exist in some controllers, splitting up the mapping path between them and the method-level RequestMapping annotations, for instance:

@RequestMapping(value = "/v2/record")
public class ThisController(...) 
....
@RequestMapping(value = "/{collectionId}/{recordId}/self.json", ...
public ModelAndView thisMethod(...)

vs.

<no class level RequestMapping annotation>
public class ThatController(...) 
(...)
@RequestMapping(value ="/v2/search.json"
public ModelAndView thatMethod(...)

Additionally, the Spring configuration (including the Servlet registration) is rather opaque XML config, but I tracked the servlet mapping down to the web.xml level:

<servlet-mapping>
    <servlet-name>api2</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

and, unfortunately, there still exists some legacy /v1 API code that we have to keep in working order. I assume then that this servlet mapping is what Swagger reads as its base path.

Because one of my colleagues is rewriting the XML Spring configuration into a Java configuration, we can resolve the issue when that's in place. I'll just leave the base url as is for now, unless anybody knows of a way to tweak whatever Swagger reads as context path?

Thanks for the insights!

@dilipkrish
Copy link
Member

you're welcome! Feel free to update this if somethings not working right

@sioulin
Copy link

sioulin commented Mar 18, 2016

The method described above didn't really work for me. However, I was able to partition APIs based on version using a custom PathProvider in Springfox Swagger 2.4.0. Here is some sample code using a custom PathProvider that works:

package my.package;

import org.joda.time.DateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.util.UriComponentsBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.paths.AbstractPathProvider;
import springfox.documentation.spring.web.paths.Paths;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import static springfox.documentation.builders.PathSelectors.regex;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket latestDocumentationPlugin() {
        return new VersionedDocket("latest");
    }

    @Bean
    public Docket v10DocumentationPlugin() {
        return new VersionedDocket("1.0");
    }

    class VersionedDocket extends Docket {
        public VersionedDocket(String version) {
            super(DocumentationType.SWAGGER_2);
            super.groupName(version)
                    .select()
                        .apis(RequestHandlerSelectors.any())
                        .paths(regex("/api/" + version + "/.*"))
                        .build()
                    .apiInfo(getApiInfo(version))
                    .pathProvider(new BasePathAwareRelativePathProvider("/api/" + version))
                    .directModelSubstitute(DateTime.class, String.class)
                    .useDefaultResponseMessages(false)
                    .enableUrlTemplating(true);
        }

        private ApiInfo getApiInfo(String version) {
            return new ApiInfo(
                    "Title",  // title
                    "",     // description
                    version,
                    "",     // terms of service url
                    new Contact("Me","", "me@me.com"),
                    "",     // licence
                    ""      // licence url
            );
        }
    }

    class BasePathAwareRelativePathProvider extends AbstractPathProvider {
        private String basePath;

        public BasePathAwareRelativePathProvider(String basePath) {
            this.basePath = basePath;
        }

        @Override
        protected String applicationPath() {
            return basePath;
        }

        @Override
        protected String getDocumentationPath() {
            return "/";
        }

        @Override
        public String getOperationPath(String operationPath) {
            UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromPath("/");
            return Paths.removeAdjacentForwardSlashes(
                    uriComponentsBuilder.path(operationPath.replaceFirst(basePath, "")).build().toString());
        }
    }
}

@dilipkrish
Copy link
Member

What Din't work for you?

@sioulin
Copy link

sioulin commented Mar 21, 2016

The method you described with adding additional servlets and servlet mappings didn't work.

Dynamic servlet = servletContext.addServlet("v1Dispatcher", new DispatcherServlet(ctx1));  
        servlet.addMapping("/api");  
        servlet.setLoadOnStartup(1); 

Dynamic servlet = servletContext.addServlet("v2Dispatcher", new DispatcherServlet(ctx2));  
        servlet.addMapping("/api/v2");  
        servlet.setLoadOnStartup(1);  

The sample code I posted using a custom path provider works.

@Luthien-in-edhil
Copy link
Author

Thanks, Sioulin. What you came up with works splendid!

@thirupathi-redpinesignals

is there any way to specify base path in spring boot application similar to version

new ApiInfoBuilder() .description(msgSource.getMessage("pi.description", null, LocaleContextHolder.getLocale())) .termsOfServiceUrl("http://trvajjala.in") .version("2.0") .build();

@dilipkrish
Copy link
Member

@thirupathi-redpinesignals you can create your own PathProvider and register the bean

@tvajjala
Copy link

tvajjala commented Aug 2, 2016

Thanks Dilip, we have one requirement to implement OAuth2 Authentication using Spring boot similar to petstore.swagger.io reference site. With Authorize button top right corner. could you please provide document or reference source code for the sample application.

@thirupathi-redpinesignals
Copy link

thirupathi-redpinesignals commented Aug 3, 2016

public class BaseVersionProvider extends AbstractPathProvider {

    public static final String ROOT = "/v2";

    public BaseVersionProvider() {
        super();
    }

    @Override
    protected String applicationPath() {

        return ROOT;
    }

    @Override
    protected String getDocumentationPath() {

        return ROOT;
    }
}

  return new Docket(DocumentationType.SWAGGER_2)
                .groupName("wyzbee-api")
                .apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.basePackage(SiteResource.class.getPackage().getName()))
                .build()
               .pathProvider(baseVersionProvider())

but base_URL on swagger still showing "/" only

@dilipkrish
Copy link
Member

@thirupathi-redpinesignals For what are you adding the base path. Can you curl/postman to that url with a base path of "/v2"?

For e.g. yr service has a context path of /app and a request mapping of /some-request-mapping that is derived from the context path, your endpoint when you curl should be /app/some-request-mapping. By overriding it to "/v2" are you thinking the it will change to /v2/some-request-mapping?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants