cito.stomp.Frame.java Source code

Java tutorial

Introduction

Here is the source code for cito.stomp.Frame.java

Source

/*
 * Copyright 2016-2017 Daniel Siviter
 *
 * 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 cito.stomp;

import static cito.stomp.Header.Standard.ACCEPT_VERSION;
import static cito.stomp.Header.Standard.CONTENT_LENGTH;
import static cito.stomp.Header.Standard.CONTENT_TYPE;
import static cito.stomp.Header.Standard.DESTINATION;
import static cito.stomp.Header.Standard.HOST;
import static cito.stomp.Header.Standard.ID;
import static cito.stomp.Header.Standard.MESSAGE_ID;
import static cito.stomp.Header.Standard.RECEIPT;
import static cito.stomp.Header.Standard.RECEIPT_ID;
import static cito.stomp.Header.Standard.SERVER;
import static cito.stomp.Header.Standard.SESSION;
import static cito.stomp.Header.Standard.SUBSCRIPTION;
import static cito.stomp.Header.Standard.TRANSACTION;
import static cito.stomp.Header.Standard.VERSION;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicLong;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import javax.ws.rs.core.MediaType;

import org.apache.commons.lang3.StringUtils;

import cito.stomp.Header.Standard;

/**
 * Defines a STOMP frame
 * 
 * @author Daniel Siviter
 * @since v1.0 [12 Jul 2016]
 */
@Immutable
@ThreadSafe
public class Frame {
    private static final AtomicLong MESSAGE_ID_COUNTER = new AtomicLong();
    public static final Frame HEART_BEAT = new Frame(Command.HEARTBEAT, new HashMap<>(0));

    private final Command command;
    private final Map<Header, List<String>> headers;
    private final Optional<ByteBuffer> body;

    /**
     * 
     * @param command
     * @param headers
     */
    Frame(@Nonnull Command command, @Nonnull Map<Header, List<String>> headers) {
        this(command, headers, Optional.empty());
    }

    /**
     * 
     * @param command
     * @param headers
     * @param body
     */
    Frame(@Nonnull Command command, @Nonnull Map<Header, List<String>> headers,
            @Nonnull Optional<ByteBuffer> body) {
        this.command = requireNonNull(command);
        final Map<Header, List<String>> tmpHeaders = new LinkedHashMap<>(headers);
        tmpHeaders.entrySet().forEach(e -> e.setValue(unmodifiableList(e.getValue())));
        this.headers = unmodifiableMap(tmpHeaders);
        this.body = body.map(ByteBuffer::asReadOnlyBuffer);
    }

    /**
     * 
     * @return
     */
    public boolean isHeartBeat() {
        return this.command == Command.HEARTBEAT;
    }

    /**
     * 
     * @return
     */
    public Command command() {
        return command;
    }

    /**
     * 
     * @return
     */
    public Map<Header, List<String>> headers() {
        return headers;
    }

    /**
     * @return the frame body
     */
    public Optional<ByteBuffer> body() {
        return body;
    }

    /**
     * Checks if the header is set on the frame.
     * 
     * @param header the header to check.
     * @return {@code true} if the frame contains the header.
     */
    public boolean contains(Header header) {
        return !Objects.isNull(get(header));
    }

    /**
     * 
     * @param header
     * @return
     */
    public List<String> get(Header header) {
        return headers().get(header);
    }

    /**
     * 
     * @param key
     * @return
     */
    public List<String> getHeader(String key) {
        return get(Header.valueOf(key));
    }

    /**
     * 
     * @param header
     * @return
     */
    public Optional<String> getFirst(@Nonnull Header header) {
        final List<String> values = get(header);
        if (values != null && !values.isEmpty()) {
            return Optional.of(values.get(0));
        }
        return Optional.empty();
    }

    /**
     * 
     * @param header
     * @return
     */
    public Optional<String> getFirstHeader(@Nonnull String header) {
        return getFirst(Header.valueOf(header));
    }

    /**
     * 
     * @param header
     * @return
     */
    public OptionalInt getFirstInt(@Nonnull Header header) {
        final Optional<String> value = getFirst(header);
        return value.isPresent() ? OptionalInt.of(Integer.parseInt(value.get())) : OptionalInt.empty();
    }

    /**
     * 
     * @return
     */
    public Optional<String> destination() {
        return getFirst(DESTINATION);
    }

    /**
     * 
     * @return
     */
    public OptionalInt contentLength() {
        return getFirstInt(CONTENT_LENGTH);
    }

    /**
     * 
     * @return
     */
    public Optional<MediaType> contentType() {
        return getFirst(CONTENT_TYPE).map(MediaType::valueOf);
    }

    /**
     * 
     * @return
     */
    public OptionalInt receipt() {
        return getFirstInt(RECEIPT);
    }

    /**
     * 
     * @return
     */
    public OptionalInt receiptId() {
        return getFirstInt(RECEIPT_ID);
    }

    /**
     * 
     * @return
     */
    public Optional<String> subscription() {
        if (this.command == Command.MESSAGE) { // why is MESSAGE so special?!
            return getFirst(SUBSCRIPTION);
        }
        return getFirst(ID);
    }

    /**
     * 
     * @return
     */
    public Optional<HeartBeat> heartBeat() {
        return getFirst(Standard.HEART_BEAT).map(HeartBeat::new);
    }

    /**
     * 
     * @return
     */
    public Optional<String> transaction() {
        return getFirst(TRANSACTION);
    }

    /**
     * 
     * @return
     */
    public Optional<String> session() {
        return getFirst(SESSION);
    }

    /**
     * Only use this for debugging purposes. An especially large frame could lead to {@link BufferOverflowException}.
     */
    @Override
    public String toString() {
        // 64k is generally much larger than the buffer used by WebSocket implementation, so this should suffice
        return UTF_8.decode(Encoding.from(this, false, 64 * 1024)).toString();
    }

    // --- Static Methods ---

    /**
     * 
     * @param host
     * @param acceptVersion
     * @return
     */
    public static Builder connect(@Nonnull String host, @Nonnull String... acceptVersion) {
        return builder(Command.CONNECT).header(HOST, host).header(ACCEPT_VERSION, acceptVersion);
    }

    /**
     * 
     * @return
     */
    public static Builder disconnect() {
        return builder(Command.DISCONNECT);
    }

    /**
     * 
     * @return
     */
    public static Builder error() {
        return builder(Command.ERROR);
    }

    /**
     * 
     * @param destination
     * @param subscriptionId
     * @param messageId
     * @param contentType
     * @param body
     * @return
     */
    public static Builder message(@Nonnull String destination, @Nonnull String subscriptionId,
            @Nonnull String messageId, MediaType contentType, @Nonnull String body) {
        return builder(Command.MESSAGE).destination(destination).subscription(subscriptionId).messageId(messageId)
                .body(contentType, body);
    }

    /**
     * 
     * @param destination
     * @param subscriptionId
     * @param messageId
     * @param contentType
     * @param body
     * @return
     */
    public static Builder message(@Nonnull String destination, @Nonnull String subscriptionId,
            @Nonnull String messageId, MediaType contentType, @Nonnull ByteBuffer body) {
        return builder(Command.MESSAGE).destination(destination).subscription(subscriptionId).messageId(messageId)
                .body(contentType, body);
    }

    /**
     * 
     * @param destination
     * @param contentType
     * @param body
     * @return
     */
    public static Builder send(@Nonnull String destination, MediaType contentType, @Nonnull ByteBuffer body) {
        return builder(Command.SEND).destination(destination).body(contentType, body);
    }

    /**
     * 
     * @param destination
     * @param contentType
     * @param body
     * @return
     */
    public static Builder send(@Nonnull String destination, MediaType contentType, @Nonnull String body) {
        return builder(Command.SEND).destination(destination).body(contentType, body);
    }

    /**
     * 
     * @param version
     * @param session
     * @param server
     * @param heartBeat
     * @return
     */
    public static Builder connnected(@Nonnull String version, @Nonnull String session, @Nonnull String server) {
        final Builder builder = builder(Command.CONNECTED).header(VERSION, version);
        builder.header(SESSION, requireNonNull(session));
        builder.header(SERVER, requireNonNull(server));
        return builder;
    }

    /**
     * 
     * @param id
     * @param destination
     * @return
     */
    public static Builder subscribe(@Nonnull String id, @Nonnull String destination) {
        return builder(Command.SUBSCRIBE).subscription(id).destination(destination);
    }

    /**
     * 
     * @param receiptId
     * @return
     */
    public static Builder receipt(@Nonnull String receiptId) {
        return builder(Command.RECEIPT).header(RECEIPT_ID, receiptId);
    }

    /**
     * 
     * @param command
     * @return
     */
    public static Builder builder(@Nonnull Command command) {
        return new Builder(command);
    }

    /**
     * 
     * @param frame
     * @return
     */
    public static Builder builder(@Nonnull Builder builder) {
        return new Builder(builder);
    }

    /**
     * 
     * @param frame
     * @return
     */
    public static Builder builder(@Nonnull Frame frame) {
        return new Builder(frame);
    }

    // --- Inner Classes ---

    /**
     * A {@link Frame} builder.
     * 
     * @author Daniel Siviter
     * @since v1.0 [15 Jul 2016]
     */
    public static class Builder {
        private final Command command;
        private final Map<Header, List<String>> headers = new LinkedHashMap<>();
        private Optional<ByteBuffer> body = Optional.empty();

        /**
         * Create a {@link Frame} builder from the given {@link Builder}.
         * 
         * @param builder
         */
        private Builder(@Nonnull Builder builder) {
            this(builder.command);

            for (Entry<Header, List<String>> e : builder.headers.entrySet()) {
                headers.put(e.getKey(), new ArrayList<>(e.getValue()));
            }
            this.body = builder.body;
        }

        /**
         * Create a {@link Frame} builder from the given {@link Frame}.
         * 
         * @param frame
         */
        private Builder(@Nonnull Frame frame) {
            this(frame.command());

            for (Entry<Header, List<String>> e : frame.headers().entrySet()) {
                headers.put(e.getKey(), new ArrayList<>(e.getValue()));
            }
            this.body = frame.body();
        }

        /**
         * Create a {@link Frame} for the {@link Command}. 
         * 
         * @param command
         */
        private Builder(@Nonnull Command command) {
            this.command = command;
        }

        /**
         * 
         * @param header
         * @param values
         * @return
         */
        public Builder header(@Nonnull Header header, @Nonnull String... values) {
            if (values == null || values.length == 0)
                throw new IllegalArgumentException("'values' cannot be null or empty!");

            final StringJoiner joiner = new StringJoiner(",");
            for (String v : values) {
                joiner.add(v);
            }

            List<String> valueList = this.headers.get(header);
            if (valueList == null) {
                this.headers.put(header, valueList = new ArrayList<>());
            }
            valueList.add(joiner.toString());
            return this;
        }

        /**
         * 
         * @param headers
         * @return
         */
        public Builder headers(@Nonnull Map<Header, String> headers) {
            for (Entry<Header, String> e : headers.entrySet()) {
                header(e.getKey(), e.getValue());
            }
            return this;
        }

        /**
         * 
         * @param destination
         * @return
         */
        public Builder destination(@Nonnull String destination) {
            if (!this.command.destination()) {
                throw new IllegalArgumentException(this.command + " does not accept a destination!");
            }
            header(DESTINATION, destination);
            return this;
        }

        /**
         * 
         * @param messageId
         * @return
         */
        public Builder messageId(@Nonnull String messageId) {
            header(MESSAGE_ID, messageId);
            return this;
        }

        /**
         * Custom Header: send the message to 
         * 
         * @param sessionId
         * @return
         */
        public Builder session(@Nonnull String session) {
            header(SESSION, session);
            return this;
        }

        /**
         * 
         * @param body
         * @return
         * @throws IllegalArgumentException if the command type does not accept a body or {@code body} is {@code null}.
         */
        public Builder body(MediaType contentType, @Nonnull String body) {
            return body(contentType, UTF_8.encode(requireNonNull(body)));
        }

        /**
         * 
         * @param contentType
         * @param body
         * @return
         * @throws IllegalArgumentException if the command type does not accept a body or {@code body} is {@code null}.
         */
        public Builder body(MediaType contentType, @Nonnull ByteBuffer body) {
            if (!this.command.body()) {
                throw new IllegalArgumentException(this.command + " does not accept a body!");
            }
            this.body = Optional.of(body);
            header(CONTENT_LENGTH, Integer.toString(body.limit()));
            return contentType == null ? this : header(CONTENT_TYPE, contentType.toString());
        }

        /**
         * 
         * @param id
         * @return
         */
        public Builder subscription(@Nonnull String id) {
            if (!this.command.subscriptionId()) {
                throw new IllegalArgumentException(this.command + " does not accept a subscription!");
            }

            if (this.command == Command.MESSAGE) { // why is MESSAGE so special?!
                header(SUBSCRIPTION, requireNonNull(id));
            } else {
                header(ID, requireNonNull(id));
            }

            return this;
        }

        /**
         * 
         * @param outgoing
         * @param incoming
         * @return
         */
        public Builder heartbeat(@Nonnull int outgoing, @Nonnull int incoming) {
            return header(Standard.HEART_BEAT, Integer.toString(outgoing), Integer.toString(incoming));
        }

        /**
         * 
         * @param versions
         * @return
         */
        public Builder version(String... versions) {
            return header(VERSION, versions);
        }

        /**
         * 
         * @param receiptId
         * @return
         */
        public Builder receipt(int receiptId) {
            return header(RECEIPT, Integer.toString(receiptId));
        }

        /**
         * 
         * @param receiptId
         * @return
         */
        public Builder receiptId(int receiptId) {
            return header(RECEIPT_ID, Integer.toString(receiptId));
        }

        /**
         * Derives values from other headers if needed.
         */
        private void derive() {
            if (!this.headers.containsKey(MESSAGE_ID) && this.command == Command.MESSAGE) {
                String messageId = Long.toString(MESSAGE_ID_COUNTER.getAndIncrement());
                if (this.headers.containsKey(SESSION))
                    messageId = this.headers.get(SESSION).get(0).concat("-").concat(messageId);
                messageId(messageId);
            }
        }

        /**
         * Verifies the minimum headers are present.
         */
        private void verify() {
            switch (this.command) {
            case ACK:
            case NACK:
                assertExists(ID);
                break;
            case BEGIN:
            case COMMIT:
            case ABORT:
                assertExists(TRANSACTION);
                break;
            case CONNECT:
            case STOMP:
                assertExists(ACCEPT_VERSION);
                assertExists(HOST);
                break;
            case CONNECTED:
                assertExists(VERSION);
                break;
            case DISCONNECT:
            case ERROR:
            case HEARTBEAT:
                break;
            case MESSAGE:
                assertExists(DESTINATION);
                assertExists(MESSAGE_ID);
                assertExists(SUBSCRIPTION);
                break;
            case RECEIPT:
                assertExists(RECEIPT_ID);
                break;
            case SEND:
                assertExists(DESTINATION);
                break;
            case SUBSCRIBE:
                assertExists(DESTINATION);
                assertExists(ID);
                break;
            case UNSUBSCRIBE:
                assertExists(ID);
                break;
            default:
                return;
            }
        }

        /**
         * 
         * @param header
         */
        private void assertExists(Header header) {
            if (!this.headers.containsKey(header))
                throw new AssertionError(String.format("Required header '%s' not set on '%s' frame!",
                        StringUtils.capitalize(header.toString().toLowerCase()), command));
        }

        /**
         * @return a newly created {@link Frame}.
         */
        public Frame build() {
            derive();
            verify();

            final Map<Header, List<String>> headers = new LinkedHashMap<>();
            for (Entry<Header, List<String>> e : this.headers.entrySet()) {
                headers.put(e.getKey(), new ArrayList<>(e.getValue()));
            }
            return new Frame(this.command, headers, this.body);
        }
    }

    /**
     * 
     * @author Daniel Siviter
     * @since v1.0 [25 Jul 2016]
     */
    public static class HeartBeat {
        public final long x, y;

        private HeartBeat(String heartBeat) {
            if (heartBeat == null) {
                this.x = 0;
                this.y = 0;
                return;
            }
            final String[] tokens = heartBeat.split(",");
            if (tokens.length != 2)
                throw new IllegalStateException("Invalid number of heart beat elements!");
            this.x = Long.parseLong(tokens[0]);
            this.y = Long.parseLong(tokens[1]);
        }
    }
}