/*
* Copyright (c) 2015-2016, Christoph Engelbert (aka noctarius) and
* contributors. All rights reserved.
*
* 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 com.noctarius.tengi.core.model;
import com.noctarius.tengi.core.impl.Validate;
import com.noctarius.tengi.core.serialization.Marshallable;
import com.noctarius.tengi.core.serialization.TypeId;
import com.noctarius.tengi.core.serialization.codec.Decoder;
import com.noctarius.tengi.core.serialization.codec.Encoder;
import com.noctarius.tengi.spi.serialization.Protocol;
import com.noctarius.tengi.spi.serialization.impl.DefaultProtocolConstants;
import java.util.HashMap;
import java.util.Map;
/**
* <p>The <tt>Packet</tt> class is a convenient way to design network packets which
* can either be used directly or by subclassing it.</p>
* <pre>
* Packet packet = new Packet("login");
* packet.setValue("username", "myUserName");
* packet.setValue("password", hash("myPassword"));
* packet.setValue("metadata", "some dynamic value");
* connection.writeObject(packet);
* </pre>
* <p>By default all properties, defined with {@link #setValue(String, Object)}, are
* serialized as objects which can be quiet inefficient, by subclassing the Packet
* instance, serialization for known properties might be optimized. That way fixed
* properties and dynamic properties can be combined.</p>
* <pre>
* @TypeId(12345)
* public class LoginPacket extends Packet {
* private String username;
* private String password;
*
* public LoginPacket() {
* super("LoginPacket");
* }
*
* public void setUsername(String username) {
* notNull("username", username);
* this.username = username;
* }
* public String getUsername() { return username; }
*
* public void setPassword(String password) {
* notNull("password", password);
* this.password = password;
* }
* public String getPassword() { return password; }
*
* protected void marshall0(Encoder encoder, Protocol protocol) {
* encoder.writeString(username);
* encoder.writeString(password);
* }
*
* protected void unmarshall0(Decoder decoder, Protocol protocol) {
* username = decoder.readString();
* password = decoder.readString();
* }
* }
*
* LoginPacket packet = new LoginPacket();
* packet.setUsername("myUserName");
* packet.setPassword(hash("myPassword"));
* packet.setValue("metadata", "some dynamic value");
* connection.writeObject(packet);
* </pre>
* <p><tt>Packet</tt> subclasses need to register their own typeid when using
* the default protocol to be uniquely identified. This can either be achieved
* by annotating the class with {@link com.noctarius.tengi.core.serialization.TypeId}
* or by implementing {@link com.noctarius.tengi.core.serialization.Identifiable}
* interface. A special {@link com.noctarius.tengi.core.serialization.marshaller.Marshaller}
* is also an option to completely adopt the default serialization but it is not recommended
* to use a <tt>Packet</tt> subclass in this case.</p>
* <p>For more information on custom serialization please see
* {@link com.noctarius.tengi.core.serialization.codec.Encoder},
* {@link com.noctarius.tengi.core.serialization.codec.Decoder} and
* {@link com.noctarius.tengi.spi.serialization.Protocol}.</p>
*/
@TypeId(DefaultProtocolConstants.TYPEID_PACKET)
public class Packet
implements Marshallable {
private final Map<String, Object> values = new HashMap<>();
private String packetName;
/**
* Creates a new <tt>Packet</tt> instance with the given packet name. The name is
* not used internally but can be used by user-code to distinguish different logic
* based on its given name.
*
* @param packetName the given name of the packet to distinguish different types of packets
*/
public Packet(String packetName) {
Validate.notNull("packageName", packetName);
this.packetName = packetName;
}
/**
* Assigns a key to a value. The value must be a of a serializable type, otherwise serialization
* will fail with an exception while sending the packet. Both key and value must be non-null,
* otherwise a {@link java.lang.NullPointerException} is thrown. If called with an already existing
* key, any previously assigned value to the key will be overridden.
*
* @param key the key to assign the value to
* @param value the assigned value
* @param <V> the type of the value
* @throws java.lang.NullPointerException whenever key or value are null
*/
public <V> void setValue(String key, V value) {
Validate.notNull("key", key);
Validate.notNull("value", value);
values.put(key, value);
}
/**
* <p>Retrieves a value from the internal value set assigned to the given key. If the key doesn't have
* an assigned value <tt>null</tt> is returned. The given key must be non-null, otherwise a
* {@link java.lang.NullPointerException} is thrown.</p>
* <p>The value is implicitly casted to the type of the assignment and might throw a
* {@link java.lang.ClassCastException} if the retrieve value and the assignment type are not assignable
* to each other.</p>
*
* @param key the key to retrieve its assigned value
* @param <V> the implicit type of the value
* @return the assigned value, if exists, otherwise null
* @throws java.lang.NullPointerException whenever key is null
* @throws java.lang.ClassCastException whenever the returned value is not assignment compatible to the assignment type
*/
public <V> V getValue(String key) {
Validate.notNull("key", key);
return (V) values.get(key);
}
/**
* Returns the packet's name which was given while creating the packet.
*
* @return the packet's name
*/
public String getPacketName() {
return packetName;
}
@Override
public final void marshall(Encoder encoder, Protocol protocol)
throws Exception {
encoder.writeInt32("size", values.size());
for (Map.Entry<String, Object> entry : values.entrySet()) {
encoder.writeString("key", entry.getKey());
encoder.writeObject("value", entry.getValue());
}
marshall0(encoder, protocol);
}
@Override
public final void unmarshall(Decoder decoder, Protocol protocol)
throws Exception {
int size = decoder.readInt32();
for (int i = 0; i < size; i++) {
String key = decoder.readString();
Object value = decoder.readObject();
values.put(key, value);
}
unmarshall0(decoder, protocol);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Packet)) {
return false;
}
Packet packet = (Packet) o;
if (!packetName.equals(packet.packetName)) {
return false;
}
if (!values.equals(packet.values)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = values.hashCode();
result = 31 * result + packetName.hashCode();
return result;
}
@Override
public String toString() {
return "Packet{" + "values=" + values + ", packetName='" + packetName + '\'' + '}';
}
/**
* This method can be used in subclasses to optimize the internal serialization of the packet.
*
* @param encoder the encoder to write values to
* @param protocol the protocol to write additional type information
*/
protected void marshall0(Encoder encoder, Protocol protocol) {
}
/**
* This method can be used in subclasses to optimize the internal deserialization of the packet.
*
* @param decoder the decoder to read values from
* @param protocol the protocol to read additional type information
*/
protected void unmarshall0(Decoder decoder, Protocol protocol) {
}
}