working on form requests and Groovy templates, update to xbib net 3.0.4

This commit is contained in:
Jörg Prante 2023-03-21 16:41:49 +01:00
parent 5803e57072
commit 1ea725cbc2
15 changed files with 231 additions and 158 deletions

View file

@ -2,6 +2,8 @@ package org.xbib.net.http.server.application.web;
import java.nio.file.Paths;
import java.time.Duration;
import org.xbib.net.http.cookie.SameSite;
import org.xbib.net.http.server.BaseApplication;
import org.xbib.net.http.server.HttpHandler;
import org.xbib.net.http.server.HttpServerContext;
@ -10,6 +12,7 @@ import org.xbib.net.http.server.session.IncomingSessionHandler;
import org.xbib.net.http.server.session.OutgoingSessionHandler;
import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.file.FileJsonSessionCodec;
import org.xbib.net.util.RandomUtil;
public class WebApplication extends BaseApplication {
@ -40,7 +43,8 @@ public class WebApplication extends BaseApplication {
sessionCodec,
getStaticFileSuffixes(),
"user_id",
"e_user_id");
"e_user_id",
() -> RandomUtil.randomString(16));
}
protected OutgoingSessionHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) {
@ -50,10 +54,13 @@ public class WebApplication extends BaseApplication {
getSecret(),
"HmacSHA1",
sessionName,
Duration.ofDays(1),
sessionCodec,
getStaticFileSuffixes(),
"user_id",
"e_user_id");
"e_user_id",
Duration.ofDays(1),
true,
false,
SameSite.LAX);
}
}

View file

@ -9,6 +9,7 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class HttpRequest extends BaseHttpRequest {
@ -47,7 +48,10 @@ public class HttpRequest extends BaseHttpRequest {
@Override
public String toString() {
return "HttpRequest[request=" + builder.fullHttpRequest + "]";
return "HttpRequest[request=" + builder.fullHttpRequest +
",parameter=" + builder.getParameter() +
",body=" + builder.fullHttpRequest.content().toString(StandardCharsets.UTF_8) +
"]";
}
public ByteBuf getByteBuf() {

View file

@ -2,6 +2,7 @@ package org.xbib.net.http.server.netty;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.http.FullHttpRequest;
import org.xbib.net.Parameter;
import org.xbib.net.URL;
import org.xbib.net.http.HttpAddress;
import org.xbib.net.http.HttpMethod;
@ -24,7 +25,7 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder {
public HttpRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) {
if (fullHttpRequest != null) {
// retain request so we can read the body later without refCnt=0 error
// retain request, so we can read the body later without refCnt=0 error
this.fullHttpRequest = fullHttpRequest.retain();
setVersion(HttpVersion.valueOf(fullHttpRequest.protocolVersion().text()));
setMethod(HttpMethod.valueOf(fullHttpRequest.method().name()));
@ -100,6 +101,10 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder {
return this;
}
protected Parameter getParameter() {
return super.parameter;
}
@Override
public HttpRequest build() {
return new HttpRequest(this);

View file

@ -1,5 +1,6 @@
package org.xbib.net.http.server;
import org.xbib.net.http.cookie.SameSite;
import org.xbib.net.http.server.route.HttpRouter;
import org.xbib.net.http.HttpAddress;
import org.xbib.net.http.HttpResponseStatus;
@ -14,6 +15,7 @@ import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.util.BlockingThreadPoolExecutor;
import org.xbib.net.http.server.validate.HttpRequestValidator;
import org.xbib.net.util.NamedThreadFactory;
import org.xbib.net.util.RandomUtil;
import org.xbib.settings.Settings;
import java.io.Closeable;
@ -194,7 +196,8 @@ public class BaseApplication implements Application {
sessionCodec,
getStaticFileSuffixes(),
"user_id",
"e_user_id");
"e_user_id",
() -> RandomUtil.randomString(16));
}
protected HttpHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) {
@ -204,11 +207,15 @@ public class BaseApplication implements Application {
getSecret(),
"HmacSHA1",
sessionName,
Duration.ofDays(1),
sessionCodec,
getStaticFileSuffixes(),
"user_id",
"e_user_id");
"e_user_id",
Duration.ofDays(1),
true,
false,
SameSite.LAX
);
}
protected HttpResponseRenderer buildResponseRenderer() {

View file

@ -1,6 +1,5 @@
package org.xbib.net.http.server;
import java.util.Map;
import org.xbib.net.Attributes;
import org.xbib.net.Parameter;
import org.xbib.net.ParameterBuilder;
@ -23,11 +22,15 @@ import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE;
public class BaseHttpServerContext implements HttpServerContext {
private static final Logger logger = Logger.getLogger(BaseHttpServerContext.class.getName());
private static final String PATH_SEPARATOR = "/";
private final Application application;
@ -84,16 +87,26 @@ public class BaseHttpServerContext implements HttpServerContext {
@Override
public void setResolverResult(HttpRouteResolver.Result<HttpService> pathResolverResult) {
this.pathResolverResult = pathResolverResult;
this.attributes.put("context", pathResolverResult.getContext());
this.attributes.put("handler", pathResolverResult.getValue());
this.attributes.put("pathparams", pathResolverResult.getParameter());
if (pathResolverResult != null) {
attributes.put("context", pathResolverResult.getContext());
attributes.put("handler", pathResolverResult.getValue());
attributes.put("pathparams", pathResolverResult.getParameter());
String contextPath = pathResolverResult.getContext() != null ?
PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null;
setContextPath(contextPath);
setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : ""));
this.httpRequest = createRequest(httpRequestBuilder);
this.attributes.put("request", httpRequest);
this.next = false;
} else {
// path resolver result null means "404 not found". Set default values.
attributes.put("context", null);
attributes.put("handler", null);
attributes.put("pathparams", null);
setContextPath(PATH_SEPARATOR);
setContextURL(request().getBaseURL());
}
httpRequest = createRequest();
logger.log(Level.FINER, "request = " + httpRequest);
attributes.put("request", httpRequest);
next = false;
}
public void setContextPath(String contextPath) {
@ -114,6 +127,7 @@ public class BaseHttpServerContext implements HttpServerContext {
return contextURL;
}
@Override
public Path resolve(String path) {
return application.resolve(path);
}
@ -187,8 +201,8 @@ public class BaseHttpServerContext implements HttpServerContext {
httpResponseBuilder.write(fileChannel, bufferSize);
}
protected HttpRequest createRequest(HttpRequestBuilder requestBuilder) {
HttpHeaders headers = requestBuilder.getHeaders();
protected HttpRequest createRequest() {
HttpHeaders headers = httpRequestBuilder.getHeaders();
String mimeType = headers.get(CONTENT_TYPE);
Charset charset = StandardCharsets.UTF_8;
if (mimeType != null) {
@ -198,30 +212,40 @@ public class BaseHttpServerContext implements HttpServerContext {
// helper URL to collect parameters in request URI
URL url = URL.builder()
.charset(charset, CodingErrorAction.REPLACE)
.path(requestBuilder.getRequestURI())
.path(httpRequestBuilder.getRequestURI())
.build();
ParameterBuilder formParameters = Parameter.builder().domain("FORM");
ParameterBuilder formParameterBuilder = Parameter.builder().domain("FORM");
// https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4
if (HttpMethod.POST.equals(requestBuilder.getMethod()) &&
if (HttpMethod.POST.equals(httpRequestBuilder.getMethod()) &&
(mimeType != null && mimeType.contains(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED))) {
Charset htmlCharset = getCharset(mimeType, StandardCharsets.ISO_8859_1);
CharBuffer charBuffer = requestBuilder.getBodyAsChars(htmlCharset);
CharBuffer charBuffer = httpRequestBuilder.getBodyAsChars(htmlCharset);
if (charBuffer != null) {
formParameters.addPercentEncodedBody(charBuffer.toString());
formParameterBuilder.addPercentEncodedBody(charBuffer.toString());
}
}
CookieBox cookieBox = attributes.get(CookieBox.class, "incomingcookies");
ParameterBuilder cookieParameters = Parameter.builder().domain("COOKIE");
ParameterBuilder cookieParameterBuilder = Parameter.builder().domain("COOKIE");
if (cookieBox != null) {
cookieBox.forEach(c -> cookieParameters.add(c.name(), c.value()));
cookieBox.forEach(c -> cookieParameterBuilder.add(c.name(), c.value()));
}
parameterBuilder.add(url.getQueryParams());
parameterBuilder.add(formParameters.build());
parameterBuilder.add(cookieParameters.build());
parameterBuilder.add(pathResolverResult.getParameter());
requestBuilder.setParameter(parameterBuilder.build());
requestBuilder.setContext(this);
return requestBuilder.build();
Parameter queryParameter = url.getQueryParams();
logger.log(Level.FINER, "adding query parameters = " + queryParameter.getDomain() + " " + queryParameter.allToString());
parameterBuilder.add(queryParameter);
Parameter formParameter = formParameterBuilder.build();
logger.log(Level.FINER, "adding form parameters = " + formParameter.getDomain() + " " + formParameter.allToString());
parameterBuilder.add(formParameter);
Parameter cookieParameter = cookieParameterBuilder.build();
logger.log(Level.FINER, "adding cookie parameters = " + cookieParameter.getDomain() + " " + cookieParameter.allToString());
parameterBuilder.add(cookieParameter);
if (pathResolverResult != null) {
Parameter pathParameter = pathResolverResult.getParameter();
logger.log(Level.FINER, "adding path parameters = " + pathParameter.getDomain() + " " + pathParameter.allToString());
parameterBuilder.add(pathParameter);
}
httpRequestBuilder.setParameter(parameterBuilder.build());
httpRequestBuilder.setContext(this);
return httpRequestBuilder.build();
}
private static Charset getCharset(String contentTypeValue, Charset defaultCharset) {

View file

@ -39,34 +39,37 @@ public class HtmlTemplateResource implements HttpServerResource {
private final String suffix;
protected final boolean negotiateLocale;
protected HtmlTemplateResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpServerContext httpServerContext) throws IOException {
this.templateResourceHandler = templateResourceHandler;
String indexFileName = templateResourceHandler.getIndexFileName();
Application application = httpServerContext.attributes().get(Application.class, "application");
this.negotiateLocale = application.getSettings().getAsBoolean("negotiateLocale", false);
Path root = templateResourceHandler.getPrefix();
root = root != null ? root : application.getHome();
if (root == null) {
throw new IllegalArgumentException("no home path set for template resource resolving");
}
logger.log(Level.FINE, "class = " + getClass().getName());
logger.log(Level.FINE, "root = " + root);
this.resourcePath = httpServerContext.request().getRequestPath().substring(1);
logger.log(Level.FINE, "resource path = " + resourcePath);
this.path = resourcePath.length() > 0 ? root.resolve(resourcePath) : root;
logger.log(Level.FINE, "path = " + path);
logger.log(Level.FINE, "index file name = " + indexFileName);
logger.log(Level.FINER, "class = " + getClass().getName() +
" root = " + root +
" resource path = " + resourcePath +
" path = " + path +
" index file name = " + indexFileName);
this.name = path.getFileName().toString();
this.baseName = AbstractResourceHandler.basename(name);
this.suffix = AbstractResourceHandler.suffix(name);
if (Files.isDirectory(path)) {
if (getIndexFileName() != null) {
Path indexPath = path.resolve(indexFileName);
logger.log(Level.FINE, "index path = " + indexPath);
logger.log(Level.FINE, "resolved to index path = " + indexPath);
if (Files.exists(indexPath)) {
logger.log(Level.FINE, "index path exists");
this.isExistsIndexFile = true;
this.path = indexPath;
logger.log(Level.FINE, "index file path found = " + path);
this.isDirectory = false;
} else {
this.isExistsIndexFile = false;
@ -81,15 +84,9 @@ public class HtmlTemplateResource implements HttpServerResource {
this.isDirectory = false;
}
this.isExists = Files.exists(path);
logger.log(Level.FINE, "exists = " + isExists);
logger.log(Level.FINE, "isDirectory = " + isDirectory);
this.url = URL.create(path.toUri().toString());
logger.log(Level.FINE, "url = " + url);
if (isExists) {
this.lastModified = Files.getLastModifiedTime(path).toInstant();
} else {
this.lastModified = Instant.now();
}
logger.log(Level.FINE, "isExists = " + isExists + " isDirectory = " + isDirectory + " url = " + url);
this.lastModified = isExists ? Files.getLastModifiedTime(path).toInstant() : Instant.now();
// length will be computed at rendering time
this.length = -1;
}

View file

@ -63,10 +63,9 @@ public class BaseHttpRoute implements HttpRoute {
@Override
public boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute) {
if (!(requestedRoute instanceof BaseHttpRoute)) {
if (!(requestedRoute instanceof BaseHttpRoute baseHttpRoute)) {
return false;
}
BaseHttpRoute baseHttpRoute = (BaseHttpRoute) requestedRoute;
if (!httpAddress.equals(baseHttpRoute.getHttpAddress())) {
return false;
}

View file

@ -29,7 +29,7 @@ public class BaseHttpRouteResolver<T> implements HttpRouteResolver<T> {
@Override
public void resolve(HttpRoute httpRoute, ResultListener<T> listener) {
for (Map.Entry<HttpRoute, T> entry : builder.routes) {
ParameterBuilder parameterBuilder = Parameter.builder();
ParameterBuilder parameterBuilder = Parameter.builder().domain("PATH");
boolean match = entry.getKey().matches(parameterBuilder, httpRoute);
if (match && listener != null) {
List<String> list = Arrays.stream(httpRoute.getPath().replaceFirst(builder.prefix, "").split("/"))

View file

@ -20,10 +20,12 @@ import org.xbib.net.http.server.Application;
import org.xbib.net.http.server.HttpDomain;
import org.xbib.net.http.server.HttpException;
import org.xbib.net.http.server.HttpHandler;
import org.xbib.net.http.server.HttpRequest;
import org.xbib.net.http.server.HttpRequestBuilder;
import org.xbib.net.http.server.HttpResponseBuilder;
import org.xbib.net.http.server.HttpServerContext;
import org.xbib.net.http.server.HttpService;
import org.xbib.net.http.server.handler.InternalServerErrorHandler;
import static org.xbib.net.http.HttpResponseStatus.NOT_FOUND;
@ -105,6 +107,7 @@ public class BaseHttpRouter implements HttpRouter {
}
if (httpRouteResolverResults.isEmpty()) {
logger.log(Level.FINE, "route resolver results is empty, generating a not found message");
httpServerContext.setResolverResult(null);
routeStatus(NOT_FOUND, httpServerContext);
return;
}
@ -113,7 +116,8 @@ public class BaseHttpRouter implements HttpRouter {
// first: create the final request
httpServerContext.setResolverResult(httpRouteResolverResult);
HttpService httpService = httpRouteResolverResult.getValue();
application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService, httpServerContext.httpRequest()));
HttpRequest httpRequest = httpServerContext.httpRequest();
application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService, httpRequest));
// second: security check, authentication etc.
if (httpService.getSecurityDomain() != null) {
logger.log(Level.FINEST, () -> "handling security domain service " + httpService);
@ -125,11 +129,12 @@ public class BaseHttpRouter implements HttpRouter {
if (httpServerContext.isDone() || httpServerContext.isFailed()) {
break;
}
// accept service and execute service
// after security checks, accept service, open and execute service
httpServerContext.attributes().put("service", httpService);
application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService));
logger.log(Level.FINEST, () -> "handling service " + httpService);
httpService.handle(httpServerContext);
// if service signals that work is done, break
if (httpServerContext.isDone() || httpServerContext.isFailed()) {
break;
}
@ -155,10 +160,17 @@ public class BaseHttpRouter implements HttpRouter {
@Override
public void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext) {
logger.log(Level.FINER, "routing status " + httpResponseStatus);
try {
HttpHandler httpHandler = getHandler(httpResponseStatus);
if (httpHandler == null) {
logger.log(Level.FINER, "handler for " + httpResponseStatus + " not present, using default error handler");
httpHandler = new InternalServerErrorHandler();
}
httpServerContext.response().reset();
httpHandler.handle(httpServerContext);
httpServerContext.done();
logger.log(Level.FINER, "routing status " + httpResponseStatus + " done");
} catch (IOException ioe) {
throw new IllegalStateException("unable to route response status, reason: " + ioe.getMessage(), ioe);
}

View file

@ -21,9 +21,9 @@ import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xbib.net.util.RandomUtil;
public class IncomingSessionHandler implements HttpHandler {
@ -46,13 +46,16 @@ public class IncomingSessionHandler implements HttpHandler {
*/
private final Set<String> suffixes;
Supplier<String> sessionIdGenerator;
public IncomingSessionHandler(String sessionSecret,
String sessionCookieAlgorithm,
String sessionCookieName,
Codec<Session> sessionCodec,
Set<String> suffixes,
String sessionUserName,
String sessionEffectiveUserName) {
String sessionEffectiveUserName,
Supplier<String> sessionIdGenerator) {
this.sessionSecret = sessionSecret;
this.sessionCookieAlgorithm = sessionCookieAlgorithm;
this.sessionCookieName = sessionCookieName;
@ -60,6 +63,7 @@ public class IncomingSessionHandler implements HttpHandler {
this.suffixes = suffixes;
this.sessionUserName = sessionUserName;
this.sessionEffectiveUserName = sessionEffectiveUserName;
this.sessionIdGenerator = sessionIdGenerator;
}
@Override
@ -76,6 +80,7 @@ public class IncomingSessionHandler implements HttpHandler {
if (session == null) {
try {
Map<String, Object> payload = decodeCookie(cookie);
logger.log(Level.FINER, "cookie decoded");
session = toSession(payload);
UserProfile userProfile = newUserProfile(payload, session);
if (userProfile != null) {
@ -96,14 +101,14 @@ public class IncomingSessionHandler implements HttpHandler {
}
if (session == null) {
try {
session = sessionCodec.create(RandomUtil.randomString(32));
session = sessionCodec.create(sessionIdGenerator.get());
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new HttpException("unable to create session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
context.attributes().put("session", session);
logger.log(Level.FINER, "incoming session " + session.id());
}
private Map<String, Object> decodeCookie(Cookie cookie) throws IOException,

View file

@ -47,22 +47,34 @@ public class OutgoingSessionHandler implements HttpHandler {
private final String sessionEffectiveUserName;
private final boolean httpOnly;
private final boolean secure;
private final SameSite sameSite;
public OutgoingSessionHandler(String sessionSecret,
String sessionCookieAlgorithm,
String sessionCookieName,
Duration sessionDuration,
Codec<Session> sessionCodec,
Set<String> suffixes,
String sessionUserName,
String sessionEffectiveUserName) {
String sessionEffectiveUserName,
Duration sessionDuration,
boolean httpOnly,
boolean secure,
SameSite sameSite) {
this.sessionSecret = sessionSecret;
this.sessionCookieAlgorithm = sessionCookieAlgorithm;
this.sessionCookieName = sessionCookieName;
this.sessionDuration = sessionDuration;
this.sessionCodec = sessionCodec;
this.suffixes = suffixes;
this.sessionUserName = sessionUserName;
this.sessionEffectiveUserName = sessionEffectiveUserName;
this.sessionDuration = sessionDuration;
this.httpOnly = httpOnly;
this.secure = secure;
this.sameSite = sameSite;
}
@Override
@ -137,28 +149,32 @@ public class OutgoingSessionHandler implements HttpHandler {
PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1);
DefaultCookie cookie = new DefaultCookie(sessionCookieName, percentEncoder.encode(cookieValue));
String domain = extractDomain(host);
if (!"localhost".equals(domain)) {
if ("localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else {
cookie.setDomain('.' + domain);
}
cookie.setPath(path);
cookie.setMaxAge(sessionDuration.toSeconds());
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setSameSite(SameSite.LAX);
cookie.setHttpOnly(httpOnly);
cookie.setSecure(secure);
cookie.setSameSite(sameSite);
return cookie;
}
private Cookie createEmptyCookie(String host, String path) {
DefaultCookie cookie = new DefaultCookie(sessionCookieName);
String domain = extractDomain(host);
if (!"localhost".equals(domain)) {
if ("localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else {
cookie.setDomain('.' + domain);
}
cookie.setPath(path);
cookie.setMaxAge(0L);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setSameSite(SameSite.LAX);
cookie.setHttpOnly(httpOnly);
cookie.setSecure(secure);
cookie.setSameSite(sameSite);
logger.log(Level.FINEST, "baked empty cookie");
return cookie;
}

View file

@ -44,28 +44,31 @@ public abstract class DefaultMarkupTemplate extends BaseTemplate {
super(templateEngine, model, modelTypes, configuration);
this.application = (Application) model.get("application");
Objects.requireNonNull(this.application, "application must not be null");
this.session = (Session) model.get("session");
Objects.requireNonNull(this.session, "session must not be null");
this.request = (HttpRequest) model.get("request");
Objects.requireNonNull(this.request, "request must not be null");
this.responseBuilder = (HttpResponseBuilder) model.get("responsebuilder");
Objects.requireNonNull(this.responseBuilder, "response must not be null");
this.request = (HttpRequest) model.get("request");
// request can be null in error templates
this.session = (Session) model.get("session");
// session can be null in error templates
}
public void responseStatus(HttpResponseStatus responseStatus) {
public void setResponseStatus(HttpResponseStatus responseStatus) {
responseBuilder.setResponseStatus(responseStatus);
}
public void contentType(String contentType) {
public void setContentType(String contentType) {
responseBuilder.setHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
}
public boolean isContentType(String contentType) {
return request.getHeaders().containsHeader(HttpHeaderNames.CONTENT_TYPE) && contentType != null &&
return request != null &&
request.getHeaders() != null &&
request.getHeaders().containsHeader(HttpHeaderNames.CONTENT_TYPE) &&
contentType != null &&
request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE).startsWith(contentType);
}
public void contentDisposition(String contentDisposition) {
public void setContentDisposition(String contentDisposition) {
responseBuilder.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition);
}

View file

@ -2,13 +2,11 @@ package org.xbib.net.http.template.groovy;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xbib.net.URL;
import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.Application;
import org.xbib.net.http.server.HttpServerContext;
import java.io.IOException;
import java.nio.file.Path;
class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource {
@ -47,36 +45,11 @@ class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource {
groovyTemplateRenderer.handle(httpServerContext);
}
@Override
public Path getPath() {
return null;
}
@Override
public String getName() {
return "status-resource";
}
@Override
public String getBaseName() {
return null;
}
@Override
public String getSuffix() {
return null;
}
@Override
public String getResourcePath() {
return null;
}
@Override
public URL getURL() {
return null;
}
@Override
public boolean isExists() {
return true;

View file

@ -17,9 +17,9 @@ import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -28,8 +28,16 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
private static final Logger logger = Logger.getLogger(GroovyTemplateResource.class.getName());
private static final Map<Path, Template> templates = new HashMap<>();
/**
* This class might be reused by mtultiple handlers.
* We use a concurrent hash map here because in case of "not found" error templating,
* the "computeIfAbsent" can be called in a nested way, and would throw a concurrent modification exception.
*/
private static final Map<Path, Template> templates = new ConcurrentHashMap<>();
/**
* This lock ensures that changing locale and rendering is executed in a pairwisely manner.
*/
private static final ReentrantLock lock = new ReentrantLock();
protected GroovyTemplateResource(HtmlTemplateResourceHandler templateResourceHandler,
@ -39,7 +47,7 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
@Override
public void render(HttpServerContext httpServerContext) throws IOException {
logger.log(Level.FINE, "rendering groovy template, path = " + getPath() + " isExists = " + isExists() + " isDirectory =" + isDirectory() );
logger.log(Level.FINER, "rendering groovy template, path = " + getPath() + " isExists = " + isExists() + " isDirectory =" + isDirectory() );
Application application = httpServerContext.attributes().get(Application.class, "application");
if (application == null) {
logger.log(Level.WARNING, "application is null");
@ -52,34 +60,36 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
}
Path templatePath = getPath();
HttpService service = httpServerContext.attributes().get(HttpService.class, "service");
GroovyTemplateService groovyTemplateService = (GroovyTemplateService) service;
if (service instanceof GroovyTemplateService groovyTemplateService) {
if (groovyTemplateService.getTemplateName() != null) {
templatePath = application.resolve(groovyTemplateService.getTemplateName());
logger.log(Level.FINE, "templatePath after application.resolve() = " + templatePath);
logger.log(Level.FINER, "templatePath after application.resolve() = " + templatePath);
} else {
logger.log(Level.FINE, "the GroovyTemplateService does not have a template name set");
logger.log(Level.FINER, "the GroovyTemplateService does not have a templateName");
}
logger.log(Level.FINE, "templatePath = " + templatePath);
}
// status response handlers have priority
GroovyHttpResonseStatusTemplateResource resource = httpServerContext.attributes().get(GroovyHttpResonseStatusTemplateResource.class, "_resource");
if (resource != null) {
logger.log(Level.FINE, "Groovy HTTP status response rendering");
String indexFileName = resource.getIndexFileName();
if (indexFileName != null) {
templatePath = application.resolve(indexFileName);
}
}
logger.log(Level.FINER, "rendering Groovy HTTP status response with templatePath = " + templatePath);
} else {
// override if 'templatePath' attribute is set
String overridePath = httpServerContext.attributes().get(String.class, "templatePath");
if (overridePath != null) {
logger.log(Level.FINE, "found override templatePath = " + overridePath);
logger.log(Level.FINER, "found override templatePath = " + overridePath);
templatePath = application.resolve(overridePath);
logger.log(Level.FINE, "found override templatePath, resolved to " + templatePath);
logger.log(Level.FINER, "found override templatePath, resolved to " + templatePath);
}
if (templatePath == null) {
logger.log(Level.FINE, "templatePath is null, OOTB effort on " + getIndexFileName());
logger.log(Level.FINER, "templatePath is null, OOTB effort on " + getIndexFileName());
// OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName()
templatePath = application.resolve(getIndexFileName());
}
}
if (isDirectory()) {
if (isExistsIndexFile()) {
templatePath = getPath().resolve(getIndexFileName());
@ -88,11 +98,15 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
throw new HttpException("forbidden", httpServerContext, HttpResponseStatus.FORBIDDEN);
}
}
logger.log(Level.FINE, "rendering groovy template " + templatePath);
if (templatePath == null) {
logger.log(Level.WARNING, "unable to render a null path");
throw new HttpException("internal path error", httpServerContext, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
templates.computeIfAbsent(templatePath, path -> {
try {
logger.log(Level.FINEST, "groovy templatePath = " + path + " creating by template engine");
return templateEngine.createTemplate(Files.readString(path));
} catch (ClassNotFoundException | IOException e) {
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
});
@ -104,12 +118,25 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
binding.setVariable("log", templateLogger);
application.getModules().forEach(m -> binding.setVariable(m.getName(), m));
DefaultTemplateResolver templateResolver = httpServerContext.attributes().get(DefaultTemplateResolver.class, "templateresolver");
if (templateResolver != null) {
if (templateResolver == null) {
// for Groovy template engines without a resolver, no need to set a locale
Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable);
return;
}
if (!negotiateLocale) {
// if no locale negotiation configured, set always the applicaiton locale. This constant value never changes.
templateResolver.setLocale(application.getLocale());
Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable);
return;
}
// handle programmatic locale change plus template making under lock so no other request/response can interrupt us
logger.log(Level.FINER, "application locale for template = " + application.getLocale());
try {
lock.lock();
templateResolver.setLocale(application.getLocale());
// language from request overrides application locale
String acceptLanguage = httpServerContext.request().getHeaders().get(HttpHeaderNames.ACCEPT_LANGUAGE);
if (acceptLanguage != null) {
Locale negotiatedLocale = LocaleNegotiator.findLocale(acceptLanguage);
@ -121,18 +148,12 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable);
} catch (Exception e) {
// in case there is not template with negotiated locale
// fail silently by ignoring negotation
templateResolver.setLocale(application.getLocale());
Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable);
} finally {
lock.unlock();
}
} else {
// for Groovy template engines without a resolver
Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable);
}
logger.log(Level.FINER, "rendering done: " + httpServerContext.isDone());
}
}

View file

@ -8,7 +8,7 @@ dependencyResolutionManagement {
version('netty-tcnative', '2.0.59.Final')
version('datastructures', '2.0.0')
version('config', '5.0.2')
version('net', '3.0.3')
version('net', '3.0.4')
library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit')
library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit')
library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit')