jhannes / action-controller Goto Github PK
View Code? Open in Web Editor NEWAction Controller micro framework for easy REST backends
License: Apache License 2.0
Action Controller micro framework for easy REST backends
License: Apache License 2.0
https://example.com:443 should always be represented as https://example.com and http://example.com:80 as http://example.com
Using a HttpRequestParameterMappingFactory
instead of relying on predefined constructors of HttpRequestParameterMapping
will avoid possible errors when creating new annotations.
Thanks to @AndreasSM for the suggestion.
Currently action-controller "swallows" my method exceptions, and I have no way to implement custom exception-handling
Since OpenAPI specs can specify payloads with 400-responses, it would be interesting to have some sort of support for converting 400-responses to exceptions, especially if the code could be generated from https://github.com/jhannes/openapi-generator-java-annotationfree
Syntax proposal:
@POST("/foo")
@JsonException
public void doThrow() throws MyApplicationException;
public class MyApplicationException {
public MyApplicationException(SomeErrorDto error) {}
public SomeErrorDto getSomeError() { ... }
}
Support should be both on the client and server side and rely on throws
declarations and POJO exceptions
When a http error is returned it may still be relevant to invoke client callback arguments, e.g.
@POST("/foo")
@JsonException
public void doThrow(
@HttpHeader("Location") Consumer<String> setLocation,
@UnencryptedCookie("Session-Id") AtomicReference<String> cookieReference
) throws MyApplicationException;
It would be especially interesting if there could be some support for async actions
Some APIs come with a certain amount of ambiguity. For instance, one might have the following scenario:
GET /users/{uid}/roles
GET /users/roles/{role}
where the first endpoint returns the roles of a specific user, while the second searches for users with a spesific role
Current situastion:
One cannot handle this in Action-controller due to error "throw new ActionControllerConfigurationException(action + " is in conflict with " + existingAction);"
Behaviour in spring:
Spring has one way of handling these situations: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-pattern-comparison
Given the following case:
Expected behaviour in action-controller:
I would expect action-controller to at least handle the same cases as spring does with regards to determining a method in order to replace existing systems without changes in the API sepc (that is, try runtime to determine a method rather than to throw a "potential conflict"-error )
Either throw an error when no method can be determined by the "most specific" rule, or to let the one with the longest constant prefix prevail.
When using @UnecryptedCookied(secure = true)
, secure should be treated as false on localhost to enable developer testing
In order to get cookies to work across domains, we have to set the Same-Site attribute for the cookie. This can also be critical if integrating between https-servers and localhost-urls for development purposes. There is no support for same-site cookies in the servlet API.
Cookies are headers on the format:
Set-Cookie: cookie1=abc; Same-Site=none; Secure; HttpOnly
Set-Cookie: cookie2=pqr; Same-Site=strict; Secure; HttpOnly
Set-Cookie: toBeDeleted=; Path=/app; HttpOnly; Max-Age=0
The following scenarios must be supported:
Proposed syntax:
@POST()
public void postData(
@Multipart("pdfFile") Optional<MultipartFile> pdfFile,
@Multipart("xmlFile") Optional<MultipartFile> xmlFile,
@Multipart("diploma") List<MultipartFile> allDiplomas
) {}
Where MultipartFile has properties name, contentType and can return an InputStream (like javax.activation.DataSource
)
This can be implemented both as a client and a server mapper
Currently PATCH, OPTIONS, HEAD and TRACE are not included by default.
Just to prove that it can be done - create a socket-based ApiExchange implementation!
This function is used to determine whether incomming json is an array or object. This does not work for kotlin List object. i.e:
@Put("/path")
fun resourcePut(
@JsonBody resourceDto: List<resourceDto>
){
….
}
Motivating example:
@GET("/file/{filename}")
@ContentBody
public BufferedInputStream getStream(
@PathParam("filename") String filename,
@Modified Consumer<OffsetDateTime> setModified,
@IfModifiedSince Optional<OffsetDateTime> ifModifiedSince
) {
File file = resolveFile(filename);
if (ifModifiedSince.map(time -> time.isBefore(file.lastModifiedTime()).orElse(false)) {
throw new HttpNotModifiedException();
}
setModified.apply(file.lastModifiedTime());
return new FileInputStream(file);
}
```
I find that I sometimes use the servlets for webjars and content in real applications. They should be improved and moved to action-controller main module.
ConfigObserver should be adapted to the use case where the configuration is stored in a database like PostgreSQL. The PostgreSQL-specific support should probably not be built-in, but it should be adapted to the case where LISTEN-NOTIFY is used to get instant updates to all instances of a running application
This requires a refactoring of ConfigObserver
and ConfigMap
classes so that:
Scan for files like .env
and .env.<profile>
and read them as Properties into environment array for more similar behavior as containerized environments
Is it weird to have this expectation in ApiServlet?
org.actioncontroller.ActionControllerConfigurationException: class org.actioncontroller.servlet.ApiServlet should have mapping ending with /*, was [/, /configinfo/, *.jsp, *.jspf, *.jspx, *.xsp, *.JSP, *.JSPF, *.JSPX, *.XSP]
It should be fine to just handle /
and /foo
and not having the WebAppContext having to handle /*
Example:
internal class AdminWebApp(
contextPrefix: String, config: ConfigurationBean, dbContext: DbContext,
dataSourceSupplier: Supplier<DataSource>
) : CommonWebAppContext(contextPrefix) {
init {
this.securityHandler =
basicAuth(
config.adminUsername,
config.adminPassword,
"internal admin"
)
val dbContextFilter = addFilter(
FilterHolder(ApiFilter(dbContext, dataSourceSupplier)),
"/configinfo",
EnumSet.of(DispatcherType.REQUEST)
)
addServlet(ServletHolder(ApiServlet(ConfigInfoServlet(config))), "/configinfo")
}
}
Because then it complains about :
javax.servlet.ServletException: org.actioncontroller.servlet.ApiServlet-6cdba6dc==org.actioncontroller.servlet.ApiServlet@9623b286{jsp=null,order=-1,inst=true,async=true,src=EMBEDDED:null}
at org.eclipse.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:643)
at org.eclipse.jetty.servlet.ServletHolder.initialize(ServletHolder.java:407)
at org.eclipse.jetty.servlet.ServletHandler.lambda$initialize$2(ServletHandler.java:690)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
at org.eclipse.jetty.servlet.ServletHandler.initialize(ServletHandler.java:714)
at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:392)
at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1304)
at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:879)
at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:306)
at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:532)
at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:171)
at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:121)
at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:89)
at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:171)
at org.eclipse.jetty.server.Server.start(Server.java:469)
at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:114)
at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:89)
at org.eclipse.jetty.server.Server.doStart(Server.java:414)
at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
at Server.start(Server.kt:93)
at Server$Companion.main(Server.kt:194)
at Server.main(Server.kt)
Caused by: org.actioncontroller.ActionControllerConfigurationException: class org.actioncontroller.servlet.ApiServlet should have mapping ending with /*, was [/, /configinfo/, *.jsp, *.jspf, *.jspx, *.xsp, *.JSP, *.JSPF, *.JSPX, *.XSP]
at org.actioncontroller.servlet.ApiServlet.init(ApiServlet.java:137)
at org.eclipse.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:625)
... 28 more
If app doesn't handle it, is it not the Server's responsibility (in this case jetty) to simply return a 404 from it's error handler?
httpClient.serverCertificates=~/.cert/*.crt
public class ClientSocketConfigurationListener extends PrefixConfigListener<SSLSocketFactory> {
private ClientSocketConfigurationListener(String prefix, ConfigValueListener<SSLSocketFactory> listener) {
super(prefix, listener);
}
@Override
protected HttpsConfiguration transform(ConfigMap config) throws Exception {
List<File> files = config.getFiles("serverCertificates");
if (files.isEmpty) {
return null;
}
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(null, null);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
for (File file : files) {
try (FileInputStream input = new FileInputStream(file)) {
keyStore.setCertificateEntry(file.getName(), certificateFactory.generateCertificate(input));
}
}
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, factory.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
}
configObserver.onValue("httpClient", ClientSocketConfigurationListener ::new, HttpsURLConnection::setDefaultSSLSocketFactory);
Adding, removing or changing a .crt file under ~/.cert should trigger update
Motivating example (complex): Listening to changes to p12-file to start https connection
https.address=myserver.example.com:8443
https.keyStore=~/.cert/https-key.p12
https.keyStorePassword=***
https.keyPassword=***
This is doubly complex, because the observer should be triggered both when the configuration changes and when the p12-file changes:
public class HttpsConfiguration {
final InetSocketAddress address;
final File keyStoreFile;
final String keyStorePassword;
final String keyPassword;
public HttpsConfiguration(InetSocketAddress address, File keyStoreFile, String keyStorePassword, String keyPassword) {
this.address= address;
this.keyStoreFile = keyStoreFile;
this.keyStorePassword= keyStorePassword;
this.keyPassword= keyPassword;
}
}
private static class HttpsConfigurationListener extends PrefixConfigListener<HttpsConfiguration> {
private HttpsConfigurationListener (String prefix, ConfigValueListener<HttpsConfiguration> listener) {
super(prefix, listener);
}
@Override
protected HttpsConfiguration transform(ConfigMap config) {
return config.optionalFile("keyStoreFile").map(file -> new HttpsConfiguration(
config.optional("address").map(ConfigListener::asInetSocketAddress).orElse(new InetSocketAddress(443)),
keyStoreFile,
config.getOrDefault("keyStorePassword", null),
config.getOrDefault("keyPassword", null),
)).orElse(null);
}
}
ApiActionResponseUnknownMappingException should use action.getGenericReturnType()
instead of action.getReturnType()
https://github.com/jhannes/action-controller/tree/master/action-controller/src/main/java/org/fakeservlet seems to belong in scope test.
Example https://github.com/jhannes/action-controller/blob/master/action-controller/src/main/java/org/fakeservlet/FakeServletResponse.java#L16 pulling in test scope dependency.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.