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

fr.cenotelie.commons.lsp.LspEndpointRemoteStream Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2017 Association Cénotélie (cenotelie.fr)
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this program.
 * If not, see .
 ******************************************************************************/

package fr.cenotelie.commons.lsp;

import fr.cenotelie.commons.jsonrpc.JsonRpcClientBase;
import fr.cenotelie.commons.jsonrpc.JsonRpcContext;
import fr.cenotelie.commons.utils.IOUtils;
import fr.cenotelie.commons.utils.TextUtils;
import fr.cenotelie.commons.utils.api.*;
import fr.cenotelie.commons.utils.concurrent.SafeRunnable;
import fr.cenotelie.commons.utils.json.Json;
import fr.cenotelie.commons.utils.json.JsonParser;
import fr.cenotelie.commons.utils.logging.BufferedLogger;
import fr.cenotelie.commons.utils.logging.Logging;
import fr.cenotelie.hime.redist.ASTNode;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Implements a remote LSP endpoint that uses streams to communicate
 *
 * @author Laurent Wouters
 */
public class LspEndpointRemoteStream extends JsonRpcClientBase implements LspEndpointRemote {
    /**
     * Timeout (in ms) for waiting
     */
    private static final int TIMEOUT = 500;
    /**
     * The counter of threads
     */
    private static final AtomicInteger COUNTER = new AtomicInteger(0);
    /**
     * The bytes for the first header to read
     */
    private static final byte[] HEADER_LENGTH = LspUtils.HEADER_CONTENT_LENGTH.getBytes(IOUtils.UTF8);
    /**
     * The header-ending sequence to detect (\r\n\r\n)
     */
    private static final int[] HEADER_ENDING = new int[]{0x0D, 0x0A, 0x0D, 0x0A};

    /**
     * The local endpoint
     */
    private final LspEndpointLocal local;
    /**
     * The output stream for sending messages to the real remote endpoint
     */
    private final OutputStream output;
    /**
     * A stream to write the received and sent messages to
     */
    private final OutputStream debug;
    /**
     * The input stream to read messages from the real remote endpoint
     */
    private final InputStream input;
    /**
     * Listening thread
     */
    private final Thread thread;
    /**
     * Whether the listening thread must exit
     */
    private final AtomicBoolean mustExit;
    /**
     * The response waiting to be consumed
     */
    private final AtomicReference response;
    /**
     * The barrier to use for waiting for a response
     */
    private final CyclicBarrier responseBarrier;
    /**
     * Whether a thread is waiting for a response
     */
    private final AtomicBoolean waitingResponses;

    /**
     * Initializes this remote endpoint
     *
     * @param local  the local endpoint
     * @param output The output stream for sending messages to the real remote endpoint
     * @param input  The input stream to read messages from the real remote endpoint
     */
    public LspEndpointRemoteStream(LspEndpointLocal local, OutputStream output, InputStream input) {
        this(local, output, input, null);
    }

    /**
     * Initializes this remote endpoint
     *
     * @param local  the local endpoint
     * @param output The output stream for sending messages to the real remote endpoint
     * @param input  The input stream to read messages from the real remote endpoint
     * @param debug  A stream to write the received and sent messages to
     */
    public LspEndpointRemoteStream(LspEndpointLocal local, OutputStream output, InputStream input, OutputStream debug) {
        super(local.getResponsesDeserializer());
        this.local = local;
        this.input = input;
        this.output = output;
        this.debug = debug;
        this.thread = new Thread(new SafeRunnable() {
            @Override
            public void doRun() {
                threadListen();
                onListenerEnded();
            }

            @Override
            protected void onRunFailed(Throwable throwable) {
                onListenerEnded();
            }
        }, LspEndpointRemoteStream.class.getCanonicalName() + ".Thread." + COUNTER.getAndIncrement());
        this.mustExit = new AtomicBoolean(false);
        this.thread.start();
        this.response = new AtomicReference<>(null);
        this.responseBarrier = new CyclicBarrier(2);
        this.waitingResponses = new AtomicBoolean(false);
    }

    @Override
    public synchronized Reply send(String message, JsonRpcContext context) {
        if (context.isEmpty())
            return sendNoReply(LspUtils.envelop(message));
        return sendAndGetReply(LspUtils.envelop(message));
    }

    /**
     * Prints a debug message on a the debug stream
     *
     * @param message The message to print
     */
    private void printDebug(String message) {
        if (debug == null)
            return;
        try {
            debug.write(TextUtils.escapeStringSpecials(message).getBytes(IOUtils.CHARSET));
            debug.write(IOUtils.LINE_SEPARATOR.getBytes(IOUtils.CHARSET));
            debug.flush();
        } catch (IOException exception) {
            // do nothing
        }
    }

    /**
     * Sends a message without expecting a reply
     *
     * @param message The message to send
     * @return The reply
     */
    private Reply sendNoReply(String message) {
        try {
            writeToOutput(message);
            return ReplySuccess.instance();
        } catch (IOException exception) {
            Logging.get().error(exception);
            return new ReplyException(exception);
        }
    }

    /**
     * Sends a message and get the reply
     *
     * @param message The message to send
     * @return The reply
     */
    private Reply sendAndGetReply(String message) {
        if (!waitingResponses.compareAndSet(false, true))
            return new ReplyFailure("Bad state");
        try {
            writeToOutput(message);
        } catch (IOException exception) {
            Logging.get().error(exception);
            waitingResponses.set(false);
            return new ReplyException(exception);
        }
        try {
            responseBarrier.await();
        } catch (Exception exception) {
            return new ReplyException(exception);
        }
        String content = response.getAndSet(null);
        waitingResponses.set(false);
        return new ReplyResult<>(content);
    }

    /**
     * Writes a payload to the output stream
     *
     * @param message The message to send
     * @throws IOException When writing failed
     */
    private synchronized void writeToOutput(String message) throws IOException {
        printDebug("<== " + message);
        byte[] bytes = message.getBytes(IOUtils.UTF8);
        output.write(bytes);
        output.flush();
    }

    @Override
    public void close() throws Exception {
        if (mustExit.compareAndSet(false, true)) {
            if (thread.isAlive()) {
                thread.interrupt();
                input.close();
                try {
                    thread.join(TIMEOUT);
                } catch (InterruptedException exception) {
                    Logging.get().error(exception);
                }
            }
        }
    }

    /**
     * Event when the listening thread ended
     */
    protected void onListenerEnded() {
        // do nothing
    }

    /**
     * Main method for the listening thread
     */
    private void threadListen() {
        while (!mustExit.get() && !thread.isInterrupted()) {
            try {
                int length = threadReadHeaderLength();
                if (length < 0)
                    return;
                if (threadSkipUntilPayload() < 0)
                    return;
                byte[] payload = threadReadPayload(length);
                if (payload == null)
                    return;
                String content = new String(payload, IOUtils.UTF8);
                printDebug("==> " + content);
                threadHandlePayload(content);
            } catch (Exception exception) {
                // stream has been closed
                if (debug != null) {
                    PrintWriter writer = new PrintWriter(debug);
                    exception.printStackTrace(writer);
                    try {
                        debug.flush();
                    } catch (IOException exception2) {
                        // do nothing
                    }
                }
                return;
            }
        }
    }

    /**
     * Reads the content-length header from the input stream
     *
     * @return The length of the payload, or -1 if the thread must exit
     * @throws IOException When reading the stream fails
     */
    private int threadReadHeaderLength() throws IOException {
        int index = 0;
        while (index < HEADER_LENGTH.length) {
            // expect to read the input at the current index
            if (mustExit.get() && thread.isInterrupted())
                return -1;
            int value = input.read();
            if (value < 0)
                return -1;
            if (value == (HEADER_LENGTH[index] & 0xFF))
                index++;
            else
                // garbage on the stream
                index = 0;
        }

        // we read the header name
        // read the colon
        int value = input.read();
        if (mustExit.get() && thread.isInterrupted() || value < 0 || value != 0x3A)
            // not the colon => exit
            return -1;

        // read the length
        while (true) {
            value = input.read();
            if (mustExit.get() && thread.isInterrupted() || value < 0)
                return -1;
            if (value >= 0x30 && value <= 0x39)
                // a digit
                break;
            if (value != 0x20)
                // not a space => error, exit
                return -1;
        }
        int length = value - 0x30;
        while (true) {
            value = input.read();
            if (mustExit.get() && thread.isInterrupted() || value < 0)
                return -1;
            if (value >= 0x30 && value <= 0x39) {
                length = length * 10 + (value - 0x30);
                continue;
            }
            if (value != 0x0D)
                // not the '\r'
                return -1;
            break;
        }
        value = input.read();
        if (mustExit.get() && thread.isInterrupted() || value != 0x0A)
            // not the '\n'
            return -1;
        return length;
    }

    /**
     * Skip all content until the header ending sequence is detected
     *
     * @return The result
     * @throws IOException When reading the stream fails
     */
    private int threadSkipUntilPayload() throws IOException {
        int index = 2;
        while (index < HEADER_ENDING.length) {
            // expect to read the input at the current index
            if (mustExit.get() && thread.isInterrupted())
                return -1;
            int value = input.read();
            if (value < 0)
                return -1;
            if (value == (HEADER_ENDING[index] & 0xFF))
                index++;
            else
                index = 0;
        }
        return 0;
    }

    /**
     * Reads a payload from the input
     *
     * @param length The length of the payload
     * @return The payload, or null if reading fails
     * @throws IOException When reading the stream fails
     */
    private byte[] threadReadPayload(int length) throws IOException {
        byte[] payload = new byte[length];
        int index = 0;
        while (index < length) {
            int read = input.read(payload, index, length - index);
            if (mustExit.get() && thread.isInterrupted() || read < 0)
                return null;
            index += read;
        }
        return payload;
    }

    /**
     * Handles the specified input payload
     *
     * @param payload An input payload
     * @return The result
     * @throws Exception When an error occurs
     */
    private int threadHandlePayload(String payload) throws Exception {
        BufferedLogger logger = new BufferedLogger();
        ASTNode definition = Json.parse(logger, payload);
        if (definition == null || !logger.getErrorMessages().isEmpty())
            // failed to parse the payload
            return threadTransmitUnknown(payload);
        // determine whether this is a request or a response
        int decision = threadInspectPayload(definition);
        if (decision < 0)
            return threadTransmitRequest(payload);
        if (decision > 0)
            return threadTransmitResponse(payload);
        return threadTransmitUnknown(payload);
    }

    /**
     * Inspects the specified payload to distinguish between a request and a response
     *
     * @param definition the payload definition
     * @return 0 for no decision, -1 for requests, 1 for responses
     */
    private int threadInspectPayload(ASTNode definition) {
        if (definition.getSymbol().getID() == JsonParser.ID.object) {
            return threadInspectPayloadObject(definition);
        } else if (definition.getSymbol().getID() == JsonParser.ID.array) {
            return threadInspectPayloadArray(definition);
        } else {
            return 0;
        }
    }

    /**
     * Inspects the specified payload to distinguish between a request and a response
     *
     * @param definition the payload definition
     * @return 0 for no decision, -1 for requests, 1 for responses
     */
    private int threadInspectPayloadObject(ASTNode definition) {
        for (ASTNode child : definition.getChildren()) {
            ASTNode nodeMemberName = child.getChildren().get(0);
            String name = TextUtils.unescape(nodeMemberName.getValue());
            name = name.substring(1, name.length() - 1);
            switch (name) {
                case "method":
                case "params":
                    return -1;
                case "result":
                case "error":
                    return 1;

            }
        }
        return 0;
    }

    /**
     * Inspects the specified payload to distinguish between a request and a response
     *
     * @param definition the payload definition
     * @return 0 for no decision, -1 for requests, 1 for responses
     */
    private int threadInspectPayloadArray(ASTNode definition) {
        for (ASTNode node : definition.getChildren()) {
            if (node.getSymbol().getID() == JsonParser.ID.object) {
                int decision = threadInspectPayloadObject(node);
                if (decision != 0)
                    return decision;
            }
        }
        return 0;
    }

    /**
     * Transmits an incoming message when the message could not be distinguished between a response and a request
     *
     * @param payload The payload to transmit
     * @return The result
     * @throws Exception When an error occurs
     */
    private int threadTransmitUnknown(String payload) throws Exception {
        if (responseBarrier.getParties() == 1)
            // a response is expected => maybe this is supposed to be the response
            return threadTransmitResponse(payload);
        else
            // this is supposed to be a request
            return threadTransmitRequest(payload);
    }

    /**
     * Transmits a response to a waiting thread
     *
     * @param payload The payload to transmit
     * @return The result
     * @throws Exception When an error occurs
     */
    private int threadTransmitResponse(String payload) throws Exception {
        if (responseBarrier.getParties() != 1)
            // no waiting thread
            throw new Exception("Invalid state");
        if (!response.compareAndSet(null, payload))
            throw new Exception("Invalid state");
        responseBarrier.await();
        return 0;
    }

    /**
     * Transmits a request to the local endpoint
     *
     * @param payload The request to transmit
     * @return The result
     * @throws Exception When an error occurs
     */
    private int threadTransmitRequest(String payload) throws Exception {
        String reply = local.getHandler().handle(LspUtils.envelop(payload));
        if (reply == null)
            return 0;
        writeToOutput(reply);
        return 0;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy