co.cask.cdap.common.service.CommandPortService.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.common.service.CommandPortService.java

Source

/*
 * Copyright  2014 Cask Data, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package co.cask.cdap.common.service;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.LineReader;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;

/**
 * This class acts as a simple TCP server that accepts textual command and produce textual response.
 * The serving loop is single thread and can only serve one client at a time. {@link CommandHandler}s
 * binded to commands are invoked from the serving thread and is expected to return promptly to not
 * blocking the serving thread.
 *
 * Sample usage:
 * <pre>
 *   CommandPortService service = CommandPortService.builder("myservice")
 *                                                  .addCommandHandler("ruok", "Are you okay?", ruokHandler)
 *                                                  .build();
 *   service.startAndWait();
 * </pre>
 *
 * To stop the service, invoke {@link #stop()} or {@link #stopAndWait()}.
 */
public final class CommandPortService extends AbstractExecutionThreadService {

    private static final Logger LOG = LoggerFactory.getLogger(CommandPortService.class);

    /**
     * Stores binding from command to {@link CommandHandler}.
     */
    private final Map<String, CommandHandler> handlers;

    /**
     * The server socket for accepting incoming requests.
     */
    private ServerSocket serverSocket;

    /**
     * Port that the server socket binded to. It will only be set after the server socket is binded.
     */
    private int port;

    /**
     * Returns a {@link Builder} to build instance of this class.
     * @param serviceName Name of the service name to build
     * @return A {@link Builder}.
     */
    public static Builder builder(String serviceName) {
        return new Builder(serviceName);
    }

    private CommandPortService(int port, Map<String, CommandHandler> handlers) {
        this.port = port;
        this.handlers = handlers;
    }

    @Override
    protected void startUp() throws Exception {
        serverSocket = new ServerSocket(port, 0, InetAddress.getByName("localhost"));
        port = serverSocket.getLocalPort();
    }

    @Override
    protected void run() throws Exception {
        LOG.info("Running commandPortService at localhost:" + port);
        serve();
    }

    @Override
    protected void shutDown() throws Exception {
        // The serverSocket would never be null if this method is called (guaranteed by AbstractExecutionThreadService).
        serverSocket.close();
    }

    @Override
    protected void triggerShutdown() {
        // The serverSocket would never be null if this method is called (guaranteed by AbstractExecutionThreadService).
        try {
            if (serverSocket.isBound()) {
                serverSocket.close();
            }
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Returns the port that the server is binded to.
     *
     * @return An int represent the port number.
     */
    public int getPort() {
        Preconditions.checkState(isRunning());
        return port;
    }

    /**
     * Starts accepting incoming request. This method would block until this service is stopped.
     *
     * @throws IOException If any I/O error occurs on the socket connection.
     */
    private void serve() throws IOException {
        while (isRunning()) {
            try {
                Socket socket = serverSocket.accept();
                BufferedWriter writer = new BufferedWriter(
                        new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
                try {
                    // Read the client command and dispatch
                    String command = new LineReader(new InputStreamReader(socket.getInputStream(), "UTF-8"))
                            .readLine();
                    CommandHandler handler = handlers.get(command);

                    if (handler != null) {
                        try {
                            handler.handle(writer);
                        } catch (Throwable t) {
                            LOG.error(String.format("Exception thrown from CommandHandler for command %s", command),
                                    t);
                        }
                    }
                } finally {
                    writer.flush();
                    socket.close();
                }
            } catch (Throwable th) {
                // NOTE: catch any exception to keep the main service running
                // Trigger by serverSocket.close() through the call from stop().
                LOG.debug(th.getMessage(), th);
            }
        }
    }

    /**
     * Builder for creating {@link CommandPortService}.
     */
    public static final class Builder {
        private final ImmutableMap.Builder<String, CommandHandler> handlerBuilder;
        private final StringBuilder helpStringBuilder;
        private boolean hasHelp;
        private int port = 0;

        /**
         * Creates a builder for the give name.
         *
         * @param serviceName Name of the {@link CommandPortService} to build.
         */
        private Builder(String serviceName) {
            handlerBuilder = ImmutableMap.builder();
            helpStringBuilder = new StringBuilder(String.format("Help for %s command port service", serviceName));
        }

        /**
         * Adds a {@link CommandHandler} for a given command.
         *
         * @param command Name of the command handled by the handler.
         * @param desc A human readable description about the command.
         * @param handler The {@link CommandHandler} to invoke when the given command is received.
         * @return This {@link Builder}.
         */
        public Builder addCommandHandler(String command, String desc, CommandHandler handler) {
            hasHelp = "help".equals(command);
            handlerBuilder.put(command, handler);
            helpStringBuilder.append("\n").append(String.format("  %s : %s", command, desc));
            return this;
        }

        public Builder setPort(int port) {
            this.port = port;
            return this;
        }

        /**
         * Builds the {@link CommandPortService}.
         *
         * @return A {@link CommandPortService}.
         */
        public CommandPortService build() {
            if (!hasHelp) {
                final String helpString = helpStringBuilder.toString();
                handlerBuilder.put("help", new CommandHandler() {
                    @Override
                    public void handle(BufferedWriter respondWriter) throws IOException {
                        respondWriter.write(helpString);
                        respondWriter.newLine();
                    }
                });
            }

            return new CommandPortService(port, handlerBuilder.build());
        }
    }

    /**
     * Interface for defining handler to react to a command.
     */
    public interface CommandHandler {

        /**
         * Invoked when the command that tied to this handler has been received
         * by the {@link CommandPortService}.
         *
         * @param respondWriter The {@link Writer} for writing response back to client
         * @throws IOException If I/O errors occurs when writing to the given writer.
         */
        void handle(BufferedWriter respondWriter) throws IOException;
    }
}