All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.spincast.plugins.response.SpincastResponseRequestContextAddon Maven / Gradle / Ivy

package org.spincast.plugins.response;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spincast.core.config.SpincastConfig;
import org.spincast.core.config.SpincastConstants;
import org.spincast.core.config.SpincastConstants.ResponseModelVariables;
import org.spincast.core.cookies.Cookie;
import org.spincast.core.cookies.CookieFactory;
import org.spincast.core.cookies.CookieSameSite;
import org.spincast.core.exchange.RequestContext;
import org.spincast.core.exchange.ResponseRequestContextAddon;
import org.spincast.core.flash.FlashMessage;
import org.spincast.core.flash.FlashMessageFactory;
import org.spincast.core.flash.FlashMessageLevel;
import org.spincast.core.flash.FlashMessagesHolder;
import org.spincast.core.json.JsonArray;
import org.spincast.core.json.JsonManager;
import org.spincast.core.json.JsonObject;
import org.spincast.core.request.Form;
import org.spincast.core.response.Alert;
import org.spincast.core.response.AlertDefault;
import org.spincast.core.response.AlertLevel;
import org.spincast.core.routing.ETagFactory;
import org.spincast.core.routing.HttpMethod;
import org.spincast.core.routing.ResourceToPush;
import org.spincast.core.server.Server;
import org.spincast.core.utils.Bool;
import org.spincast.core.utils.ContentTypeDefaults;
import org.spincast.core.utils.GzipOption;
import org.spincast.core.utils.SpincastStatics;
import org.spincast.core.utils.SpincastUtils;
import org.spincast.core.xml.XmlManager;
import org.spincast.shaded.org.apache.commons.io.IOUtils;
import org.spincast.shaded.org.apache.commons.lang3.StringUtils;
import org.spincast.shaded.org.apache.commons.lang3.time.DateUtils;
import org.spincast.shaded.org.apache.http.HttpStatus;
import org.spincast.shaded.org.apache.http.client.utils.URIBuilder;

import com.google.common.net.HttpHeaders;
import com.google.inject.Inject;

public class SpincastResponseRequestContextAddon>
                                                implements ResponseRequestContextAddon {

    protected static final Logger logger = LoggerFactory.getLogger(SpincastResponseRequestContextAddon.class);

    protected static final boolean IS_RESPONSE_CHARACTERS_BASED_BY_DEFAULT = false;

    private final R requestContext;
    private final Server server;
    private final JsonManager jsonManager;
    private final XmlManager xmlManager;
    private final SpincastConfig spincastConfig;
    private final SpincastUtils spincastUtils;
    private final ETagFactory etagFactory;
    private final FlashMessagesHolder flashMessagesHolder;
    private final FlashMessageFactory flashMessageFactory;
    private final CookieFactory cookieFactory;

    private String responseContentType = null;
    private int responseStatusCode = HttpStatus.SC_OK;
    private final ByteArrayOutputStream byteArrayOutputStreamIn = new ByteArrayOutputStream(256);
    private final ByteArrayOutputStream byteArrayOutputStreamOut = new ByteArrayOutputStream(256);
    private GZIPOutputStream gzipOutputStream = null;
    private Bool isShouldGzip = null;

    private String charactersCharsetName = "UTF-8";
    private boolean isResponseCharactersBased = IS_RESPONSE_CHARACTERS_BASED_BY_DEFAULT;
    private boolean requestSizeValidated = false;
    private Map> headers;
    private GzipOption gzipOption = GzipOption.DEFAULT;
    private Map cookies;
    private Set resourcesToPush;

    private JsonObject responseModel;

    @Inject
    public SpincastResponseRequestContextAddon(R requestContext,
                                               Server server,
                                               JsonManager jsonManager,
                                               XmlManager xmlManager,
                                               SpincastConfig spincastConfig,
                                               SpincastUtils spincastUtils,
                                               ETagFactory etagFactory,
                                               FlashMessagesHolder flashMessagesHolder,
                                               FlashMessageFactory flashMessageFactory,
                                               CookieFactory cookieFactory) {
        this.requestContext = requestContext;
        this.server = server;
        this.jsonManager = jsonManager;
        this.xmlManager = xmlManager;
        this.spincastConfig = spincastConfig;
        this.spincastUtils = spincastUtils;
        this.etagFactory = etagFactory;
        this.flashMessagesHolder = flashMessagesHolder;
        this.flashMessageFactory = flashMessageFactory;
        this.cookieFactory = cookieFactory;
    }

    protected R getRequestContext() {
        return this.requestContext;
    }

    protected Server getServer() {
        return this.server;
    }

    protected Object getExchange() {
        return getRequestContext().exchange();
    }

    protected JsonManager getJsonManager() {
        return this.jsonManager;
    }

    protected XmlManager getXmlManager() {
        return this.xmlManager;
    }

    protected SpincastConfig getSpincastConfig() {
        return this.spincastConfig;
    }

    protected SpincastUtils getSpincastUtils() {
        return this.spincastUtils;
    }

    protected ETagFactory getEtagFactory() {
        return this.etagFactory;
    }

    protected FlashMessagesHolder getFlashMessagesHolder() {
        return this.flashMessagesHolder;
    }

    protected FlashMessageFactory getFlashMessageFactory() {
        return this.flashMessageFactory;
    }

    protected CookieFactory getCookieFactory() {
        return this.cookieFactory;
    }

    protected ByteArrayOutputStream getBuffer() {
        return this.byteArrayOutputStreamIn;
    }

    protected ByteArrayOutputStream getOut() {
        return this.byteArrayOutputStreamOut;
    }

    @Override
    public JsonObject getModel() {
        if (this.responseModel == null) {
            this.responseModel = getJsonManager().create();
        }
        return this.responseModel;
    }

    public GZIPOutputStream getGzipBuffer() {
        try {
            if (this.gzipOutputStream == null) {
                this.gzipOutputStream = new GZIPOutputStream(getOut(), true);
            }
            return this.gzipOutputStream;
        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    protected boolean isRequestSizeValidated() {
        return this.requestSizeValidated;
    }

    protected void setRequestSizeValidated(boolean requestSizeValidated) {
        this.requestSizeValidated = requestSizeValidated;
    }

    @Override
    public ResponseRequestContextAddon setGzipOption(GzipOption gzipOption) {

        if (gzipOption == null) {
            gzipOption = GzipOption.DEFAULT;
        }

        if (isHeadersSent() && gzipOption != getGzipOption()) {
            logger.warn("The headers are sent, you can't change the gzip options.");
            return this;
        }

        this.gzipOption = gzipOption;

        return this;
    }

    @Override
    public GzipOption getGzipOption() {
        return this.gzipOption;
    }

    @Override
    public int getStatusCode() {
        return this.responseStatusCode;
    }

    @Override
    public ResponseRequestContextAddon setStatusCode(int responseStatusCode) {

        if (isHeadersSent()) {
            if (responseStatusCode != getStatusCode()) {
                logger.warn("Response headers already sent, the http status code can't be changed...");
            }
        } else {
            this.responseStatusCode = responseStatusCode;
        }

        return this;
    }

    @Override
    public String getContentType() {
        return this.responseContentType;
    }

    @Override
    public ResponseRequestContextAddon setContentType(String responseContentType) {

        if (isHeadersSent()) {
            if (responseContentType != null && !responseContentType.equals(getContentType())) {
                logger.warn("Response headers already sent, the content-type can't be changed...");
            }
        } else {

            if ((responseContentType != null && !responseContentType.equals(getContentType())) ||
                (responseContentType == null && getContentType() != null)) {

                //==========================================
                // isShouldGzip will be revalidated since it can
                // change depending of the content-type.
                //==========================================
                GzipOption gzipOption = getGzipOption();
                if (gzipOption == GzipOption.DEFAULT) {
                    this.isShouldGzip = null;
                }
            }
            this.responseContentType = responseContentType;
        }

        return this;
    }

    /**
     * Try to determine is the response is characters based or not.
     * This is allow us to use a default Content-Type header if none
     * was specified.
     */
    protected boolean isResponseCharactersBased() {
        return this.isResponseCharactersBased;
    }

    @Override
    public boolean isClosed() {
        return getServer().isResponseClosed(getExchange());
    }

    @Override
    public void redirect() {
        redirect("", false, null);
    }

    @Override
    public void redirect(FlashMessage flashMessage) {
        redirect("", false, flashMessage);
    }

    @Override
    public void redirect(FlashMessageLevel flashMessageType, String flashMessageText) {
        redirect(flashMessageType, flashMessageText, null);
    }

    @Override
    public void redirect(FlashMessageLevel flashMessageType, String flashMessageText, JsonObject flashMessageVariables) {
        redirect(getFlashMessageFactory().create(flashMessageType, flashMessageText, flashMessageVariables));
    }

    @Override
    public void redirect(String newUrl) {
        redirect(newUrl, false, null);
    }

    @Override
    public void redirect(String newUrl, FlashMessage flashMessage) {
        redirect(newUrl, false, flashMessage);
    }

    @Override
    public void redirect(String newUrl, FlashMessageLevel flashMessageType, String flashMessageText) {
        redirect(newUrl, flashMessageType, flashMessageText, null);
    }

    @Override
    public void redirect(String newUrl, FlashMessageLevel flashMessageType, String flashMessageText,
                         JsonObject flashMessageVariables) {
        redirect(newUrl, getFlashMessageFactory().create(flashMessageType, flashMessageText, flashMessageVariables));
    }

    @Override
    public void redirect(String newUrl, JsonObject flashMessageVariables) {
        redirect(newUrl, getFlashMessageFactory().create(FlashMessageLevel.SUCCESS, null, flashMessageVariables));
    }

    @Override
    public void redirect(String newUrl, boolean permanently) {
        redirect(newUrl, permanently, null);
    }

    @Override
    public void redirect(String newUrl, boolean permanently, FlashMessage flashMessage) {
        if (permanently) {
            redirect(newUrl, HttpStatus.SC_MOVED_PERMANENTLY, flashMessage);
        } else {
            redirect(newUrl, HttpStatus.SC_MOVED_TEMPORARILY, flashMessage);
        }
    }

    @Override
    public void redirect(String newUrl, boolean permanently, FlashMessageLevel flashMessageType, String flashMessageText) {
        redirect(newUrl, permanently, getFlashMessageFactory().create(flashMessageType, flashMessageText));
    }

    @Override
    public void redirect(String newUrl, boolean permanently, FlashMessageLevel flashMessageType, String flashMessageText,
                         JsonObject flashMessageVariables) {
        redirect(newUrl, permanently, getFlashMessageFactory().create(flashMessageType, flashMessageText, flashMessageVariables));
    }

    @Override
    public void redirect(String newUrl, int specific3xxCode) {
        redirect(newUrl, specific3xxCode, null);
    }

    @Override
    public void redirect(String newUrl, int specific3xxCode, FlashMessageLevel flashMessageType, String flashMessageText) {
        redirect(newUrl, specific3xxCode, getFlashMessageFactory().create(flashMessageType, flashMessageText));
    }

    @Override
    public void redirect(String newUrl, int specific3xxCode, FlashMessageLevel flashMessageType, String flashMessageText,
                         JsonObject flashMessageVariables) {
        redirect(newUrl,
                 specific3xxCode,
                 getFlashMessageFactory().create(flashMessageType, flashMessageText, flashMessageVariables));
    }

    @Override
    public void redirect(String newUrl, int specific3xxCode, FlashMessage flashMessage) {

        try {
            if (isHeadersSent()) {
                throw new RuntimeException("Can't set redirect, the headers are already sent.");
            }

            setStatusCode(specific3xxCode);

            //==========================================
            // If the URL to redirect to is empty, we
            // redirect to the current page.
            //==========================================
            if (StringUtils.isBlank(newUrl)) {
                newUrl = getRequestContext().request().getFullUrlOriginal();
            } else {
                newUrl = newUrl.trim();

                // @see http://stackoverflow.com/a/12840255/843699
                if (newUrl.startsWith("//")) {
                    newUrl = getServer().getRequestScheme(getExchange()) + ":" + newUrl;
                }
            }

            URI uri = new URI(newUrl);
            if (!uri.isAbsolute()) {

                URI currentUri = new URI(getRequestContext().request().getFullUrl());

                String anchor = null;
                if (uri.getFragment() != null) {
                    anchor = uri.getFragment();
                }

                String queryString = null;
                if (uri.getQuery() != null) {
                    queryString = uri.getQuery();
                }

                //==========================================
                // Simple anchor or queryString?
                // We use the current path.
                //==========================================
                String path = uri.getPath();
                if (newUrl.startsWith("#") || newUrl.startsWith("?")) {
                    path = currentUri.getPath();
                }
                //==========================================
                // Relative path?
                // We happen it to the current path.
                //==========================================
                else if (!newUrl.startsWith("/")) {

                    String currentPath = currentUri.getPath();
                    currentPath = StringUtils.strip(currentPath, "/");

                    int lastSlashPos = currentPath.lastIndexOf("/");
                    if (lastSlashPos < 0) {
                        path = "/" + path;
                    } else {
                        path = "/" + currentPath.substring(0, lastSlashPos) + "/" + path;
                    }
                }

                uri = new URI(currentUri.getScheme(),
                              currentUri.getUserInfo(),
                              currentUri.getHost(),
                              currentUri.getPort(),
                              path,
                              queryString,
                              anchor);

                newUrl = uri.toString();
            }

            //==========================================
            // Flash message to use?
            //==========================================
            if (flashMessage != null) {
                newUrl = saveFlashMessage(newUrl, flashMessage);
            }

            setHeader(HttpHeaders.LOCATION, newUrl);

        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    /**
     * Saves a Flash message.
     *
     * Returned a modified version of the final URL to redirect to,
     * if required.
     */
    protected String saveFlashMessage(String url, FlashMessage flashMessage) {

        if (flashMessage == null) {
            return url;
        }

        String flashMessageId = getFlashMessagesHolder().saveFlashMessage(flashMessage);

        //==========================================
        // TODO Maybe we should use a user session.
        //==========================================
        if (getRequestContext().request().isCookiesEnabledValidated()) {
            getRequestContext().response().setCookieSession(getSpincastConfig().getCookieNameFlashMessage(), flashMessageId);
            return url;

        //==========================================@formatter:off
        // We add the id of the flash message to the
        // redirected URL since we don't know if cookies
        // are enabled or not.
        //==========================================@formatter:on
        } else {

            try {

                URIBuilder builder = new URIBuilder(url);
                builder.setParameter(getSpincastConfig().getQueryParamFlashMessageId(), flashMessageId);
                return builder.build().toString();

            } catch (Exception ex) {
                throw SpincastStatics.runtimize(ex);
            }
        }
    }

    @Override
    public void sendBytes(byte[] bytes) {
        sendBytes(bytes, null, false);
    }

    @Override
    public void sendBytes(byte[] bytes, String contentType) {
        sendBytes(bytes, contentType, false);
    }

    /**
     * Send some bytes + flush if specified.
     */
    @Override
    public void sendBytes(byte[] bytes, String contentType, boolean flush) {
        send(bytes, contentType, flush);
    }

    protected void send(byte[] bytes, String contentType, boolean flush) {

        if (isClosed()) {
            logger.debug("The response is closed, nothing more can be sent!");
            return;
        }

        if (isHeadersSent()) {
            if (contentType != null && !contentType.equals(getContentType())) {
                logger.warn("Response headers are already sent, the content-type won't be changed...");
            }
        } else {
            if (contentType != null) {
                if (getContentType() != null && !contentType.equals(getContentType())) {
                    logger.warn("The content-type is changed from " + getContentType() + " to " + contentType);
                }
                setContentType(contentType);
            }
        }

        if (bytes != null) {
            try {
                getBuffer().write(bytes);
            } catch (Exception ex) {
                throw SpincastStatics.runtimize(ex);
            }
        }

        if (flush) {
            flush();
        }
    }

    @Override
    public void sendCharacters(String content, String contentType) {
        sendCharacters(content, contentType, false);
    }

    @Override
    public void sendCharacters(String content, String contentType, boolean flush) {

        try {

            //==========================================
            // The response is probably characters based.
            // This will only help us set a default COntent-Type
            // header if none was set.
            //==========================================
            this.isResponseCharactersBased = true;

            byte[] bytes = null;
            if (content != null) {
                bytes = content.getBytes(getCharactersCharsetName());
            }
            send(bytes, contentType, flush);
        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    @Override
    public String getCharactersCharsetName() {
        return this.charactersCharsetName;
    }

    @Override
    public ResponseRequestContextAddon setCharactersCharsetName(String charactersCharsetName) {

        Objects.requireNonNull(charactersCharsetName, "charactersCharsetName can't be NULL");

        if (isHeadersSent() && !charactersCharsetName.equalsIgnoreCase(getCharactersCharsetName())) {
            logger.warn("Some data have already been send, it may not be a good idea to change the Charset now.");
        }
        this.charactersCharsetName = charactersCharsetName;

        return this;
    }

    @Override
    public void sendPlainText(String string) {
        sendPlainText(string, false);
    }

    @Override
    public void sendPlainText(String string, boolean flush) {
        sendCharacters(string, ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendHtml(String html) {
        sendHtml(html, false);
    }

    @Override
    public void sendHtml(String string, boolean flush) {
        sendCharacters(string, ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendParseHtml(String html) {
        sendParse(html, ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset(), false);
    }

    @Override
    public void sendParseHtml(String html, boolean flush) {
        sendParse(html, ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendParse(String content, String contentType) {
        sendParse(content, contentType, false);
    }

    @Override
    public void sendParse(String content, String contentType, boolean flush) {

        if (StringUtils.isBlank(contentType)) {
            logger.warn("The Content-Type was not specified : 'text/html' will be used");
            contentType = ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset();
        }

        if (content == null) {
            content = "";
        }

        String evaluated = getRequestContext().templating().evaluate(content, getModel());
        sendCharacters(evaluated, contentType, flush);
    }

    @Override
    public void sendTemplateHtml(String templatePath) {
        sendTemplateHtml(templatePath, true, false);
    }

    @Override
    public void sendTemplateHtml(String templatePath, boolean isClasspathPath) {
        sendTemplateHtml(templatePath, isClasspathPath, false);
    }

    @Override
    public void sendTemplateHtml(String templatePath, boolean isClasspathPath, boolean flush) {
        sendTemplate(templatePath, isClasspathPath, ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendTemplate(String templatePath, String contentType) {
        sendTemplate(templatePath, true, contentType, false);
    }

    @Override
    public void sendTemplate(String templatePath, String contentType, boolean flush) {
        sendTemplate(templatePath, true, contentType, flush);
    }

    @Override
    public void sendTemplate(String templatePath, boolean isClasspathPath, String contentType) {
        sendTemplate(templatePath, isClasspathPath, contentType, false);
    }

    @Override
    public void sendTemplate(String templatePath, boolean isClasspathPath, String contentType, boolean flush) {

        if (StringUtils.isBlank(contentType)) {

            contentType = getSpincastUtils().getMimeTypeFromPath(templatePath);
            if (contentType == null) {
                logger.warn("The Content-Type was not specified and can't be determined from the template path '" +
                            templatePath +
                            "': 'text/html' will be used");
                contentType = ContentTypeDefaults.HTML.getMainVariationWithUtf8Charset();
            }
        }

        String evaluated = getRequestContext().templating().fromTemplate(templatePath, isClasspathPath, getModel());
        sendCharacters(evaluated, contentType, flush);
    }

    @Override
    public void sendJson() {
        sendJson(false);
    }

    @Override
    public void sendJson(boolean flush) {

        addAlertsToModel();

        String json = getJsonManager().toJsonString(getModel());
        sendCharacters(json, ContentTypeDefaults.JSON.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendJson(String jsonString) {
        sendJson(jsonString, false);
    }

    @Override
    public void sendJson(String jsonString, boolean flush) {
        sendCharacters(jsonString, ContentTypeDefaults.JSON.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendJson(Object obj) {
        sendJson(obj, false);
    }

    @Override
    public void sendJson(Object obj, boolean flush) {

        if (obj instanceof String) {
            sendJson((String)obj);
            return;
        }

        String json = getJsonManager().toJsonString(obj);
        sendCharacters(json, ContentTypeDefaults.JSON.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendXml() {
        sendXml(false);
    }

    @Override
    public void sendXml(boolean flush) {

        addAlertsToModel();

        String xml = getXmlManager().toXml(getModel());
        sendCharacters(xml, ContentTypeDefaults.XML.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendXml(String xml) {
        sendXml(xml, false);
    }

    @Override
    public void sendXml(String xml, boolean flush) {
        sendCharacters(xml, ContentTypeDefaults.XML.getMainVariationWithUtf8Charset(), flush);
    }

    @Override
    public void sendXml(Object obj) {
        sendXml(obj, false);
    }

    @Override
    public void sendXml(Object obj, boolean flush) {

        if (obj instanceof String) {
            sendXml((String)obj);
            return;
        }

        String xml = getXmlManager().toXml(obj);
        sendCharacters(xml, ContentTypeDefaults.XML.getMainVariationWithUtf8Charset(), flush);
    }

    protected void addAlertsToModel() {

        if (isAddAlertsToModel()) {

            Map map = getRequestContext().templating().getSpincastReservedMap();

            @SuppressWarnings("unchecked")
            List alerts =
                    (List)map.get(SpincastConstants.TemplatingGlobalVariables.DEFAULT_GLOBAL_TEMPLATING_VAR_KEY_ALERTS);
            if (alerts != null && alerts.size() > 0) {

                JsonObject model = getModel();

                String spincastModelObjKey = getSpincastConfig().getSpincastModelRootVariableName();
                JsonObject spincastModelObj = model.getJsonObjectOrEmpty(spincastModelObjKey);
                model.set(spincastModelObjKey, spincastModelObj);

                JsonArray alertsArray =
                        spincastModelObj.getJsonArrayOrEmpty(ResponseModelVariables.DEFAULT_RESPONSE_MODEL_VAR_ALERTS);
                spincastModelObj.set(ResponseModelVariables.DEFAULT_RESPONSE_MODEL_VAR_ALERTS, alertsArray);

                for (Alert alert : alerts) {
                    alertsArray.add(alert);
                }
            }
        }
    }

    /**
     * Should Alert messages (and therefore Flash message)
     * be added to the model when sending this one as Json or XML?
     */
    protected boolean isAddAlertsToModel() {
        return true;
    }

    @Override
    public ResponseRequestContextAddon resetBuffer() {

        try {
            getBuffer().reset();

            if (!isHeadersSent()) {
                this.isResponseCharactersBased = IS_RESPONSE_CHARACTERS_BASED_BY_DEFAULT;
            }
        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }

        return this;
    }

    @Override
    public ResponseRequestContextAddon resetEverything() {
        return resetEverything(true);
    }

    @Override
    public ResponseRequestContextAddon resetEverything(boolean resetCookies) {

        resetBuffer();

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, the cookies, headers and status code won't be reset...");
        } else {
            if (resetCookies) {
                getCookiesAdded().clear();
            }

            getHeaders().clear();
            setContentType(null);
            setStatusCode(HttpStatus.SC_OK);
        }

        return this;
    }

    @Override
    public byte[] getUnsentBytes() {
        return getBuffer().toByteArray();
    }

    @Override
    public String getUnsentCharacters() {
        return getUnsentCharacters("UTF-8");
    }

    @Override
    public String getUnsentCharacters(String encoding) {
        try {
            byte[] unsentBytes = getUnsentBytes();
            return new String(unsentBytes, encoding);
        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    @Override
    public ResponseRequestContextAddon removeHeader(String name) {

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, can't change them...");
            return this;
        }

        getHeaders().remove(name);

        return this;
    }

    @Override
    public ResponseRequestContextAddon setHeader(String name, String value) {

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, can't change them...");
            return this;
        }

        if (value == null) {
            removeHeader(name);
            return this;
        }
        setHeader(name, Arrays.asList(value));

        return this;
    }

    @Override
    public ResponseRequestContextAddon setHeader(String name, List values) {

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, can't change them...");
            return this;
        }

        if (values == null) {
            removeHeader(name);
            return this;
        }
        getHeaders().put(name, values);

        return this;
    }

    @Override
    public ResponseRequestContextAddon addHeaderValue(String name, String value) {

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, can't change them...");
            return this;
        }

        if (value == null) {
            return this;
        }
        addHeaderValues(name, Arrays.asList(value));

        return this;
    }

    @Override
    public ResponseRequestContextAddon addHeaderValues(String name, List values) {

        if (isHeadersSent()) {
            logger.warn("Response headers are already sent, can't change them...");
            return this;
        }

        if (values == null) {
            return this;
        }
        List currentValues = getHeaders().get(name);
        if (currentValues == null) {
            currentValues = new ArrayList();
            getHeaders().put(name, currentValues);
        }
        currentValues.addAll(values);

        return this;
    }

    @Override
    public Map> getHeaders() {
        if (this.headers == null) {
            Map> headersFromServer = getServer().getResponseHeaders(getExchange());

            // We use a TreeMap with String.CASE_INSENSITIVE_ORDER so the
            // "get" is case insensitive!
            TreeMap> treeMap = new TreeMap>(String.CASE_INSENSITIVE_ORDER);
            if (headersFromServer != null) {
                treeMap.putAll(headersFromServer);
            }

            this.headers = treeMap;
        }
        return this.headers;
    }

    @Override
    public List getHeader(String name) {
        if (StringUtils.isBlank(name)) {
            return new LinkedList();
        }
        // This get is case insensitive.
        List values = getHeaders().get(name);
        if (values == null) {
            values = new LinkedList();
        }
        return values;
    }

    @Override
    public String getHeaderFirst(String name) {
        List values = getHeader(name);
        if (values != null && values.size() > 0) {
            return values.get(0);
        }
        return null;
    }

    @Override
    public boolean isHeadersSent() {
        return getServer().isResponseHeadersSent(getExchange());
    }

    protected void setIsShouldGzip(boolean isShouldGzip) {

        GzipOption gzipOption = getGzipOption();
        if (gzipOption != GzipOption.DEFAULT) {
            logger.warn("Can't turn on/off the gzip feature since the GzipOption is " + gzipOption);
            return;
        }

        try {
            if (isHeadersSent()) {
                if (this.isShouldGzip != null && this.isShouldGzip.getBoolean() != isShouldGzip) {
                    logger.warn("Can't turn on/off the gzip feature since headers are already sent.");
                }
                return;
            }

            //==========================================
            // If we turn off the gziping, we have to
            // change the buffer.
            //==========================================
            if (this.isShouldGzip != null && this.isShouldGzip.getBoolean() && !isShouldGzip) {

                this.byteArrayOutputStreamIn.reset();

                getGzipBuffer().close();
                ByteArrayOutputStream buffer = getBuffer();
                if (buffer.size() > 0) {
                    GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(buffer.toByteArray()));
                    byte[] ungzipedBytes = IOUtils.toByteArray(gzipInputStream);
                    this.byteArrayOutputStreamIn.write(ungzipedBytes);
                }
                this.gzipOutputStream = null;
            }

            this.isShouldGzip = Bool.from(isShouldGzip);
        } catch (Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    protected boolean isShouldGzip() {

        //==========================================
        // The GzipOption has priority.
        //==========================================
        GzipOption gzipOption = getGzipOption();
        if (gzipOption == GzipOption.FORCE) {
            return true;
        } else if (gzipOption == GzipOption.DISABLE) {
            return false;
        } else if (gzipOption != GzipOption.DEFAULT) {
            throw new RuntimeException("Unimplemented : " + gzipOption);
        }

        if (this.isShouldGzip == null) {

            //==========================================
            // Check if there is a gzip 'Accept-Encoding' header
            //==========================================
            boolean hasGzipAcceptHeader = false;
            List acceptEncodings = getRequestContext().request().getHeader(HttpHeaders.ACCEPT_ENCODING);
            if (acceptEncodings != null) {
                for (String acceptEncoding : acceptEncodings) {
                    if (acceptEncoding == null) {
                        continue;
                    }
                    if (acceptEncoding.contains("gzip")) {
                        hasGzipAcceptHeader = true;
                        break;
                    }
                }
            }

            String responseContentType = getContentType();

            //==========================================
            // Try to guess the content-type
            //==========================================
            if (responseContentType == null) {
                String path = getRequestContext().request().getRequestPath();
                responseContentType = getSpincastUtils().getMimeTypeFromPath(path);
            }

            //==========================================
            // Check if its a content-type for which we
            // shouldn't use gzip.
            //==========================================
            if (responseContentType != null) {
                if (!getSpincastUtils().isContentTypeToSkipGziping(responseContentType)) {
                    if (!hasGzipAcceptHeader) {
                        logger.debug("No gzip 'Accept-Encoding' header, we won't gzip the response.");
                        setIsShouldGzip(false);
                    } else {
                        setIsShouldGzip(true);
                    }
                }
            } else {
                setIsShouldGzip(false);
            }
        }

        if (this.isShouldGzip == null) {
            return false;
        }
        return this.isShouldGzip.getBoolean();
    }

    @Override
    public void end() {
        flush(true);
    }

    @Override
    public void flush() {
        flush(false);
    }

    @Override
    public void flush(boolean close) {

        try {
            if (isClosed()) {
                return;
            }

            //==========================================
            // If we haven't read the request body yet, the
            // request size may not have been checked. So we
            // read it now to be able to send a
            // "413 - Request entity too large" status if
            // required.
            //==========================================
            if (!isRequestSizeValidated() && !isHeadersSent()) {
                setRequestSizeValidated(true);
                boolean requestSizeOk = getServer().forceRequestSizeValidation(getExchange());
                if (!requestSizeOk) {
                    resetEverything();
                    setStatusCode(HttpStatus.SC_REQUEST_TOO_LONG);
                }
            }

            //==========================================
            // Are there any extra resources to push (HTTP/2)?
            //==========================================
            if (getResourcesToPush().size() > 0) {
                getServer().push(getExchange(), getResourcesToPush());
            }

            ByteArrayOutputStream buffer = getBuffer();

            //==========================================
            // Send the Headers!
            //==========================================
            if (!isHeadersSent()) {

                getServer().setResponseStatusCode(getExchange(), getStatusCode());

                String responseContentType = getContentType();
                if (responseContentType == null) {

                    String mimeType = getSpincastUtils().getMimeTypeFromPath(getRequestContext().request()
                                                                                                .getRequestPath());
                    if (mimeType != null) {
                        responseContentType = mimeType;
                    } else {
                        if (isResponseCharactersBased() || buffer.size() == 0) {
                            responseContentType = ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset();
                        } else {
                            responseContentType = ContentTypeDefaults.BINARY.getMainVariation();
                        }
                    }
                    setContentType(responseContentType);
                }

                setHeader(HttpHeaders.CONTENT_TYPE, responseContentType);

                //==========================================
                // Status code
                //==========================================
                getServer().setResponseStatusCode(getExchange(), getStatusCode());

                //==========================================
                // Nothing sent yet and we end the response, we can
                // send a Content-Length header!
                //==========================================
                if (close && !isShouldGzip()) {
                    setHeader(HttpHeaders.CONTENT_LENGTH, "" + buffer.size());
                }

                //==========================================
                // Add Cookies
                //==========================================

                //==========================================
                // Do we add the cookies validator?
                //==========================================
                if (getSpincastConfig().isEnableCookiesValidator()) {
                    setCookie(createCookiesValidatorCookie());
                }
                getServer().addCookies(getExchange(), getCookiesAdded());

                //==========================================
                // Gzip headers
                //==========================================
                if (isShouldGzip()) {
                    setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
                    setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }

                //==========================================
                // Add the headers to the server
                //==========================================
                getServer().setResponseHeaders(getExchange(), getHeaders());
            }

            byte[] bytesToFlush;
            if (isShouldGzip()) {

                getGzipBuffer().write(buffer.toByteArray());

                //==========================================
                // Required for the bytes to reach the underlying
                // out.
                //==========================================
                getGzipBuffer().flush();

                //==========================================
                // We must close the GZIPOutputStream at the end
                // so the correct gzip "footer" is sent...
                //==========================================
                if (close) {
                    getGzipBuffer().close();
                }

                bytesToFlush = getOut().toByteArray();
                getOut().reset();

            } else {
                bytesToFlush = buffer.toByteArray();
            }
            buffer.reset();

            getServer().flushBytes(getExchange(), bytesToFlush, close);

        } catch (Exception ex) {
            logger.error("Error with request " + getRequestContext().request().getFullUrl());
            throw SpincastStatics.runtimize(ex);
        }
    }

    private Cookie createCookiesValidatorCookie() {

        Cookie cookie = createCookie(getSpincastConfig().getCookiesValidatorCookieName());
        cookie.setValue("1");
        cookie.setExpiresUsingMaxAge(60 * 60 * 24 * 365);

        return cookie;
    }

    @Override
    public ResponseRequestContextAddon setCacheSeconds(int cacheSeconds) {
        return setCacheSeconds(cacheSeconds, false);
    }

    @Override
    public ResponseRequestContextAddon setCacheSeconds(int cacheSeconds, boolean isPrivateCache) {

        if (cacheSeconds <= 0) {
            logger.warn("A number of seconds below 1 doesn't send any cache headers: " + cacheSeconds);
            return this;
        }

        if (isHeadersSent()) {
            logger.error("The headers are sent, you can't add cache headers.");
            return this;
        }

        String cacheControl = "max-age=" + cacheSeconds;
        if (isPrivateCache) {
            cacheControl = "private, " + cacheControl;
        } else {
            cacheControl = "public, " + cacheControl;
        }

        setHeader(HttpHeaders.CACHE_CONTROL, cacheControl);

        return this;
    }

    @Override
    public void setModel(JsonObject model) {
        this.responseModel = model;
    }

    @Override
    public void addAlert(AlertLevel alertType, String alertText) {

        Map spincastMap = getRequestContext().templating().getSpincastReservedMap();

        @SuppressWarnings("unchecked")
        List alerts =
                (List)spincastMap.get(SpincastConstants.TemplatingGlobalVariables.DEFAULT_GLOBAL_TEMPLATING_VAR_KEY_ALERTS);
        if (alerts == null) {
            alerts = new ArrayList();
            spincastMap.put(SpincastConstants.TemplatingGlobalVariables.DEFAULT_GLOBAL_TEMPLATING_VAR_KEY_ALERTS,
                            alerts);
        }

        alerts.add(createAlert(alertType, alertText));
    }

    protected Alert createAlert(AlertLevel alertType, String alertText) {
        return new AlertDefault(alertType, alertText);
    }

    @Override
    public Map getCookiesAdded() {
        if (this.cookies == null) {
            this.cookies = new HashMap();
        }
        return this.cookies;
    }

    @Override
    public Cookie getCookieAdded(String name) {
        return getCookiesAdded().get(name);
    }

    @Override
    public void setCookie(Cookie cookie) {

        boolean valid = validateCookie(cookie);
        if (!valid) {
            return;
        }

        getCookiesAdded().put(cookie.getName(), cookie);
    }

    @Override
    public void setCookieSession(String name, String value) {
        Cookie cookie = getCookieFactory().createCookie(name, value);
        setCookie(cookie);
    }

    @Override
    public void setCookieSessionSafe(String name, String value) {
        addCookieSafe(name, value, null);
    }

    @Override
    public void setCookie1year(String name, String value) {
        setCookie(name, value, 31536000);
    }

    @Override
    public void setCookie1yearSafe(String name, String value) {
        addCookieSafe(name, value, 31536000);
    }

    @Override
    public void setCookie10years(String name, String value) {
        setCookie(name, value, 315360000);
    }

    @Override
    public void setCookie10yearsSafe(String name, String value) {
        addCookieSafe(name, value, 315360000);
    }

    @Override
    public void setCookie(String name, String value, int nbrSecondsToLive) {
        Cookie cookie = getCookieFactory().createCookie(name, value);
        cookie.setExpiresUsingMaxAge(nbrSecondsToLive);
        setCookie(cookie);
    }

    @Override
    public void setCookie(String name, String value, int nbrSecondsToLive, boolean httpOnly) {
        Cookie cookie = getCookieFactory().createCookie(name, value);
        cookie.setExpiresUsingMaxAge(nbrSecondsToLive);
        cookie.setHttpOnly(httpOnly);
        setCookie(cookie);
    }

    protected void addCookieSafe(String name, String value, Integer nbrSecondsToLive) {
        Cookie cookie = getCookieFactory().createCookie(name, value);
        if (nbrSecondsToLive != null) {
            cookie.setExpiresUsingMaxAge(nbrSecondsToLive);
        }
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setSameSite(CookieSameSite.LAX);
        setCookie(cookie);
    }

    @Override
    public void setCookie(String name,
                          String value,
                          String path,
                          String domain,
                          Date expires,
                          boolean secure,
                          boolean httpOnly,
                          CookieSameSite cookieSameSite,
                          boolean discard,
                          int version) {
        Cookie cookie = getCookieFactory().createCookie(name,
                                                        value,
                                                        path,
                                                        domain,
                                                        expires,
                                                        secure,
                                                        httpOnly,
                                                        cookieSameSite,
                                                        discard,
                                                        version);
        setCookie(cookie);
    }

    protected boolean validateCookie(Cookie cookie) {
        Objects.requireNonNull(cookie, "Can't add a NULL cookie");

        String name = cookie.getName();
        if (StringUtils.isBlank(name)) {
            throw new RuntimeException("A cookie can't have an empty name");
        }
        return true;
    }

    @Override
    public void deleteCookie(String name) {
        if (name == null) {
            return;
        }

        Cookie cookie = createCookie(name);
        cookie.setExpires(DateUtils.addYears(new Date(), -1));
        setCookie(cookie);
    }

    @Override
    public void deleteAllCookiesUserHas() {

        getCookiesAdded().clear();

        for (String cookieName : getRequestContext().request().getCookiesValues().keySet()) {
            deleteCookie(cookieName);
        }
    }

    @Override
    public Cookie createCookie(String name) {
        return getCookieFactory().createCookie(name);
    }

    @Override
    public void addForm(Form form) {
        addForm(form, getSpincastConfig().getValidationElementDefaultName());
    }

    @Override
    public void addForm(Form form, String validationElementName) {
        getModel().set(form.getFormName(), form);

        Object validationElementObj = getModel().getObject(validationElementName);
        if (validationElementObj == null) {
            validationElementObj = getJsonManager().create();
            getModel().set(validationElementName, validationElementObj);
        } else if (!(validationElementObj instanceof JsonObject)) {
            throw new RuntimeException("The '" + validationElementName + "' element already exists on the response's model " +
                                       "but is not a JsonObject! It can't be used as the validation element : " +
                                       validationElementObj);
        }
        JsonObject validationElement = (JsonObject)validationElementObj;

        //==========================================
        // Tells the form to use that validation element
        // to strore validation messages.
        //==========================================
        form.setValidationObject(validationElement);
    }

    public Set getResourcesToPush() {
        if (this.resourcesToPush == null) {
            this.resourcesToPush = new HashSet();
        }
        return this.resourcesToPush;
    }

    @Override
    public ResponseRequestContextAddon push(HttpMethod httpMethod, String path, Map> requestHeaders) {
        Objects.requireNonNull(httpMethod, "The httpMethod can't be NULL");
        if (StringUtils.isBlank(path)) {
            throw new RuntimeException("The path to the resource to push can't be empty.");
        }

        path = path.startsWith("/") ? path : "/" + path;
        path = tweakResourceToPushPath(path);

        getResourcesToPush().add(new ResourceToPush(httpMethod, path, requestHeaders));

        return this;
    }

    protected String tweakResourceToPushPath(String path) {

        //==========================================
        // Replace any cache buster placeholders.
        //==========================================
        path = path.replace(RESOURCE_TO_PUSH_PLACEHOLDERS_CACHE_BUSTER, getSpincastUtils().getCacheBusterCode());

        return path;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy