reader) {
customReaders.add(reader);
return this;
}
/**
* Registers an object that can convert rest method parameters (e.g. querystring, header, form or path params)
* into custom classes.
* In most cases, it is easier to instead use {@link #addCustomParamConverter(Class, ParamConverter)}
*
* @param paramConverterProvider A provider of parameter converters
* @return This builder
*/
public RestHandlerBuilder addCustomParamConverterProvider(ParamConverterProvider paramConverterProvider) {
customParamConverterProviders.add(paramConverterProvider);
return this;
}
/**
* Registers a parameter converter class that convert strings to and from a custom class.
* This allows you to specify query string parameters, form values, header params and path params as custom classes.
* For more functionality, {@link #addCustomParamConverterProvider(ParamConverterProvider)} is also available.
*
* @param paramClass The class that this converter is meant for.
* @param converter The converter
* @param The type of the parameter
* @return This builder
*/
public
RestHandlerBuilder addCustomParamConverter(Class
paramClass, ParamConverter
converter) {
return addCustomParamConverterProvider(new ParamConverterProvider() {
@Override
public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) {
if (!rawType.equals(paramClass)) {
return null;
}
return (ParamConverter) converter;
}
});
}
/**
* Enables an Open API JSON URL at the specified endpoint. This JSON describes the API exposed
* by the rest resources declared by this builder, and can be used by UIs such as Swagger.
*
* @param url The URL to serve from, for example /openapi.json
or null
to disable the JSON endpoint. Disabled by default.
* @return The current Rest Handler Builder
* @see #withOpenApiDocument(OpenAPIObjectBuilder)
* @see #withOpenApiHtmlUrl(String)
*/
public RestHandlerBuilder withOpenApiJsonUrl(String url) {
this.openApiJsonUrl = url;
return this;
}
/**
* Enables a simple HTML endpoint that documents the API exposed by the rest resources declared by this builder.
*
* @param url The URL to serve from, for example /api.html
or null
to disable the HTML endpoint. Disabled by default.
* @return The current Rest Handler Builder
* @see #withOpenApiDocument(OpenAPIObjectBuilder)
* @see #withOpenApiJsonUrl(String)
* @see #withOpenApiHtmlCss(String)
*/
public RestHandlerBuilder withOpenApiHtmlUrl(String url) {
this.openApiHtmlUrl = url;
return this;
}
/**
* Specifies if values passed to method parameters with {@link jakarta.ws.rs.QueryParam} or {@link jakarta.ws.rs.HeaderParam} annotations should be transformed or not.
* The primary use of this is to allow querystring parameters such as /path?value=one,two,three
to be interpreted
* as a list of three values rather than a single string. This only applies to parameters that are collections.
* The default is {@link CollectionParameterStrategy#NO_TRANSFORM} which is the JAX-RS standard.
* Note: until MuServer 1.0, if no value is specified but methods with collection parameters are detected
* then the handler will fail to start and this value will need to be explicitly set. This is in order to highlight the change
* in behaviour introduced in Mu Server 0.70 where it used {@link CollectionParameterStrategy#SPLIT_ON_COMMA} behaviour.
* @param collectionParameterStrategy The strategy to use
* @return This builder
*/
public RestHandlerBuilder withCollectionParameterStrategy(CollectionParameterStrategy collectionParameterStrategy) {
this.collectionParameterStrategy = collectionParameterStrategy;
return this;
}
/**
* When using the HTML endpoint made available by calling {@link #withOpenApiDocument(OpenAPIObjectBuilder)}
* this allows you to override the default CSS that is used.
*
* @param css A string containing a style sheet definition.
* @return The current Rest Handler Builder
*/
public RestHandlerBuilder withOpenApiHtmlCss(String css) {
this.openApiHtmlCss = css;
return this;
}
/**
* Use this value to create JSON and HTML documentation for your rest service.
* Minimal example:
*
* OpenAPIObjectBuilder.openAPIObject()
* .withInfo(InfoObjectBuilder.infoObject()
* .withTitle("Mu Server Sample API")
* .withVersion("1.0")
* .build())
*
* Extended example:
*
* OpenAPIObjectBuilder.openAPIObject()
* .withInfo(InfoObjectBuilder.infoObject()
* .withTitle("Mu Server Sample API")
* .withVersion("1.0")
* .withLicense(LicenseObjectBuilder.Apache2_0())
* .withDescription("This is the **description**\n\nWhich is markdown")
* .withTermsOfService(URI.create("http://example.org/terms/"))
* .build())
* .withExternalDocs(externalDocumentationObject()
* .withDescription("Full documentation")
* .withUrl(URI.create("http://example.org/docs"))
* .build())
*
* The path information and operation information will be automatically generated. By default, you can access
* the Open API specification of your rest service at /openapi.json
or view the HTML at
* /api.html
*
* @param openAPIObject An API Object builder with the {@link OpenAPIObjectBuilder#withInfo(InfoObject)} set.
* @return The current Rest Handler Builder
* @see OpenAPIObjectBuilder#openAPIObject()
* @see #withOpenApiJsonUrl(String)
* @see #withOpenApiHtmlUrl(String)
*/
public RestHandlerBuilder withOpenApiDocument(OpenAPIObjectBuilder openAPIObject) {
this.openAPIObject = openAPIObject;
return this;
}
/**
* Adds a mapper that converts an exception to a response.
* For example, you may create a custom exception such as a ValidationException that you throw from your
* jax-rs methods. A mapper for this exception type could return a Response with a 400 code and a custom
* validation error message.
*
* @param The exception type that the mapper can handle
* @param exceptionClass The type of exception to map.
* @param exceptionMapper A function that creates a {@link jakarta.ws.rs.core.Response} suitable for the exception.
* @return Returns this builder.
*/
public RestHandlerBuilder addExceptionMapper(Class exceptionClass, ExceptionMapper exceptionMapper) {
this.exceptionMappers.put(exceptionClass, exceptionMapper);
return this;
}
/**
* Registers an object that is able to customize {@link io.muserver.openapi.SchemaObject}s generated by this rest handler
* for OpenAPI documentation.
* This is only used when calling the URL specified by {@link #withOpenApiJsonUrl(String)}
* Note: if a rest resource implements {@link SchemaObjectCustomizer} then it will be automatically
* registered.
* @param customizer The customizer to register
* @return This builder
*/
public RestHandlerBuilder addSchemaObjectCustomizer(SchemaObjectCustomizer customizer) {
this.schemaObjectCustomizers.add(Objects.requireNonNull(customizer, "customizer"));
return this;
}
/**
* Creates a handler builder for JAX-RS REST services.
* Note that CORS is disabled by default.
*
* @param resources Instances of classes that have a {@link jakarta.ws.rs.Path} annotation.
* @return Returns a builder that can be used to specify more config
*/
public static RestHandlerBuilder restHandler(Object... resources) {
return new RestHandlerBuilder().addResource(resources);
}
/**
* Specifies the CORS config for the REST services. Defaults to {@link CORSConfigBuilder#disabled()}
* Note: an alternative to adding CORS config to the Rest Handler Builder is to add a handler with
* {@link CORSHandlerBuilder#corsHandler()} which can apply the headers to all handlers (not just JAX-RS endpoints).
*
* @param corsConfig The CORS config to use
* @return This builder.
* @see CORSConfigBuilder
*/
public RestHandlerBuilder withCORS(CORSConfig corsConfig) {
this.corsConfig = corsConfig;
return this;
}
/**
* Specifies the CORS config for the REST services. Defaults to {@link CORSConfigBuilder#disabled()}
* Note: an alternative to adding CORS config to the Rest Handler Builder is to add a handler with
* {@link CORSHandlerBuilder#corsHandler()} which can apply the headers to all handlers (not just JAX-RS endpoints).
*
* @param corsConfig The CORS config to use
* @return This builder.
* @see CORSConfigBuilder
*/
public RestHandlerBuilder withCORS(CORSConfigBuilder corsConfig) {
return withCORS(corsConfig.build());
}
/**
* Registers a request filter, which is run before a rest method is executed.
* It will be run after the method has been matched, or if the {@link PreMatching} annotation is applied to the
* filter then it will run before matching occurs.
* To access the {@link jakarta.ws.rs.container.ResourceInfo} or {@link io.muserver.MuRequest} for the current
* request, the following code can be used:
*
* ResourceInfo resourceInfo = (ResourceInfo) context.getProperty(MuRuntimeDelegate.RESOURCE_INFO_PROPERTY);
* MuRequest muRequest = (MuRequest) context.getProperty(MuRuntimeDelegate.MU_REQUEST_PROPERTY);
*
* @param filter The filter to register
* @return This builder
*/
public RestHandlerBuilder addRequestFilter(ContainerRequestFilter filter) {
if (filter.getClass().getDeclaredAnnotation(PreMatching.class) != null) {
this.preMatchRequestFilters.add(filter);
} else {
this.requestFilters.add(filter);
}
return this;
}
/**
* Registers a response filter, which is called after execution of a method takes place.
* To access the {@link jakarta.ws.rs.container.ResourceInfo} or {@link io.muserver.MuRequest} for the current
* request, the following code can be used:
*
* ResourceInfo resourceInfo = (ResourceInfo) context.getProperty(MuRuntimeDelegate.RESOURCE_INFO_PROPERTY);
* MuRequest muRequest = (MuRequest) context.getProperty(MuRuntimeDelegate.MU_REQUEST_PROPERTY);
*
* @param filter The filter to register
* @return This builder
*/
public RestHandlerBuilder addResponseFilter(ContainerResponseFilter filter) {
this.responseFilters.add(filter);
return this;
}
/**
* Registers a custom OpenAPI schema description for the given class.
* This allows you to provide rich schema objects (created with {@link SchemaObjectBuilder#schemaObject()}) in your
* OpenAPI documents. Wherever the give type is used as a parameter or body, the given schema will be used to describe it.
* Warning: When generating OpenAPI documentation, the schema information will be added to the /components/schemas
* section with a key equal to the simple class name of the given data class. If you do not wish to expose the class name
* in your API documentation, you can override it by annotating the class with a {@link Description} annotation in which
* case the value
field will be used.
* @param dataClass The type of class to describe
* @param schema The schema object describing the class
* @return This builder
*/
public RestHandlerBuilder addCustomSchema(Class> dataClass, SchemaObject schema) {
String id;
Description desc = dataClass.getDeclaredAnnotation(Description.class);
if (desc != null) {
id = desc.value();
} else {
id = dataClass.getSimpleName();
}
while (true) {
boolean anyMatch = false;
for (SchemaReference customSchema : customSchemas) {
if (customSchema.id.equals(id)) {
anyMatch = true;
break;
}
}
if (anyMatch) {
id += "0";
} else {
break;
}
}
String regex = "^[a-zA-Z0-9.\\-_]+$";
if (!id.matches(regex)) {
throw new IllegalArgumentException("The ID " + id + " given for custom schema for class " + dataClass.getName() + " does not match required regex " + regex);
}
this.customSchemas.add(new SchemaReference(id, dataClass, null, schema));
return this;
}
/**
* Registers a writer interceptor allowing for inspection and alteration of response bodies.
* Interceptors are executed in the order added, and are called before any message body
* writers added by {@link #addCustomWriter(MessageBodyWriter)}.
* To access the {@link jakarta.ws.rs.container.ResourceInfo} or {@link io.muserver.MuRequest} for the current
* request, the following code can be used:
*
* ResourceInfo resourceInfo = (ResourceInfo) context.getProperty(MuRuntimeDelegate.RESOURCE_INFO_PROPERTY);
* MuRequest muRequest = (MuRequest) context.getProperty(MuRuntimeDelegate.MU_REQUEST_PROPERTY);
* @param writerInterceptor The interceptor to add. If null
then this is a no-op.
* @return This builder
*/
public RestHandlerBuilder addWriterInterceptor(WriterInterceptor writerInterceptor) {
if (writerInterceptor != null) {
this.writerInterceptors.add(writerInterceptor);
}
return this;
}
/**
* Registers a reader interceptor allowing for inspection and alteration of request bodies.
* Interceptors are executed in the order added, and are called before any message body
* readers added by {@link #addCustomReader(MessageBodyReader)}.
* To access the {@link jakarta.ws.rs.container.ResourceInfo} or {@link io.muserver.MuRequest} for the current
* request, the following code can be used:
*
* ResourceInfo resourceInfo = (ResourceInfo) context.getProperty(MuRuntimeDelegate.RESOURCE_INFO_PROPERTY);
* MuRequest muRequest = (MuRequest) context.getProperty(MuRuntimeDelegate.MU_REQUEST_PROPERTY);
* @param readerInterceptor The interceptor to add. If null
then this is a no-op.
* @return This builder
*/
public RestHandlerBuilder addReaderInterceptor(ReaderInterceptor readerInterceptor) {
if (readerInterceptor != null) {
this.readerInterceptors.add(0, readerInterceptor);
}
return this;
}
/**
* @return The current value of this property
*/
public List resources() {
return Collections.unmodifiableList(resources);
}
/**
* @return The current value of this property
*/
public List> customWriters() {
return customWriters.stream().map(w -> (MessageBodyWriter>)w).collect(Collectors.toList());
}
/**
* @return The current value of this property
*/
public List writerInterceptors() {
return Collections.unmodifiableList(writerInterceptors);
}
/**
* @return The current value of this property
*/
public List> customReaders() {
return customReaders.stream().map(r -> (MessageBodyReader>)r).collect(Collectors.toList());
}
/**
* @return The current value of this property
*/
public List readerInterceptors() {
return Collections.unmodifiableList(readerInterceptors);
}
/**
* @return The current value of this property
*/
public List customParamConverterProviders() {
return Collections.unmodifiableList(customParamConverterProviders);
}
/**
* @return The current value of this property
*/
public Map, SchemaObject> customSchemas() {
return customSchemas.stream().collect(Collectors.toMap(ref -> ref.type, ref -> ref.schema));
}
/**
* @return The current value of this property
*/
public String openApiJsonUrl() {
return openApiJsonUrl;
}
/**
* @return The current value of this property
*/
public String openApiHtmlUrl() {
return openApiHtmlUrl;
}
/**
* @return The current value of this property
*/
public OpenAPIObjectBuilder openAPIObject() {
return openAPIObject;
}
/**
* @return The current value of this property
*/
public String openApiHtmlCss() {
return openApiHtmlCss;
}
/**
* @return The current value of this property
*/
public Map, ExceptionMapper extends Throwable>> exceptionMappers() {
return Collections.unmodifiableMap(exceptionMappers);
}
/**
* @return The current value of this property
*/
public List preMatchRequestFilters() {
return Collections.unmodifiableList(preMatchRequestFilters);
}
/**
* @return The current value of this property
*/
public List requestFilters() {
return Collections.unmodifiableList(requestFilters);
}
/**
* @return The current value of this property
*/
public List responseFilters() {
return Collections.unmodifiableList(responseFilters);
}
/**
* @return The current value of this property
*/
public CORSConfig corsConfig() {
return corsConfig;
}
/**
* @return The current value of this property
*/
public List schemaObjectCustomizers() {
return Collections.unmodifiableList(schemaObjectCustomizers);
}
/**
* @return The current value of this property
*/
public CollectionParameterStrategy collectionParameterStrategy() {
return collectionParameterStrategy;
}
/**
* @return The newly build {@link RestHandler}
*/
public RestHandler build() {
List readers = EntityProviders.builtInReaders();
readers.addAll(customReaders);
List writers = EntityProviders.builtInWriters();
writers.addAll(customWriters);
EntityProviders entityProviders = new EntityProviders(readers, writers);
List paramConverterProviders = new ArrayList<>(customParamConverterProviders);
paramConverterProviders.add(new BuiltInParamConverterProvider());
List list = new ArrayList<>();
SchemaObjectCustomizer schemaObjectCustomizer = new CompositeSchemaObjectCustomizer(schemaObjectCustomizers);
for (Object resource : resources) {
if (resource instanceof SchemaObjectCustomizer && !schemaObjectCustomizers.contains(resource)) {
schemaObjectCustomizers.add((SchemaObjectCustomizer) resource);
}
}
for (Object restResource : resources) {
list.add(ResourceClass.fromObject(restResource, paramConverterProviders, schemaObjectCustomizer));
}
List roots = Collections.unmodifiableList(list);
OpenApiDocumentor documentor = null;
if (openApiHtmlUrl != null || openApiJsonUrl != null) {
if (openApiHtmlCss == null) {
InputStream cssStream = RestHandlerBuilder.class.getResourceAsStream("/io/muserver/resources/api.css");
Scanner scanner = new Scanner(cssStream, "UTF-8").useDelimiter("\\A");
openApiHtmlCss = scanner.next();
scanner.close();
}
OpenAPIObjectBuilder openAPIObjectToUse = this.openAPIObject == null ? OpenAPIObjectBuilder.openAPIObject() : this.openAPIObject;
openAPIObjectToUse.withPaths(pathsObject().build());
documentor = new OpenApiDocumentor(roots, openApiJsonUrl, openApiHtmlUrl, openAPIObjectToUse.build(), openApiHtmlCss, corsConfig, new ArrayList<>(customSchemas), schemaObjectCustomizer, paramConverterProviders);
}
CustomExceptionMapper customExceptionMapper = new CustomExceptionMapper(exceptionMappers);
FilterManagerThing filterManagerThing = new FilterManagerThing(preMatchRequestFilters, requestFilters, responseFilters);
CollectionParameterStrategy cps = this.collectionParameterStrategy;
if (cps == null) {
for (ResourceClass root : roots) {
for (ResourceMethod rm : root.resourceMethods) {
for (ResourceMethodParam param : rm.params) {
if (Collection.class.isAssignableFrom(param.parameterHandle.getType()) && (param.source == ResourceMethodParam.ValueSource.HEADER_PARAM || param.source == ResourceMethodParam.ValueSource.QUERY_PARAM)) {
throw new IllegalStateException("Please specify a string handling strategy for collections for querystring and header parameters. " +
"Please note that the behaviour of these parameters have changed since Mu Server 0.70.0 to follow the JAX-RS standard. " +
"Previously, a parameter values such as 'one,two,three' when passed to a collection parameter would be interpreted as 3 values, " +
"however the JAX-RS standard is for this to be a single value. To follow the standard, please use " +
"RestHandlerBuilder.withCollectionParameterStrategy(CollectionParameterStrategy.NO_TRANSFORM) or keep early behaviour where the value is split " +
"into multiple values, use RestHandlerBuilder.withCollectionParameterStrategy(CollectionParameterStrategy.SPLIT_ON_COMMA) no your rest handler builder instance.");
}
}
}
}
cps = CollectionParameterStrategy.NO_TRANSFORM;
}
return new RestHandler(entityProviders, roots, documentor, customExceptionMapper, filterManagerThing, corsConfig, paramConverterProviders, schemaObjectCustomizer, readerInterceptors, writerInterceptors, cps);
}
}