Skip to content

Commit

Permalink
Merge pull request #16915 from ebullient/http-path-backport
Browse files Browse the repository at this point in the history
Http path backport
  • Loading branch information
gsmet authored Apr 30, 2021
2 parents 55555db + 58f78c5 commit be67688
Show file tree
Hide file tree
Showing 34 changed files with 586 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,12 @@ public static URI normalizeWithBase(URI base, String segment, boolean trailingSl
URI resolvedUri = base.resolve(segmentUri);
return resolvedUri;
}

public static String relativize(String rootPath, String leafPath) {
if (leafPath.startsWith(rootPath)) {
return leafPath.substring(rootPath.length());
}

return null;
}
}
69 changes: 59 additions & 10 deletions docs/src/main/asciidoc/dev-ui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ include::./attributes.adoc[]
This guide covers the Quarkus Dev UI for link:building-my-first-extension[extension authors].

Quarkus now ships with a new experimental Dev UI, which is available in dev mode (when you start
quarkus with `mvn quarkus:dev`) at http://localhost:8080/q/dev[/q/dev] and will show you something like
quarkus with `mvn quarkus:dev`) at http://localhost:8080/q/dev[/q/dev] by default. It will show you something like
this:

image::dev-ui-overview.png[alt=Dev UI overview,role="center"]
Expand Down Expand Up @@ -46,16 +46,56 @@ two links with some styling and icons:

[source,html]
----
<a href="/q/openapi" class="badge badge-light">
<i class="fa ..."></i>
<a href="{config:http-path('quarkus.smallrye-openapi.path')}" class="badge badge-light">
<i class="fa fa-map-signs fa-fw"></i>
OpenAPI</a>
<a href="/q/swagger-ui/" class="badge badge-light">
<i class="fa ..."></i>
<br>
<a href="{config:http-path('quarkus.swagger-ui.path')}/" class="badge badge-light">
<i class="fa fa-map-signs fa-fw"></i>
Swagger UI</a>
----

TIP: We use the Font Awesome Free icon set.

Note how the paths are specified: `{config:http-path('quarkus.smallrye-openapi.path')}`. This is a special
directive that the quarkus dev console understands: it will replace that value with the resolved route
named 'quarkus.smallrye-openapi.path'.

The corresponding non-application endpoint is declared using `.routeConfigKey` to associate the route with a name:

[source,java]
----
nonApplicationRootPathBuildItem.routeBuilder()
.route(openApiConfig.path) // <1>
.routeConfigKey("quarkus.smallrye-openapi.path") // <2>
...
.build();
----
<1> The configured path is resolved into a valid route.
<2> The resolved route path is then associated with the key `quarkus.smallrye-openapi.path`.

== Path considerations

Paths are tricky business. Keep the following in mind:

* Assume your UI will be nested under the dev endpoint. Do not provide a way to customize this without a strong reason.
* Never construct your own absolute paths. Adding a suffix to a known, normalized and resolved path is fine.

Configured paths, like the `dev` endpoint used by the console or the SmallRye OpenAPI path shown in the example above,
need to be properly resolved against both `quarkus.http.root-path` and `quarkus.http.non-application-root-path`.
Use `NonApplicationRootPathBuildItem` or `HttpRootPathBuildItem` to construct endpoint routes and identify resolved
path values that can then be used in templates.

The `{devRootAppend}` variable can also be used in templates to construct URLs for static dev console resources, for example:

[source,html]
----
<img src="{devRootAppend}/resources/images/quarkus_icon_rgb_reverse.svg" width="40" height="30" class="d-inline-block align-middle" alt="Quarkus"/>
----

Refer to the link:all-config#quarkus-vertx-http_quarkus.http.non-application-root-path[Quarkus Vertx HTTP configuration reference]
for details on how the non-application root path is configured.

== Template and styling support

Both the `embedded.html` files and any full page you add in `/dev-templates` will be interpreted by
Expand All @@ -75,6 +115,9 @@ A `config:property(name)` expression can be used to output the config value for
The property name can be either a string literal or obtained dynamically by another expression.
For example `{config:property('quarkus.lambda.handler')}` and `{config:property(foo.propertyName)}`.

Reminder: do not use this to retrieve raw configured path values. As shown above, use `{config:http-path(...)}` with
a known route configuration key when working with resource paths.

== Adding full pages

To add full pages for your Dev UI extension such as this one:
Expand Down Expand Up @@ -137,17 +180,19 @@ link:building-my-first-extension#description-of-a-quarkus-extension[`deployment`

== Linking to your full-page templates

Every full-page template lives under the `/q/dev/{groupId}.{artifactId}/` URI (for example
`/q/dev/io.quarkus.quarkus-cache/`), so if you want to link
to them from your `embedded.html` file you can use the `urlbase` template parameter to point to them:
Full-page templates for extensions live under a pre-defined `{devRootAppend}/{groupId}.{artifactId}/` directory
that is referenced using the `urlbase` template parameter. Using configuration defaults, that would resolve to
`/q/dev/io.quarkus.quarkus-cache/`, as an example.

[source,java]
Use the `{urlbase}` template parameter to reference this folder in `embedded.html`:

[source,html]
----
<a href="{urlbase}/caches" class="badge badge-light">// <1>
<i class="fa ..."></i>
Caches <span class="badge badge-light">{info:cacheInfos.size()}</span></a>
----
<1> Use the `urlbase` template parameter to point to where your full-page templates are located
<1> Use the `urlbase` template parameter to reference full-page templates for your extension

== Passing information to your templates

Expand Down Expand Up @@ -266,6 +311,7 @@ This can be done by adding another link:building-my-first-extension#deploying-th
declare the action in your extension's
link:building-my-first-extension#description-of-a-quarkus-extension[`deployment`] module:


[source,java]
----
package io.quarkus.cache.deployment.devconsole;
Expand All @@ -290,11 +336,13 @@ public class DevConsoleProcessor {
<1> Mark the recorder as optional, so it will only be invoked when in dev mode
<2> Declare a `POST {urlbase}/caches` route handled by the given handler


Note: you can see <<action-example,how to invoke this action from your full page>>.

Now all you have to do is implement the recorder in your extension's
link:building-my-first-extension#description-of-a-quarkus-extension[`runtime module`]:


[source,java]
----
package io.quarkus.cache.runtime.devconsole;
Expand Down Expand Up @@ -338,6 +386,7 @@ public class CacheDevConsoleRecorder {
<3> Don't forget to add a message for the user to let them know everything went fine
<4> You can also add error messages


NOTE: Flash messages are handled by the `main` DEV template and will result in nice notifications for your
users:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void createPrometheusRoute(BuildProducer<RouteBuildItem> routes,
.routeFunction(pConfig.path, recorder.route())
.handler(recorder.getHandler())
.requiresLegacyRedirect()
.displayOnNotFoundPage("Metrics", pConfig.path)
.displayOnNotFoundPage("Metrics")
.build());

// Match paths that begin with the deployment path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,10 @@ public List<HandlerChainCustomizer> scan(MethodInfo method, Map<String, Object>
});
}

private String determineApplicationPath(IndexView index, String defaultPath) {
private String determineApplicationPath(IndexView index, Optional<String> defaultPath) {
Collection<AnnotationInstance> applicationPaths = index.getAnnotations(ResteasyReactiveDotNames.APPLICATION_PATH);
if (applicationPaths.isEmpty()) {
return defaultPath;
return defaultPath.orElse("/");
}
// currently we only examine the first class that is annotated with @ApplicationPath so best
// fail if the user code has multiple such annotations instead of surprising the user
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package io.quarkus.resteasy.reactive.server.deployment;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "rest")
public class ResteasyReactiveServerConfig {

/**
* Set this to override the default path for JAX-RS resources if there are no
* annotated application classes.
* Set this to define the application path that serves as the base URI for all
* JAX-RS resource URIs provided by {@code @Path} annotations when there are no
* {@code @ApplicationPath} annotations defined on {@code Application} classes.
* <p>
* This value is always resolved relative to {@code quarkus.http.root-path}.
*/
@ConfigItem(defaultValue = "/")
String path;
@ConfigItem
Optional<String> path;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/hello")
/**
* Per spec:
* <quote>
* Paths are relative. For an annotated class the base URI is the application path, see ApplicationPath.
* For an annotated method the base URI is the effective URI of the containing class. For the purposes of
* absolutizing a path against the base URI , a leading '/' in a path is ignored and base URIs are treated
* as if they ended in '/'.
* </quote>
*/
@Path("hello")
public class HelloResource {

@GET
public String hello() {
return "hello";
}

@GET
@Path("/nested")
public String nested() {
return "world hello";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.resteasy.reactive.server.test.path;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class RelativeRestPathTestCase {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withConfigurationResource("empty.properties")
.overrideConfigKey("quarkus.rest.path", "foo")
.overrideConfigKey("quarkus.http.root-path", "/app")
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClass(HelloResource.class));

@Test
public void testRestPath() {
RestAssured.basePath = "/";
// This is expected behavior (relative path appended to HTTP root path)
RestAssured.when().get("/app/foo/hello").then().body(Matchers.is("hello"));
RestAssured.when().get("/app/foo/hello/nested").then().body(Matchers.is("world hello"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.resteasy.reactive.server.test.path;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class RestApplicationPathTestCase {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withConfigurationResource("empty.properties")
.overrideConfigKey("quarkus.rest.path", "/foo")
.overrideConfigKey("quarkus.http.root-path", "/app")
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(HelloResource.class, BarApp.class));

/**
* Using @ApplicationPath will overlay/replace `quarkus.rest.path`.
* Per spec:
* <quote>
* Identifies the application path that serves as the base URI for all resource
* URIs provided by Path. May only be applied to a subclass of Application.
* </quote>
*
* This path will also be relative to the configured HTTP root
*/
@ApplicationPath("/bar")
public static class BarApp extends Application {
}

@Test
public void testRestPath() {
RestAssured.basePath = "/";
RestAssured.when().get("/app/bar/hello").then().body(Matchers.is("hello"));
RestAssured.when().get("/app/bar/hello/nested").then().body(Matchers.is("world hello"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
Expand All @@ -14,15 +13,16 @@ public class RestPathTestCase {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClass(HelloResource.class)
.addAsResource(new StringAsset("quarkus.rest.path=/foo"), "application.properties"));
.withConfigurationResource("empty.properties")
.overrideConfigKey("quarkus.rest.path", "/foo")
.overrideConfigKey("quarkus.http.root-path", "/app")
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClass(HelloResource.class));

@Test
public void testRestPath() {
RestAssured.get("/hello")
.then().statusCode(404);
RestAssured.get("/foo/hello")
.then().statusCode(200).body(Matchers.equalTo("hello"));

RestAssured.basePath = "/";
RestAssured.when().get("/app/foo/hello").then().body(Matchers.is("hello"));
RestAssured.when().get("/app/foo/hello/nested").then().body(Matchers.is("world hello"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
public class SmallRyeGraphQLConfig {

/**
* The rootPath under which queries will be served. Default to /graphql
* The rootPath under which queries will be served. Default to graphql
* By default, this value will be resolved as a path relative to `${quarkus.http.root-path}`.
*/
@ConfigItem(defaultValue = "/graphql")
@ConfigItem(defaultValue = "graphql")
String rootPath;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
*/
public class SmallRyeGraphQLProcessor {
private static final Logger LOG = Logger.getLogger(SmallRyeGraphQLProcessor.class);
private static final String SCHEMA_PATH = "/schema.graphql";
private static final String SCHEMA_PATH = "schema.graphql";
private static final String SPI_PATH = "META-INF/services/";

// For Service integration
Expand Down Expand Up @@ -194,14 +194,15 @@ void buildExecutionService(
@BuildStep
void buildSchemaEndpoint(
BuildProducer<RouteBuildItem> routeProducer,
HttpRootPathBuildItem httpRootPathBuildItem,
SmallRyeGraphQLInitializedBuildItem graphQLInitializedBuildItem,
SmallRyeGraphQLRecorder recorder,
SmallRyeGraphQLConfig graphQLConfig) {

Handler<RoutingContext> schemaHandler = recorder.schemaHandler(graphQLInitializedBuildItem.getInitialized());

routeProducer.produce(new RouteBuildItem.Builder()
.route(graphQLConfig.rootPath + SCHEMA_PATH)
routeProducer.produce(httpRootPathBuildItem.routeBuilder()
.nestedRoute(graphQLConfig.rootPath, SCHEMA_PATH)
.handler(schemaHandler)
.displayOnNotFoundPage("MicroProfile GraphQL Schema")
.blockingRoute()
Expand All @@ -214,6 +215,7 @@ void buildSchemaEndpoint(
@Consume(BeanContainerBuildItem.class)
void buildExecutionEndpoint(
BuildProducer<RouteBuildItem> routeProducer,
HttpRootPathBuildItem httpRootPathBuildItem,
SmallRyeGraphQLInitializedBuildItem graphQLInitializedBuildItem,
SmallRyeGraphQLRecorder recorder,
ShutdownContextBuildItem shutdownContext,
Expand All @@ -238,9 +240,10 @@ void buildExecutionEndpoint(

Handler<RoutingContext> executionHandler = recorder.executionHandler(graphQLInitializedBuildItem.getInitialized(),
allowGet);
routeProducer.produce(new RouteBuildItem.Builder()
routeProducer.produce(httpRootPathBuildItem.routeBuilder()
.routeFunction(graphQLConfig.rootPath, recorder.routeFunction(bodyHandlerBuildItem.getHandler()))
.handler(executionHandler)
.routeConfigKey("quarkus.smallrye-graphql.root-path")
.displayOnNotFoundPage("MicroProfile GraphQL Endpoint")
.blockingRoute()
.build());
Expand Down Expand Up @@ -540,6 +543,7 @@ void registerGraphQLUiHandler(
smallRyeGraphQLBuildItem.getGraphqlUiPath(), runtimeConfig);
routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder()
.route(graphQLConfig.ui.rootPath)
.routeConfigKey("quarkus.smallrye-graphql.ui.root-path")
.displayOnNotFoundPage("MicroProfile GraphQL UI")
.handler(handler)
.requiresLegacyRedirect()
Expand Down
Loading

0 comments on commit be67688

Please sign in to comment.