public abstract class Command {

    private final CommandCode commandCode;

    protected Command(CommandCode commandCode) {
        this.commandCode = commandCode;
    }

    public CommandCode getCommandCode() {
        return commandCode;
    }

} public final class ListIdentity extends Command {

    public static final CommandCode COMMAND_CODE = CommandCode.ListIdentity;

    private final CipIdentityItem identityItem;
    private final CipSecurityItem securityItem;

    public ListIdentity() {
        this(null);
    }

    public ListIdentity(CipIdentityItem identityItem) {
        this(identityItem, null);
    }

    public ListIdentity(CipIdentityItem identityItem, CipSecurityItem securityItem) {
        super(COMMAND_CODE);

        this.identityItem = identityItem;
        this.securityItem = securityItem;
    }

    /**
     * @deprecated use {@link #getIdentityItem()}
     */
    @Deprecated
    public Optional<CipIdentityItem> getIdentity() {
        return Optional.ofNullable(identityItem);
    }

    /**
     * @return an {@link Optional} containing the {@link CipIdentityItem}, if present.
     */
    public Optional<CipIdentityItem> getIdentityItem() {
        return Optional.ofNullable(identityItem);
    }

    /**
     * @return an {@link Optional} containing the {@link CipSecurityItem}, if present.
     */
    public Optional<CipSecurityItem> getSecurityItem() {
        return Optional.ofNullable(securityItem);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ListIdentity that = (ListIdentity) o;
        return Objects.equals(identityItem, that.identityItem) &&
            Objects.equals(securityItem, that.securityItem);
    }

    @Override
    public int hashCode() {
        return Objects.hash(identityItem, securityItem);
    }

    public static ByteBuf encode(ListIdentity command, ByteBuf buffer) {
        List<CpfItem> items = new ArrayList<>();
        command.getIdentityItem().ifPresent(items::add);
        command.getSecurityItem().ifPresent(items::add);

        if (items.size() > 0) {
            CpfItem[] itemArray = items.toArray(new CpfItem[0]);
            CpfPacket.encode(new CpfPacket(itemArray), buffer);
        }

        return buffer;
    }

    public static ListIdentity decode(ByteBuf buffer) {
        if (buffer.readableBytes() > 0) {
            CpfPacket packet = CpfPacket.decode(buffer);
            CpfItem[] items = packet.getItems();

            CipIdentityItem identityItem;
            CipSecurityItem securityItem = null;

            if (items.length >= 1) {
                assert items[0] instanceof CipIdentityItem;
                identityItem = (CipIdentityItem) items[0];

                if (items.length >= 2 && items[1] instanceof CipSecurityItem) {
                    securityItem = (CipSecurityItem) items[1];
                }

                return new ListIdentity(identityItem, securityItem);
            } else {
                return new ListIdentity();
            }
        }

        return new ListIdentity();
    }

} public final class ListInterfaces extends Command {

    private final InterfaceInformation[] interfaces;

    public ListInterfaces(InterfaceInformation... interfaces) {
        super(CommandCode.ListInterfaces);

        this.interfaces = interfaces;
    }

    public InterfaceInformation[] getInterfaces() {
        return interfaces;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ListInterfaces that = (ListInterfaces) o;

        return Arrays.equals(interfaces, that.interfaces);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(interfaces);
    }

    public static ByteBuf encode(ListInterfaces command, ByteBuf buffer) {
        if (command.getInterfaces().length != 0) {
            buffer.writeShort(command.getInterfaces().length);

            for (InterfaceInformation interfaceInformation : command.getInterfaces()) {
                InterfaceInformation.encode(interfaceInformation, buffer);
            }
        }

        return buffer;
    }

    public static ListInterfaces decode(ByteBuf buffer) {
        int itemCount = buffer.readableBytes() >= 2 ? buffer.readUnsignedShort() : 0;

        InterfaceInformation[] interfaces = new InterfaceInformation[itemCount];

        for (int i = 0; i < itemCount; i++) {
            interfaces[i] = InterfaceInformation.decode(buffer);
        }

        return new ListInterfaces(interfaces);
    }

    public static class InterfaceInformation {

        private final int itemId;
        private final byte[] data;

        public InterfaceInformation(int itemId, byte[] data) {
            this.itemId = itemId;
            this.data = data;
        }

        public int getItemId() {
            return itemId;
        }

        public byte[] getData() {
            return data;
        }

        public static ByteBuf encode(InterfaceInformation interfaceInformation, ByteBuf buffer) {
            buffer.writeShort(interfaceInformation.getItemId());
            buffer.writeShort(interfaceInformation.getData().length);
            buffer.writeBytes(interfaceInformation.getData());

            return buffer;
        }

        public static InterfaceInformation decode(ByteBuf buffer) {
            int itemId = buffer.readUnsignedShort();
            int dataLength = buffer.readUnsignedShort();
            byte[] data = new byte[dataLength];
            buffer.readBytes(data);

            return new InterfaceInformation(itemId, data);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            InterfaceInformation that = (InterfaceInformation) o;

            return itemId == that.itemId && Arrays.equals(data, that.data);
        }

        @Override
        public int hashCode() {
            int result = itemId;
            result = 31 * result + Arrays.hashCode(data);
            return result;
        }

    }
} public final class ListServices extends Command {

    private final ServiceInformation[] services;

    public ListServices(ServiceInformation... services) {
        super(CommandCode.ListServices);

        this.services = services;
    }

    public ServiceInformation[] getServices() {
        return services;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ListServices that = (ListServices) o;

        return Arrays.equals(services, that.services);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(services);
    }

    public static ByteBuf encode(ListServices command, ByteBuf buffer) {
        if (command.getServices().length != 0) {
            buffer.writeShort(command.getServices().length);

            for (ServiceInformation serviceInformation : command.getServices()) {
                ServiceInformation.encode(serviceInformation, buffer);
            }
        }

        return buffer;
    }

    public static ListServices decode(ByteBuf buffer) {
        int itemCount = buffer.readableBytes() >= 2 ? buffer.readUnsignedShort() : 0;

        ServiceInformation[] services = new ServiceInformation[itemCount];

        for (int i = 0; i < itemCount; i++) {
            services[i] = ServiceInformation.decode(buffer);
        }

        return new ListServices(services);
    }

    public static class ServiceInformation {

        private final int typeCode;
        private final int version;
        private final int capabilityFlags;
        private final String name;

        public ServiceInformation(int typeCode, int version, int capabilityFlags, String name) {
            this.typeCode = typeCode;
            this.version = version;
            this.capabilityFlags = capabilityFlags;
            this.name = name;
        }

        public int getTypeCode() {
            return typeCode;
        }

        public int getVersion() {
            return version;
        }

        public int getCapabilityFlags() {
            return capabilityFlags;
        }

        public String getName() {
            return name;
        }

        public static ByteBuf encode(ServiceInformation serviceInformation, ByteBuf buffer) {
            buffer.writeShort(serviceInformation.getTypeCode());

            // The 16 bytes of the name plus two shorts.
            buffer.writeShort(20);

            // Encode the item...
            buffer.writeShort(serviceInformation.getVersion());
            buffer.writeShort(serviceInformation.getCapabilityFlags());
            writeString(serviceInformation.getName(), buffer);

            return buffer;
        }

        public static ServiceInformation decode(ByteBuf buffer) {
            int typeCode = buffer.readUnsignedShort();
            int itemLength = buffer.readShort();
            int version = buffer.readShort();
            int capabilityFlags = buffer.readShort();
            String name = readString(buffer, itemLength - 4).trim();
            return new ServiceInformation(typeCode, version, capabilityFlags, name);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            ServiceInformation that = (ServiceInformation) o;

            return (typeCode == that.typeCode) && (version == that.version) &&
                    (capabilityFlags == that.capabilityFlags) && name.equals(that.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(getTypeCode(), getVersion(), getCapabilityFlags(), getName());
        }

        private static String readString(ByteBuf buffer, int length) {
            length = Math.min(Math.min(length, 255), buffer.readableBytes());
            byte[] bs = new byte[length];
            buffer.readBytes(bs);
            return new String(bs, Charset.forName("US-ASCII"));
        }

        private static void writeString(String s, ByteBuf buffer) {
            byte[] fullBytes = new byte[16];
            byte[] bytes = s.getBytes(Charset.forName("US-ASCII"));
            System.arraycopy(bytes, 0, fullBytes, 0, Math.min(bytes.length, 16));
            buffer.writeBytes(fullBytes);
        }

    }
} public final class Nop extends Command {

    private final byte[] data;

    public Nop() {
        this(new byte[0]);
    }

    public Nop(byte[] data) {
        super(CommandCode.Nop);

        this.data = data;
    }

    public byte[] getData() {
        return data;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Nop nop = (Nop) o;

        return Arrays.equals(data, nop.data);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(data);
    }

    public static ByteBuf encode(Nop command, ByteBuf buffer) {
        buffer.writeBytes(command.getData());

        return buffer;
    }

    public static Nop decode(ByteBuf buffer) {
        int size = Math.min(buffer.readableBytes(), 65511);
        byte[] data = new byte[size];
        buffer.readBytes(data);

        return new Nop(data);
    }

} public final class RegisterSession extends Command {

    public static final int DEFAULT_PROTOCOL_VERSION = 1;
    public static final int DEFAULT_OPTION_FLAGS = 0;

    private final int protocolVersion;
    private final int optionFlags;

    public RegisterSession() {
        this(DEFAULT_PROTOCOL_VERSION, DEFAULT_OPTION_FLAGS);
    }

    public RegisterSession(int protocolVersion, int optionFlags) {
        super(CommandCode.RegisterSession);

        this.protocolVersion = protocolVersion;
        this.optionFlags = optionFlags;
    }

    public int getProtocolVersion() {
        return protocolVersion;
    }

    public int getOptionFlags() {
        return optionFlags;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        RegisterSession that = (RegisterSession) o;

        return optionFlags == that.optionFlags && protocolVersion == that.protocolVersion;
    }

    @Override
    public int hashCode() {
        int result = protocolVersion;
        result = 31 * result + optionFlags;
        return result;
    }

    public static ByteBuf encode(RegisterSession command, ByteBuf buffer) {
        buffer.writeShort(command.getProtocolVersion());
        buffer.writeShort(command.getOptionFlags());

        return buffer;
    }

    public static RegisterSession decode(ByteBuf buffer) {
        return new RegisterSession(
            buffer.readUnsignedShort(),
            buffer.readUnsignedShort()
        );
    }

} public final class SendRRData extends Command {

    public static final long DEFAULT_INTERFACE_HANDLE = 0;
    public static final int DEFAULT_TIMEOUT = 0;

    private final long interfaceHandle;
    private final int timeout;
    private final CpfPacket packet;

    public SendRRData(CpfPacket packet) {
        this(DEFAULT_INTERFACE_HANDLE, DEFAULT_TIMEOUT, packet);
    }

    public SendRRData(long interfaceHandle, int timeout, CpfPacket packet) {
        super(CommandCode.SendRRData);

        this.interfaceHandle = interfaceHandle;
        this.timeout = timeout;
        this.packet = packet;
    }

    public long getInterfaceHandle() {
        return interfaceHandle;
    }

    public int getTimeout() {
        return timeout;
    }

    public CpfPacket getPacket() {
        return packet;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        SendRRData that = (SendRRData) o;

        return interfaceHandle == that.interfaceHandle &&
            timeout == that.timeout &&
            packet.equals(that.packet);
    }

    @Override
    public int hashCode() {
        int result = (int) (interfaceHandle ^ (interfaceHandle >>> 32));
        result = 31 * result + timeout;
        result = 31 * result + packet.hashCode();
        return result;
    }

    public static ByteBuf encode(SendRRData command, ByteBuf buffer) {
        buffer.writeInt((int) command.getInterfaceHandle());
        buffer.writeShort(command.getTimeout());

        CpfPacket.encode(command.getPacket(), buffer);

        return buffer;
    }

    public static SendRRData decode(ByteBuf buffer) {
        return new SendRRData(
            buffer.readUnsignedInt(),
            buffer.readUnsignedShort(),
            CpfPacket.decode(buffer)
        );
    }

} public final class SendUnitData extends Command {

    public static final long DEFAULT_INTERFACE_HANDLE = 0;
    public static final int DEFAULT_TIMEOUT = 0;

    private final long interfaceHandle;
    private final int timeout;
    private final CpfPacket packet;

    public SendUnitData(CpfPacket packet) {
        this(DEFAULT_INTERFACE_HANDLE, DEFAULT_TIMEOUT, packet);
    }

    public SendUnitData(long interfaceHandle, int timeout, CpfPacket packet) {
        super(CommandCode.SendUnitData);

        this.interfaceHandle = interfaceHandle;
        this.timeout = timeout;
        this.packet = packet;
    }

    public long getInterfaceHandle() {
        return interfaceHandle;
    }

    public int getTimeout() {
        return timeout;
    }

    public CpfPacket getPacket() {
        return packet;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        SendUnitData that = (SendUnitData) o;

        return interfaceHandle == that.interfaceHandle &&
            timeout == that.timeout &&
            packet.equals(that.packet);
    }

    @Override
    public int hashCode() {
        int result = (int) (interfaceHandle ^ (interfaceHandle >>> 32));
        result = 31 * result + timeout;
        result = 31 * result + packet.hashCode();
        return result;
    }

    public static ByteBuf encode(SendUnitData command, ByteBuf buffer) {
        buffer.writeInt((int) command.getInterfaceHandle());
        buffer.writeShort(command.getTimeout());

        CpfPacket.encode(command.getPacket(), buffer);

        return buffer;
    }

    public static SendUnitData decode(ByteBuf buffer) {
        return new SendUnitData(
            buffer.readUnsignedInt(),
            buffer.readUnsignedShort(),
            CpfPacket.decode(buffer)
        );
    }

} public final class UnRegisterSession extends Command {

    public UnRegisterSession() {
        super(CommandCode.UnRegisterSession);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        return !(o == null || getClass() != o.getClass());
    }

    @Override
    public int hashCode() {
        return 0;
    }

    public static ByteBuf encode(UnRegisterSession command, ByteBuf buffer) {
        return buffer;
    }

    public static UnRegisterSession decode(ByteBuf buffer) {
        return new UnRegisterSession();
    }

} public final class CpfPacket {

    private final CpfItem[] items;

    public CpfPacket(CpfItem... items) {
        this.items = items;
    }

    public CpfItem[] getItems() {
        return items;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CpfPacket cpfPacket = (CpfPacket) o;

        return Arrays.equals(items, cpfPacket.items);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(items);
    }

    public static ByteBuf encode(CpfPacket packet, ByteBuf buffer) {
        buffer.writeShort(packet.getItems().length);

        for (CpfItem item : packet.getItems()) {
            CpfItem.encode(item, buffer);
        }

        return buffer;
    }

    public static CpfPacket decode(ByteBuf buffer) {
        int itemCount = buffer.readUnsignedShort();
        CpfItem[] items = new CpfItem[itemCount];

        for (int i = 0; i < itemCount; i++) {
            items[i] = CpfItem.decode(buffer);
        }

        return new CpfPacket(items);
    }
}
import com.digitalpetri.enip.commands.Command;
import com.digitalpetri.enip.commands.ListIdentity;
import com.digitalpetri.enip.commands.ListInterfaces;
import com.digitalpetri.enip.commands.ListServices;
import com.digitalpetri.enip.commands.Nop;
import com.digitalpetri.enip.commands.RegisterSession;
import com.digitalpetri.enip.commands.SendRRData;
import com.digitalpetri.enip.commands.SendUnitData;
import com.digitalpetri.enip.commands.UnRegisterSession;
import com.digitalpetri.enip.cpf.CpfPacket;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
package com.digitalpetri.enip;



public class EnipPacketTest extends SerializationTest<EnipPacket> {

    @Test(dataProvider = "getCommand")
    public void testSerialization(Command command) {
        EnipPacket packet = new EnipPacket(
            command.getCommandCode(),
            1L,
            EnipStatus.EIP_SUCCESS,
            2L,
            command
        );

        EnipPacket decoded = encodeDecode(packet, EnipPacket::encode, EnipPacket::decode);

        assertEquals(packet, decoded);
    }

    @DataProvider
    private static Object[][] getCommand() {
        return new Object[][]{
            {new ListIdentity()},
            {new ListInterfaces()},
            {new ListServices()},
            {new Nop()},
            {new RegisterSession()},
            {new SendRRData(new CpfPacket())},