/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 flex.messaging.endpoints;

import flex.management.runtime.messaging.endpoints.EndpointControl;
import flex.messaging.FlexContext;
import flex.messaging.FlexSession;
import flex.messaging.HttpFlexSession;
import flex.messaging.MessageClient;
import flex.messaging.client.FlexClient;
import flex.messaging.config.ConfigMap;
import flex.messaging.config.ConfigurationConstants;
import flex.messaging.endpoints.amf.AMFFilter;
import flex.messaging.io.MessageIOConstants;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.log.HTTPRequestLog;
import flex.messaging.messages.CommandMessage;
import flex.messaging.messages.Message;
import flex.messaging.util.SettingsReplaceUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Abstract base class for all the HTTP-based endpoints.
 */
public abstract class BaseHTTPEndpoint extends AbstractEndpoint {
    //--------------------------------------------------------------------------
    //
    // Public Static Constants
    //
    //--------------------------------------------------------------------------

    /**
     * The secure and insecure URL schemes for the HTTP endpoint.
     */
    public static final String HTTP_PROTOCOL_SCHEME = "http";
    public static final String HTTPS_PROTOCOL_SCHEME = "https";

    //--------------------------------------------------------------------------
    //
    // Private Static Constants
    //
    //--------------------------------------------------------------------------

    private static final String ADD_NO_CACHE_HEADERS = "add-no-cache-headers";
    private static final String REDIRECT_URL = "redirect-url";
    private static final String INVALIDATE_SESSION_ON_DISCONNECT = "invalidate-session-on-disconnect";
    private static final String HTTP_RESPONSE_HEADERS = "http-response-headers";
    private static final String HEADER_ATTR = "header";

    private static final String HEADER_NAME_ORIGIN = "Origin";
    private static final String ACCESS_CONTROL = "Access-Control-";
    private static final String SESSION_REWRITING_ENABLED = "session-rewriting-enabled";

    private static final int ERR_MSG_DUPLICATE_SESSIONS_DETECTED = 10035;
    private static final String REQUEST_ATTR_DUPLICATE_SESSION_FLAG = "flex.messaging.request.DuplicateSessionDetected";

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * Constructs an unmanaged <code>BaseHTTPEndpoint</code>.
     */
    public BaseHTTPEndpoint() {
        this(false);
    }

    /**
     * Constructs a <code>BaseHTTPEndpoint</code> with the specified management setting.
     *
     * @param enableManagement <code>true</code> if the <code>BaseHTTPEndpoint</code>
     *                         is manageable; otherwise <code>false</code>.
     */
    public BaseHTTPEndpoint(boolean enableManagement) {
        super(enableManagement);
    }

    //--------------------------------------------------------------------------
    //
    // Initialize, validate, start, and stop methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Initializes the <code>Endpoint</code> with the properties.
     * If subclasses override this method, they must call <code>super.initialize()</code>.
     *
     * @param id         The ID of the <code>Endpoint</code>.
     * @param properties Properties for the <code>Endpoint</code>.
     */
    @Override
    public void initialize(String id, ConfigMap properties) {
        super.initialize(id, properties);

        if (properties == null || properties.size() == 0)
            return;

        // General HTTP props.
        addNoCacheHeaders = properties.getPropertyAsBoolean(ADD_NO_CACHE_HEADERS, true);
        redirectURL = properties.getPropertyAsString(REDIRECT_URL, null);
        invalidateSessionOnDisconnect = properties.getPropertyAsBoolean(INVALIDATE_SESSION_ON_DISCONNECT, false);
        loginAfterDisconnect = properties.getPropertyAsBoolean(ConfigurationConstants.LOGIN_AFTER_DISCONNECT_ELEMENT, false);
        sessionRewritingEnabled = properties.getPropertyAsBoolean(SESSION_REWRITING_ENABLED, true);
        initializeHttpResponseHeaders(properties);
        validateEndpointProtocol();
    }

    /**
     * Starts the <code>Endpoint</code> by creating a filter chain and setting
     * up serializers and deserializers.
     */
    @Override
    public void start() {
        if (isStarted())
            return;

        super.start();

        filterChain = createFilterChain();
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     * Controller used to manage this endpoint.
     */
    protected EndpointControl controller;

    /**
     * AMF processing filter chain used by this endpoint.
     */
    protected AMFFilter filterChain;

    /**
     * Headers to add to the HTTP response.
     */
    protected List<HttpHeader> httpResponseHeaders;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  addNoCacheHeaders
    //----------------------------------

    protected boolean addNoCacheHeaders = true;

    /**
     * Retrieves the <code>add-no-cache-headers</code> property.
     *
     * @return <code>true</code> if <code>add-no-cache-headers</code> is enabled;
     * <code>false</code> otherwise.
     */
    public boolean isAddNoCacheHeaders() {
        return addNoCacheHeaders;
    }

    /**
     * Sets the <code>add-no-cache-headers</code> property.
     *
     * @param addNoCacheHeaders The <code>add-no-cache-headers</code> property.
     */
    public void setAddNoCacheHeaders(boolean addNoCacheHeaders) {
        this.addNoCacheHeaders = addNoCacheHeaders;
    }

    //----------------------------------
    //  loginAfterDisconnect
    //----------------------------------

    /**
     * This is a property used on the client.
     */
    protected boolean loginAfterDisconnect;

    //----------------------------------
    //  invalidateSessionOnDisconnect
    //----------------------------------

    protected boolean invalidateSessionOnDisconnect;

    /**
     * Indicates whether the server session will be invalidated
     * when a client channel disconnects.
     * The default is <code>false</code>.
     *
     * @return <code>true</code> if the server session will be invalidated
     * when a client channel disconnects, <code>false</code> otherwise.
     */
    public boolean isInvalidateSessionOnDisconnect() {
        return invalidateSessionOnDisconnect;
    }

    /**
     * Determines whether to invalidate the server session for a client
     * that disconnects its channel.
     * The default is <code>false</code>.
     *
     * @param value <code>true</code> to invalidate the server session for a client
     *              that disconnects its channel, <code>false</code> otherwise.
     */
    public void setInvalidateSessionOnDisconnect(boolean value) {
        invalidateSessionOnDisconnect = value;
    }

    //----------------------------------
    //  redirectURL
    //----------------------------------

    protected String redirectURL;

    /**
     * Retrieves the <code>redirect-url</code> property.
     *
     * @return The <code>redirect-url</code> property.
     */
    public String getRedirectURL() {
        return redirectURL;
    }

    /**
     * Sets the <code>redirect-url</code> property.
     *
     * @param redirectURL The <code>redirect-url</code> property.
     */
    public void setRedirectURL(String redirectURL) {
        this.redirectURL = redirectURL;
    }

    //----------------------------------
    //  sessionRewritingEnabled
    //----------------------------------

    protected boolean sessionRewritingEnabled = true;

    /**
     * Indicates whether the server will fall back on rewriting URLs to include
     * session identifiers in the URL when HTTP session cookies are not allowed
     * on the client. The default is <code>true</code>.
     *
     * @return <code>true</code> if the session rewriting is enabled.
     */
    public boolean isSessionRewritingEnabled() {
        return sessionRewritingEnabled;
    }

    /**
     * Sets whether the session rewriting is enabled.
     *
     * @param value The session writing enabled value.
     */
    public void setSessionRewritingEnabled(boolean value) {
        sessionRewritingEnabled = value;
    }

    //--------------------------------------------------------------------------
    //
    // Public Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Handle AMF/AMFX encoded messages sent over HTTP.
     *
     * @param req The original servlet request.
     * @param res The active servlet response.
     */
    @Override
    public void service(HttpServletRequest req, HttpServletResponse res) {
        super.service(req, res);

        try {
            // Setup serialization and type marshalling contexts
            setThreadLocals();

            // Create a context for this request
            ActionContext context = new ActionContext();

            // Pass endpoint's mpi settings to the context so that it knows what level of
            // performance metrics should be gathered during serialization/deserialization
            context.setRecordMessageSizes(isRecordMessageSizes());
            context.setRecordMessageTimes(isRecordMessageTimes());

            // Send invocation through filter chain, which ends at the MessageBroker
            filterChain.invoke(context);

            // After serialization completes, increment endpoint byte counters,
            // if the endpoint is managed
            if (isManaged()) {
                controller.addToBytesDeserialized(context.getDeserializedBytes());
                controller.addToBytesSerialized(context.getSerializedBytes());
            }

            if (context.getStatus() != MessageIOConstants.STATUS_NOTAMF) {
                if (addNoCacheHeaders)
                    addNoCacheHeaders(req, res);

                addHeadersToResponse(req, res);

                ByteArrayOutputStream outBuffer = context.getResponseOutput();

                res.setContentType(getResponseContentType());

                res.setContentLength(outBuffer.size());
                outBuffer.writeTo(res.getOutputStream());
                res.flushBuffer();
            } else {
                // Not an AMF request, probably viewed in a browser
                if (redirectURL != null) {
                    try {
                        //Check for redirect URL context-root token
                        redirectURL = SettingsReplaceUtil.replaceContextPath(redirectURL, req.getContextPath());
                        res.sendRedirect(redirectURL);
                    } catch (IllegalStateException alreadyFlushed) {
                        // ignore
                    }
                }
            }
        } catch (IOException ioe) {
            // This happens when client closes the connection, log it at info level
            log.info(ioe.getMessage());
            // Store exception information for latter logging
            req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, ioe.toString());
        } catch (Throwable t) {
            log.error(t.getMessage(), t);
            // Store exception information for latter logging
            req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, t.toString());
        } finally {
            clearThreadLocals();
        }
    }


    /**
     * Returns a <code>ConfigMap</code> of endpoint properties that the client
     * needs. This includes properties from <code>super.describeEndpoint</code>
     * and additional <code>BaseHTTPEndpoint</code> specific properties under
     * "properties" key.
     */
    @Override
    public ConfigMap describeEndpoint() {
        ConfigMap endpointConfig = super.describeEndpoint();

        if (loginAfterDisconnect) {
            ConfigMap loginAfterDisconnect = new ConfigMap();
            // Adding as a value rather than attribute to the parent
            loginAfterDisconnect.addProperty(EMPTY_STRING, TRUE_STRING);

            ConfigMap properties = endpointConfig.getPropertyAsMap(PROPERTIES_ELEMENT, null);
            if (properties == null) {
                properties = new ConfigMap();
                endpointConfig.addProperty(PROPERTIES_ELEMENT, properties);
            }
            properties.addProperty(ConfigurationConstants.LOGIN_AFTER_DISCONNECT_ELEMENT, loginAfterDisconnect);
        }

        return endpointConfig;
    }

    /**
     * Overrides to guard against duplicate HTTP-based sessions for the same FlexClient
     * which will occur if the remote host has disabled session cookies.
     *
     * @see AbstractEndpoint#setupFlexClient(String)
     */
    @Override
    public FlexClient setupFlexClient(String id) {
        FlexClient flexClient = super.setupFlexClient(id);

        // Scan for duplicate HTTP-sessions and if found, invalidate them and throw a MessageException.
        // A request attribute is used to deal with batched AMF messages that arrive in a single request by trigger multiple passes through this method.
        boolean duplicateSessionDetected = (FlexContext.getHttpRequest().getAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG) != null);

        List<FlexSession> sessions = null;
        if (!duplicateSessionDetected) {
            sessions = flexClient.getFlexSessions();
            int n = sessions.size();
            if (n > 1) {
                List<HttpFlexSession> httpFlexSessions = new ArrayList<HttpFlexSession>();
                for (int i = 0; i < n; i++) {
                    FlexSession currentSession = sessions.get(i);
                    if (currentSession instanceof HttpFlexSession)
                        httpFlexSessions.add((HttpFlexSession) currentSession);
                    if (httpFlexSessions.size() > 1) {
                        FlexContext.getHttpRequest().setAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG, httpFlexSessions);
                        duplicateSessionDetected = true;
                        break;
                    }
                }
            }
        }

        // If more than one was found, remote host isn't using session cookies. Kill all duplicate sessions and return an error.
        // Simplest to just re-scan the list given that it will be very short, but use an iterator for concurrent modification.
        if (duplicateSessionDetected) {
            Object attributeValue = FlexContext.getHttpRequest().getAttribute(REQUEST_ATTR_DUPLICATE_SESSION_FLAG);
            String newSessionId = null;
            String oldSessionId = null;
            if (attributeValue != null) {
                @SuppressWarnings("unchecked")
                List<HttpFlexSession> httpFlexSessions = (List<HttpFlexSession>) attributeValue;
                oldSessionId = httpFlexSessions.get(0).getId();
                newSessionId = httpFlexSessions.get(1).getId();
            }

            if (sessions != null) {
                for (FlexSession session : sessions) {
                    if (session instanceof HttpFlexSession) {
                        session.invalidate();
                    }
                }
            }

            // Return an error to the client.

            DuplicateSessionException e = new DuplicateSessionException();
            // Duplicate HTTP-based FlexSession error: A request for FlexClient ''{0}'' arrived over a new FlexSession ''{1}'', but FlexClient is already associated with FlexSession ''{2}'', therefore it cannot be associated with the new session.
            e.setMessage(ERR_MSG_DUPLICATE_SESSIONS_DETECTED, new Object[]{flexClient.getId(), newSessionId, oldSessionId});
            throw e;
        }

        return flexClient;
    }

    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Adds custom headers specified in the config to the HTTP response. The only
     * exception is that access control headers (Access-Control-*) are sent only
     * if there is an Origin header in the request.
     *
     * @param request  The HTTP request.
     * @param response The HTTP response.
     */
    protected void addHeadersToResponse(HttpServletRequest request, HttpServletResponse response) {
        if (httpResponseHeaders == null || httpResponseHeaders.isEmpty())
            return;

        String origin = request.getHeader(HEADER_NAME_ORIGIN);
        boolean originHeaderExists = origin != null && origin.length() != 0;

        for (HttpHeader header : httpResponseHeaders) {
            if (header.name.startsWith(ACCESS_CONTROL) && !originHeaderExists)
                continue;

            response.addHeader(header.name, header.value);
        }
    }

    /**
     * Create the gateway filters that transform action requests
     * and responses.
     */
    protected abstract AMFFilter createFilterChain();

    /**
     * Returns the content type used by the connection handler to set on the
     * HTTP response. Subclasses should either return MessageIOConstants.AMF_CONTENT_TYPE
     * or MessageIOConstants.XML_CONTENT_TYPE.
     */
    protected abstract String getResponseContentType();

    /**
     * Returns https which is the secure protocol scheme for the endpoint.
     *
     * @return https.
     */
    @Override
    protected String getSecureProtocolScheme() {
        return HTTPS_PROTOCOL_SCHEME;
    }

    /**
     * Returns http which is the insecure protocol scheme for the endpoint.
     *
     * @return http.
     */
    @Override
    protected String getInsecureProtocolScheme() {
        return HTTP_PROTOCOL_SCHEME;
    }

    /**
     * @see flex.messaging.endpoints.AbstractEndpoint#handleChannelDisconnect(CommandMessage)
     */
    @Override
    protected Message handleChannelDisconnect(CommandMessage disconnectCommand) {
        HttpFlexSession session = (HttpFlexSession) FlexContext.getFlexSession();
        FlexClient flexClient = FlexContext.getFlexClient();

        // Shut down any subscriptions established over this channel/endpoint
        // for this specific FlexClient.
        if (flexClient.isValid()) {
            String endpointId = getId();
            List<MessageClient> messageClients = flexClient.getMessageClients();
            for (MessageClient messageClient : messageClients) {
                if (messageClient.getEndpointId().equals(endpointId)) {
                    messageClient.setClientChannelDisconnected(true);
                    messageClient.invalidate();
                }
            }
        }

        // And optionally invalidate the session.
        if (session.isValid() && isInvalidateSessionOnDisconnect())
            session.invalidate(false /* don't recreate */);

        return super.handleChannelDisconnect(disconnectCommand);
    }

    protected void initializeHttpResponseHeaders(ConfigMap properties) {
        if (!properties.containsKey(HTTP_RESPONSE_HEADERS))
            return;

        ConfigMap httpResponseHeaders = properties.getPropertyAsMap(HTTP_RESPONSE_HEADERS, null);
        if (httpResponseHeaders == null)
            return;

        @SuppressWarnings("unchecked")
        List<String> headers = httpResponseHeaders.getPropertyAsList(HEADER_ATTR, null);
        if (headers == null || headers.isEmpty())
            return;

        if (this.httpResponseHeaders == null)
            this.httpResponseHeaders = new ArrayList<HttpHeader>();

        for (String header : headers) {
            int colonIndex = header.indexOf(":");
            String name = header.substring(0, colonIndex).trim();
            String value = header.substring(colonIndex + 1).trim();
            this.httpResponseHeaders.add(new HttpHeader(name, value));
        }
    }

    //--------------------------------------------------------------------------
    //
    // Nested Classes
    //
    //--------------------------------------------------------------------------

    /**
     * Helper class used for headers in the HTTP request/response.
     */
    static class HttpHeader {
        public HttpHeader(String name, String value) {
            this.name = name;
            this.value = value;
        }

        public final String name;
        public final String value;
    }
}