org.opendaylight.controller.cluster.access.concepts.Message Maven / Gradle / Ivy
/*
* Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.controller.cluster.access.concepts;
import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import java.io.DataInput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.controller.cluster.access.ABIVersion;
import org.opendaylight.yangtools.concepts.Immutable;
import org.opendaylight.yangtools.concepts.WritableIdentifier;
import org.opendaylight.yangtools.concepts.WritableObjects;
/**
* An abstract concept of a Message. This class cannot be instantiated directly, use its specializations {@link Request}
* and {@link Response}.
*
*
* Messages have a target and a sequence number. Sequence numbers are expected to be assigned monotonically on a
* per-target basis, hence two targets can observe the same sequence number.
*
*
* This class includes explicit versioning for forward- and backward- compatibility of serialization format. This is
* achieved by using the serialization proxy pattern. Subclasses are in complete control of what proxy is used to
* serialize a particular object on the wire. This class can serve as an explicit version marker, hence no further
* action is necessary in the deserialization path.
*
*
* For the serialization path an explicit call from the user is required to select the appropriate serialization
* version. This is done via {@link #toVersion(ABIVersion)} method, which should return a copy of this object with
* the requested ABI version recorded and should return the appropriate serialization proxy.
*
*
* This workflow allows least disturbance across ABI versions, as all messages not affected by a ABI version bump
* will remain working with the same serialization format for the new ABI version.
*
*
* Note that this class specifies the {@link Immutable} contract, which means that all subclasses must follow this API
* contract.
*
* @param Target identifier type
* @param Message type
*/
public abstract class Message>
implements Immutable, Serializable {
/**
* Externalizable proxy for use with {@link Message} subclasses.
*
* @param Target identifier type
* @param Message class
*/
protected interface SerialForm> extends Externalizable {
@NonNull C message();
void setMessage(@NonNull C message);
@Override
default void writeExternal(final ObjectOutput out) throws IOException {
final var message = message();
message.getTarget().writeTo(out);
WritableObjects.writeLong(out, message.getSequence());
writeExternal(out, message);
}
void writeExternal(@NonNull ObjectOutput out, @NonNull C msg) throws IOException;
@Override
default void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
final var target = verifyNotNull(readTarget(in));
final var sequence = WritableObjects.readLong(in);
setMessage(verifyNotNull(readExternal(in, target, sequence)));
}
@NonNull C readExternal(@NonNull ObjectInput in, @NonNull T target, long sequence)
throws IOException, ClassNotFoundException;
Object readResolve();
@NonNull T readTarget(@NonNull DataInput in) throws IOException;
}
@java.io.Serial
private static final long serialVersionUID = 1L;
private final @NonNull ABIVersion version;
private final long sequence;
private final @NonNull T target;
private Message(final ABIVersion version, final T target, final long sequence) {
this.target = requireNonNull(target);
this.version = requireNonNull(version);
this.sequence = sequence;
}
Message(final T target, final long sequence) {
this(ABIVersion.current(), target, sequence);
}
Message(final C msg, final ABIVersion version) {
this(version, msg.getTarget(), msg.getSequence());
}
/**
* Get the target identifier for this message.
*
* @return Target identifier
*/
public final @NonNull T getTarget() {
return target;
}
/**
* Get the logical sequence number.
*
* @return logical sequence number
*/
public final long getSequence() {
return sequence;
}
@VisibleForTesting
public final @NonNull ABIVersion getVersion() {
return version;
}
/**
* Return a message which will end up being serialized in the specified {@link ABIVersion}.
*
* @param toVersion Request {@link ABIVersion}
* @return A new message which will use ABIVersion as its serialization.
*/
@SuppressWarnings("unchecked")
public final @NonNull C toVersion(final @NonNull ABIVersion toVersion) {
if (version == toVersion) {
return (C)this;
}
return switch (toVersion) {
case POTASSIUM -> verifyNotNull(cloneAsVersion(toVersion));
default -> throw new IllegalArgumentException("Unhandled ABI version " + toVersion);
};
}
/**
* Create a copy of this message which will serialize to a stream corresponding to the specified method. This
* method should be implemented by the concrete final message class and should invoke the equivalent of
* {@link #Message(Message, ABIVersion)}.
*
* @param targetVersion target ABI version
* @return A message with the specified serialization stream
* @throws IllegalArgumentException if this message does not support the target ABI
*/
protected abstract @NonNull C cloneAsVersion(@NonNull ABIVersion targetVersion);
@Override
public final String toString() {
return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
}
/**
* Add attributes to the output of {@link #toString()}. Subclasses wanting to contribute additional information
* should override this method. Any null attributes will be omitted from the output.
*
* @param toStringHelper a {@link ToStringHelper} instance
* @return The {@link ToStringHelper} passed in as argument
* @throws NullPointerException if toStringHelper is null
*/
protected @NonNull ToStringHelper addToStringAttributes(final @NonNull ToStringHelper toStringHelper) {
return toStringHelper.add("target", target).add("sequence", Long.toUnsignedString(sequence));
}
/**
* Instantiate a serialization proxy for this object for the target ABI version. Implementations should return
* different objects for incompatible {@link ABIVersion}s. This method should never fail, as any compatibility
* checks should have been done by {@link #cloneAsVersion(ABIVersion)}.
*
* @param reqVersion Requested ABI version
* @return Proxy for this object
*/
protected abstract @NonNull SerialForm externalizableProxy(@NonNull ABIVersion reqVersion);
@java.io.Serial
protected final Object writeReplace() {
return externalizableProxy(version);
}
protected final void throwNSE() throws NotSerializableException {
throw new NotSerializableException(getClass().getName());
}
@java.io.Serial
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
throwNSE();
}
@java.io.Serial
private void readObjectNoData() throws ObjectStreamException {
throwNSE();
}
@java.io.Serial
private void writeObject(final ObjectOutputStream stream) throws IOException {
throwNSE();
}
}