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

io.socket.engineio.server.transport.Polling Maven / Gradle / Ivy

package io.socket.engineio.server.transport;

import io.socket.engineio.server.Transport;
import io.socket.engineio.server.parser.Packet;
import io.socket.engineio.server.parser.Parser;
import io.socket.engineio.server.utils.JsonUtils;
import io.socket.engineio.server.utils.ParseQS;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Polling transport.
 */
public final class Polling extends Transport implements AsyncListener {

    public static final String NAME = "polling";

    private static final List> PACKET_CLOSE = Collections.unmodifiableList(new ArrayList>() {{
        add(new Packet(Packet.CLOSE));
    }});
    private static final List> PACKET_NOOP = Collections.unmodifiableList(new ArrayList>() {{
        add(new Packet(Packet.NOOP));
    }});

    private final Object mLockObject;

    private HttpServletRequest mPollRequest;
    private HttpServletResponse mPollResponse;
    private boolean mWritable;
    private boolean mShouldClose;
    private Map mQuery;
    private Map> mHeaders;

    public Polling(Object lockObject, Parser parser) {
        super(parser);
        mLockObject = lockObject;

        mWritable = false;
        mShouldClose = false;
    }

    /* Transport */

    @Override
    public Map getInitialQuery() {
        return mQuery;
    }

    @Override
    public Map> getInitialHeaders() {
        return mHeaders;
    }

    @Override
    public void onRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        synchronized (mLockObject) {
            if (mQuery == null) {
                if (request.getQueryString() != null) {
                    mQuery = ParseQS.decode(request.getQueryString());
                } else {
                    mQuery = new HashMap<>();
                }
            }
            if (mHeaders == null) {
                mHeaders = new HashMap<>();

                final Enumeration iter = request.getHeaderNames();
                if (iter != null) {
                    while (iter.hasMoreElements()) {
                        final String headerName = iter.nextElement();
                        final List headerValues = new ArrayList<>();

                        final Enumeration iter2 = request.getHeaders(headerName);
                        while (iter2.hasMoreElements()) {
                            headerValues.add(iter2.nextElement());
                        }

                        mHeaders.put(headerName, headerValues);
                    }
                }
            }

            switch (request.getMethod().toLowerCase()) {
                case "get":
                    onPollRequest(request, response);
                    break;
                case "post":
                    onDataRequest(request, response);
                    break;
                default:
                    response.setStatus(500);
                    response.getWriter().write("");
                    break;
            }
        }
    }

    @Override
    public void send(List> packets) {
        synchronized (mLockObject) {
            mWritable = false;

            if(mShouldClose) {
                packets.add(new Packet<>(Packet.CLOSE));
            }

            @SuppressWarnings("unchecked") final Map query = (Map) mPollRequest.getAttribute("query");
            final boolean jsonp = query.containsKey("j");

            if(packets.size() == 0) {
                throw new IllegalArgumentException("No packets to send.");
            }
            mParser.encodePayload(packets, true, data -> {
                final String contentType;
                final byte[] contentBytes;

                if (jsonp) {
                    final String jsonpIndex = query.get("j").replaceAll("[^0-9]", "");
                    final String jsonContentString = (data instanceof String)?
                            ("\"" + JsonUtils.escape((String) data) + "\"") :
                            serializeByteArray((byte[])data);
                    final String jsContentString = jsonContentString
                            .replace("\u2028", "\\u2028")
                            .replace("\u2029", "\\u2029");
                    final String contentString = "___eio[" + jsonpIndex + "](" + jsContentString + ")";

                    contentType = "text/javascript; charset=UTF-8";
                    contentBytes = contentString.getBytes(StandardCharsets.UTF_8);
                } else {
                    contentType = (data instanceof String)? "text/plain; charset=UTF-8" : "application/octet-stream";
                    contentBytes = (data instanceof String)? ((String)data).getBytes(StandardCharsets.UTF_8) : ((byte[])data);
                }

                mPollResponse.setContentType(contentType);
                mPollResponse.setContentLength(contentBytes.length);

                try (OutputStream outputStream = mPollResponse.getOutputStream()) {
                    outputStream.write(contentBytes);
                } catch (IOException ex) {
                    onError("write failure", ex.getMessage());
                }

                if (mPollRequest.isAsyncStarted()) {
                    mPollRequest.getAsyncContext().complete();
                }

                mPollRequest = null;
                mPollResponse = null;
            });

            if(mShouldClose) {
                onClose();
            }
        }
    }

    @Override
    public boolean isWritable() {
        return mWritable;
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    protected void doClose() {
        synchronized (mLockObject) {
            if(mWritable) {
                send(new ArrayList<>(PACKET_CLOSE));
                onClose();
            } else {
                mShouldClose = true;
            }
        }
    }

    @Override
    protected void onData(Object data) {
        mParser.decodePayload(data, (packet, index, total) -> {
            if(packet.type.equals(Packet.CLOSE)) {
                onClose();
                return false;
            } else {
                onPacket(packet);
                return true;
            }
        });
    }

    @Override
    protected void onClose() {
        if(mWritable) {
            send(new ArrayList<>(PACKET_NOOP));
        }
        super.onClose();
    }

    /* AsyncListener */

    @Override
    public void onStartAsync(AsyncEvent asyncEvent) {
    }

    @Override
    public void onComplete(AsyncEvent asyncEvent) {
    }

    @Override
    public void onTimeout(AsyncEvent asyncEvent) {
        send(new ArrayList<>(PACKET_NOOP));
    }

    @Override
    public void onError(AsyncEvent asyncEvent) {
        onError("async failure", null);
    }

    /* Private */

    private void onPollRequest(HttpServletRequest request,
                               @SuppressWarnings("unused") HttpServletResponse response) {
        if (mPollRequest != null) {
            onError("overlap from client", "");
            mPollResponse.setStatus(500);
            try (PrintWriter writer = mPollResponse.getWriter()) {
                writer.print("error");
                writer.flush();
            } catch (IOException ignore) {
            }
            return;
        }

        mPollRequest = request;
        mPollResponse = response;

        boolean asyncEnabled = false;
        if (request.isAsyncSupported() || request.isAsyncStarted()) {
            final AsyncContext asyncContext = request.startAsync();
            asyncContext.addListener(this);
            asyncContext.setTimeout(3 * 60 * 1000);

            asyncEnabled = true;
        }

        mWritable = true;
        emit("drain");

        if (mWritable && (!asyncEnabled || mShouldClose)) {
            send(new ArrayList<>(PACKET_NOOP));
        }
    }

    private void onDataRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
        @SuppressWarnings("unchecked") final Map query = (Map) request.getAttribute("query");

        final boolean jsonp = query.containsKey("j");
        final byte[] readBuffer = readInput(request);

        if (jsonp) {
            final String packetPayloadRaw = ParseQS.decode(new String(readBuffer, StandardCharsets.UTF_8)).get("d");
            final String packetPayload = packetPayloadRaw.replace("\\n", "\n");
            onData(packetPayload);
        } else {
            onData((new String(readBuffer, StandardCharsets.UTF_8)));
        }

        response.setContentType("text/html");
        response.getWriter().write("ok");
    }

    private String serializeByteArray(byte[] input) {
        final String[] array = new String[input.length];
        for (int i = 0; i < input.length; i++) {
            array[i] = Byte.toString(input[i]);
        }

        return '[' + String.join(",", array) + ']';
    }

    private byte[] readInput(final HttpServletRequest request) throws IOException {
        try(final ServletInputStream inputStream = request.getInputStream()) {
            final byte[] readBuffer = new byte[request.getContentLength()];

            int remaining = readBuffer.length;
            while (remaining > 0) {
                remaining -= inputStream.read(readBuffer, readBuffer.length - remaining, readBuffer.length);
            }

            return readBuffer;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy