
ninja.utils.AbstractContext Maven / Gradle / Ivy
/**
* Copyright (C) the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ninja.utils;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import ninja.bodyparser.BodyParserEngine;
import ninja.bodyparser.BodyParserEngineManager;
import ninja.params.ParamParsers;
import ninja.session.FlashScope;
import ninja.session.Session;
import ninja.validation.Validation;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import com.google.inject.Injector;
import ninja.ContentTypes;
import ninja.Context;
import ninja.Cookie;
import ninja.Result;
import ninja.Route;
/**
* Abstract Context.Impl that implements features that are not reliant
* on the concrete Context implementation. For example, a concrete implementation
* may have to provide a getHeader()
method, but this class could
* supply a default implementation of getAcceptContentType()
since
* it only needs to fetch a value from getHeader()
.
*
* When adding features to a Context
please think about whether
* it should be fully or partially implemented here or in the concrete implementation.
*/
abstract public class AbstractContext implements Context.Impl {
private static final Splitter splitter = Splitter.on(',')
.trimResults()
.omitEmptyStrings();
static final private Logger logger = LoggerFactory.getLogger(AbstractContext.class);
// subclasses need to access these
final protected BodyParserEngineManager bodyParserEngineManager;
final protected FlashScope flashScope;
final protected NinjaProperties ninjaProperties;
final protected Session session;
final protected Validation validation;
final protected Injector injector;
final protected ParamParsers paramParsers;
protected Route route;
// in async mode these values will be set to null so its critical they
// are saved when a context is initialized
private String requestPath;
private String contextPath;
@Inject
public AbstractContext(
BodyParserEngineManager bodyParserEngineManager,
FlashScope flashScope,
NinjaProperties ninjaProperties,
Session session,
Validation validation,
Injector injector,
ParamParsers paramParsers) {
this.bodyParserEngineManager = bodyParserEngineManager;
this.flashScope = flashScope;
this.ninjaProperties = ninjaProperties;
this.session = session;
this.validation = validation;
this.injector = injector;
this.paramParsers = paramParsers;
}
protected void init(String contextPath, String requestPath) {
this.contextPath = contextPath;
this.requestPath = requestPath;
// init flash scope:
flashScope.init(this);
// init session scope:
session.init(this);
}
@Override
public void setRoute(Route route) {
this.route = route;
}
@Override
public Route getRoute() {
return route;
}
@Override
public Validation getValidation() {
return validation;
}
@Deprecated
@Override
public FlashScope getFlashCookie() {
return flashScope;
}
@Override
public FlashScope getFlashScope() {
return flashScope;
}
@Deprecated
@Override
public Session getSessionCookie() {
return session;
}
@Override
public Session getSession() {
return session;
}
@Override
public String getContextPath() {
return contextPath;
}
@Override
public String getRequestPath() {
return requestPath;
}
/**
* Get the "real" address of the client connection. Does not take any
* header (e.g. X-Forwarded-For) into account.
* @return The real address of the client connection
*/
abstract protected String getRealRemoteAddr();
@Override
public String getRemoteAddr() {
boolean isUsageOfXForwardedHeaderEnabled
= ninjaProperties.getBooleanWithDefault(
Context.NINJA_PROPERTIES_X_FORWARDED_FOR, false);
if (isUsageOfXForwardedHeaderEnabled) {
String forwardHeader = getHeader(X_FORWARD_HEADER);
if (forwardHeader != null) {
if (forwardHeader.contains(",")) {
// sometimes the header is of form client ip,proxy 1 ip,proxy 2 ip,...,proxy n ip,
// we just want the client
forwardHeader = StringUtils.split(forwardHeader, ',')[0].trim();
}
try {
// If ip4/6 address string handed over, simply does pattern validation.
InetAddress.getByName(forwardHeader);
return forwardHeader;
} catch (UnknownHostException e) {
// give up
}
}
}
// fallback to the real remote address
return getRealRemoteAddr();
}
@Override
public T getAttribute(String name, Class clazz) {
return clazz.cast(getAttribute(name));
}
@Override
public String getPathParameter(String key) {
String encodedParameter = route.getPathParametersEncoded(
getRequestPath()).get(key);
if (encodedParameter == null) {
return null;
} else {
return URI.create(encodedParameter).getPath();
}
}
@Override
public String getPathParameterEncoded(String key) {
return route.getPathParametersEncoded(getRequestPath()).get(key);
}
@Override
public Integer getPathParameterAsInteger(String key) {
String parameter = getPathParameter(key);
try {
return Integer.parseInt(parameter);
} catch (NumberFormatException e) {
return null;
}
}
@Override
public String getParameter(String key, String defaultValue) {
String parameter = getParameter(key);
if (parameter == null) {
parameter = defaultValue;
}
return parameter;
}
@Override
public Integer getParameterAsInteger(String key) {
return getParameterAsInteger(key, null);
}
@Override
public Integer getParameterAsInteger(String key, Integer defaultValue) {
String parameter = getParameter(key);
try {
return Integer.parseInt(parameter);
} catch (NumberFormatException e) {
return defaultValue;
}
}
@Override
public T getParameterAs(String key, Class clazz) {
return getParameterAs(key, clazz, null);
}
@Override
public T getParameterAs(String key, Class clazz, T defaultValue) {
String parameter = getParameter(key);
T value = (T) paramParsers.getParamParser(clazz).parseParameter(key, parameter, validation);
return validation.hasViolation(key) ? defaultValue : value;
}
@Override
public T parseBody(Class classOfT) {
String rawContentType = getRequestContentType();
// If the Content-type: xxx header is not set we return null.
// we cannot parse that request.
if (rawContentType == null) {
logger.debug("Not able to parse body because request did not send content type header at: {}", getRequestPath());
return null;
}
// If Content-type is application/json; charset=utf-8 we split away the charset
// application/json
String contentTypeOnly = HttpHeaderUtils.getContentTypeFromContentTypeAndCharacterSetting(
rawContentType);
BodyParserEngine bodyParserEngine = bodyParserEngineManager
.getBodyParserEngineForContentType(contentTypeOnly);
if (bodyParserEngine == null) {
logger.debug("No BodyParserEngine found for Content-Type {} at route {}", contentTypeOnly, getRequestPath());
return null;
}
return bodyParserEngine.invoke(this, classOfT);
}
@Override
public String getCookieValue(String name) {
Cookie cookie = getCookie(name);
if (cookie == null) {
return null;
}
return cookie.getValue();
}
@Override
public void unsetCookie(Cookie cookie) {
addCookie(Cookie.builder(cookie).setMaxAge(0).build());
}
@Override
public void asyncRequestComplete() {
returnResultAsync(null);
}
protected ResponseStreams finalizeHeaders(Result result, Boolean handleFlashAndSessionCookie) {
// copy ninja flash and session data directory to this context
if (handleFlashAndSessionCookie) {
flashScope.save(this);
session.save(this);
}
// copy any cookies from result
for (ninja.Cookie cookie : result.getCookies()) {
addCookie(cookie);
}
// subclasses responsible for creating the ResponseStreams instance
return null;
}
@Override
public ResponseStreams finalizeHeadersWithoutFlashAndSessionCookie(Result result) {
return finalizeHeaders(result, false);
}
@Override
public ResponseStreams finalizeHeaders(Result result) {
return finalizeHeaders(result, true);
}
@Override
public String getAcceptContentType() {
final String contentType = getHeader("accept");
if (Strings.isNullOrEmpty(contentType)) {
return Result.TEXT_HTML;
}
final String bestMatch = splitter.splitToList(contentType).stream()
.map(MediaRange::new)
.filter(mediaRange -> mediaRange.weight != 0.0)
.sorted()
// 2021 chrome, android, etc. make a signed exchange the highest weight
.filter(v -> v.mediaRange != null && !v.mediaRange.contains("application/signed-exchange"))
.findFirst()
.flatMap(mediaRange -> Optional.of(mediaRange.mediaRange))
.orElse("text/html"); // fall back to text/html, if there are no good media ranges
if (bestMatch.contains("application/xhtml")
|| bestMatch.contains("text/html")
|| bestMatch.startsWith("*/*")) {
return Result.TEXT_HTML;
}
if (bestMatch.contains("application/xml")
|| bestMatch.contains("text/xml")) {
return Result.APPLICATION_XML;
}
if (bestMatch.contains("application/json")
|| bestMatch.contains("text/javascript")) {
return Result.APPLICATION_JSON;
}
if (bestMatch.contains("text/plain")) {
return Result.TEXT_PLAIN;
}
if (bestMatch.contains("application/octet-stream")) {
return Result.APPLICATION_OCTET_STREAM;
}
if (bestMatch.endsWith("*/*")) {
return Result.TEXT_HTML;
}
return bestMatch;
}
@Override
public String getAcceptEncoding() {
return getHeader("accept-encoding");
}
@Override
public String getAcceptLanguage() {
return getHeader("accept-language");
}
@Override
public String getAcceptCharset() {
return getHeader("accept-charset");
}
@Override
public boolean isRequestJson() {
String contentType = getRequestContentType();
if (contentType == null || contentType.isEmpty()) {
return false;
}
return contentType.startsWith(ContentTypes.APPLICATION_JSON);
}
@Override
public boolean isRequestXml() {
String contentType = getRequestContentType();
if (contentType == null || contentType.isEmpty()) {
return false;
}
return contentType.startsWith(ContentTypes.APPLICATION_XML);
}
private static final class MediaRange implements Comparable {
@Override
public int compareTo(Object o) {
MediaRange other = (MediaRange) o;
// Compare the ranges for specificity
if (this.extensions.size() < other.extensions.size()) {
// this media range is more specific and should be assigned higher priority
return 1;
} else if (this.extensions.size() > other.extensions.size()) {
// the other range is more specific and should be used
return -1;
}
if (this.extensions.size() == other.extensions.size()) {
// these two ranges are equally specific and the weights are the deciding factor
return Double.compare(other.weight, this.weight);
}
return 0;
}
private static final Splitter splitter = Splitter.on(';').trimResults();
private String mediaRange;
private double weight;
private List extensions;
public MediaRange(String value) {
List parts = splitter.splitToList(value);
this.mediaRange = parts.get(0);
Optional weight = parts.stream().filter(part -> part.startsWith("q=")).findFirst();
this.weight = Double.parseDouble(weight.orElse("q=1").replace("q=", ""));
this.extensions = parts.stream().filter(part -> !part.startsWith("q=")).filter(part -> part.contains("=")).collect(Collectors.toList());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy