net.sf.eBus.messages.EMessageObject Maven / Gradle / Ivy
//
// Copyright 2013, 2016, 2019, 2020 Charles W. Rapp
//
// 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 net.sf.eBus.messages;
import java.io.Serializable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.ValidationException;
import net.sf.eBus.util.Validator;
/**
* This abstract class defines the two eBus message classes:
* {@link EMessage} and {@link EField}. Message instances are
* used to publish notifications, requests, and replies. Field
* instances are used to implement user-defined fields within
* both messages and other fields.
*
* Messages and field contain {@code public final} data members.
* If the message or field can be transmitted between eBus
* applications, then the data member type must be one of the
* following:
*
* boolean Boolean BigInteger BigDecimal byte Byte char Character
Class Date double Double Duration EField EFieldList EMessage
EMessageList EMessageKey enum File float Float InetAddress
InetSocketAddress Instant int Integer LocalDate LocalDateTime
LocalTime long Long MonthDay OffsetDateTime OffsetTime Period
short Short String URI YearMonth ZonedDateTime ZoneId ZoneOffset
*
* Any eBus field can be made into a homogenous array by
* appending {@code []} to the field name.
*
*
* This data type restriction does not apply if the
* message or field is marked as {@link ELocalOnly local only}.
* In that case the data members may be any valid Java type.
* Also the following De-serialization requirements do not apply.
* However, local only message and fields may not appear in a
* non-local message or field.
*
* De-serialization
*
* As of eBus release 5.2.0, eBus determines field member
* serialization order based on the field's length. The goal
* being to maintain correct by alignment. Because serialization
* order can no longer be determined up front, {@code EMessage}
* and {@code EField} instances must be re-created using a
* technique not based on field ordering. This is no done using
* the GoF builder paradigm.
*
*
* Consider the following eBus notification message:
*
* public final class EquityQuote
extends ENotificationMessage
implements Serializable
{
// Member fields.
public final PriceType priceType; // PriceType is an enum
public final PriceSize bid; // PriceSize is an EField subclass
public final PriceSize ask;
private static final long serialVersionUID = 1L; // Defined for Serializable
// Remaining code will be added by each step.
}
* Step 1: builder method
*
* Every non-local {@code EMessage} and {@code EField} must
* define a {@code public static builder} method which returns a
* builder instance associated with the message or field.
* The builder class name is left up to the developer. In this
* example the class is simply {@code Builder}:
*
* public static Builder builder() {
return (new Builder());
}
*
* It is recommended that this {@code builder} method be the only
* way to create a {@code EquityQuote.Builder} instance.
*
* Step 2: Declare builder class
* public static final class Builder
extends ENotificationMessage.Builder<EquityQuote, Builder>
{
// This code filled in by the following steps.
}
*
* The builder class must extend the target message's
* base class builder. In this example, since {@code EquityQuote}
* extends {@code ENotificationMessage}, {@code Builder} must
* extend {@link ENotificationMessage.Builder}. If the message
* extends request or reply, then the builder would be required
* to extend the request or reply builder, respectively.
*
*
* {@code ENotification.Builder} generic class parameters
* {@code M} and {@code B} (which all super class Builder classes
* define) are the target message class and the target builder
* class, respectively. The first is needed for the superclass
* {@code public final M build()} method used to create the
* target message instance. The second generic parameter is used
* by super class setter methods (set Step 5 for more about
* setters).
*
*
* The builder inner class must be {@code public static} because
* classes outside this package will need to access the class and
* {@code static} because the builder instance must in no way be
* dependent on the target message class containing it.
*
* Step 3: Builder fields
private PriceType mPriceType;
private PriceSize bid;
private PriceSize ask;
*
* The builder class must have exactly the same number of fields
* as the target message with the same data types except
* this time these fields are {@code private}, non-{@code final}.
* {@code private} because only the encapsulating target class is
* allowed to access these fields. Non-{@code final} because the
* field values will be set post-construction.
*
* Step 4: Builder constructor
*
* A builder should have only one {@code private}, no argument
* constructor because only {@code EquityQuote builder} method
* should be allowed to construct this builder:
*
* private Builder() {
// Superclass constructor requires target class.
super (EquityQuote.class);
// Set fields to defaults if necessary.
}
* Step 5: Setter methods
*
* For each {@code public final} target message field the builder
* must define a setter method whose name
* exactly matches the field name and whose sole
* parameter has exactly matches the field data type.
* The setter return type is the builder class which allows for
* setter method chaining:
* {@code builder.height(1).weight(2).age(3)}. This is the reason
* for the second class parameter {@code B}. If a message field
* is defined in the superclass, then the superclass builder is
* responsible for defining the setter method. But if the
* superclass setter returns the superclass type, then setter
* chain breaks. The solution is for superclass setters to return
* type {@code B}. While this requires a downcast it will work as
* long as {@code B} was set correctly.
*
*
* In this example {@code EquityQuote.Builder} has the following
* setters:
*
* public Builder priceType(final PriceType pt) { ... };
public Builder bid(final PriceSize ps) { ... };
public Builder ask(final PriceSize ps) { ... };
*
* How the setter is defined depends on what argument validation
* is required. If no validation is done, then the setter can
* set the field value and return. If validation is done, then
* an invalid argument should result in a thrown exception.
*
* public Builder bid(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mBid = ps;
return (this);
}
*
* If validation is performed then that validation must be
* independent of the other data members and focus on the setter
* argument but itself. This is due to the unknown ordering of
* the setter method calls. When a setter is called, there is
* no way of knowing which builder fields are set, if any. If
* inter-field validation is required, then see Step 6 to learn
* how a builder can implement this requirement.
*
* Step 6: Builder validation
*
* Before the target {@code EMessage} or {@code EField} is
* instantiated, builder member fields may be checked for
* correctness. Application code has finished calling the setters
* and called {@link EMessageObject.Builder#build}. This method
* then calls
* {@link EMessageObject.Builder#validate(Validator)}
* to determine if the builder is correctly configured. The
* {@code validate} method should not throw an exception
* but rather collects errors in the {@link Validator problems}
* explaining which fields are not set correctly and why. Upon
* return an empty {@code Validator} means that the builder is
* properly configured and the target message can now be built. A
* non-empty {@code Validator} results in a
* {@link ValidationException} being thrown which contains the
* problems list.
*
*
* When overriding {@code validate} use a method chain which
* starts with {@code super.validate(problems)} and is followed
* by a {@code Validate.require} method call for each field
* needing to be checked. The point here is that all such fields
* should be tested allowing all invalid settings to be reported
* in one go. This helps avoid the debugging
* "infinite loop testing" where only one problem is reported at
* a time. When that problem is corrected, the next problem is
* reported. This continues on until all problems are corrected.
* By reporting all problems at one time, the developer corrects
* them in one go and the testing moves on beyond this point.
*
*
* The following code shows how validation may be implemented.
*
* @Override public Validator validate(final Validator problems) {
// Call super.validate(problems) first to perform superclass validation.
return (super.validate(problems)
// priceType field must be set.
.requireNotNull(mPriceType, "priceType")
// One-sided bids are allowed.
// Note: test returns true if-and-only-if v1 and v2 are valid.
.requireTrue((v1, v2) -> (v1 != null || v2 != null),
mBid, // v1
mAsk, // v2
"bid", // v1 field name
"ask", // v2 field name
"bid and ask both not set") // error message
// Bid price must be less than ask price. PriceSize implements Comparable.
// Be sure this is a two-sided quote which means bid and ask are both not null.
// This test is run even if the previous test fails.
.requireTrue((v1, v2) -> (v1 != null && v2 != null && mBid.compareTo(mAsk) < 0),
mBid, // v1
mAsk, // v2
"bid", // v1 field name
"ask", // v2 field name
"bid ≥ ask")); // error message
}
*
* Please note that {@code super.validate(problems)} must be
* called so that superclass can perform its field validation.
*
*
* {@code Validator.requireTrue} supports one and two argument
* predicates only. If three or more fields are required for
* inter-field validation, then that validation test should be
* done in a separate method which returns a
* {@code MultiKey<String, String>} error pair and added
* to the error list via {@link Validator#addError(MultiKey2)}.
* If the validation method returns {@code null} then
* {@code addError} ignores it and does nothing. An example
* implementation follows:
*
* // Checks if the equity stock order report's cumulative and leaves quantities equals the total order size.
private MultiKey2<String, String> checkQuantities() {
return ((mLeavesQty > 0 && (mCumQty + mLeavesQty != mQty) ?
new MultiKey2<>("cumulative quantity, remaining quantity", "cumulative quantity + remaining quantity != quantity") :
null));
}
Add the quantities check to the validaor method chain:
return (super.validate(problems)
.addError(checkQuantities));
* Step 7: Build target
*
* The abstract method the builder class is required to override
* is {@code buildImpl}. This class returns the target class
* instance built from the configured fields.
*
* @Override protected EquityQuote buildImpl() {
return (new EquityQuote(this));
}
*
* {@code buildImpl} is called after {@code validate} shows no
* problems with the builder configuration.
*
* Step 8: Target class constructor
*
* The final link connecting the target class and target class
* builder is the target class constructor:
*
* private EquityQuote(final Builder builder) {
super (builder);
this.priceType = builder.mPriceType;
this.bid = builder.mBid;
this.ask = builder.mAsk;
}
*
* This constructor is {@code private} because only
* {@code EquityQuote.Builder.buildImpl} has access to this
* constructor. Also note that since {@code Builder} is an
* {@code EquityQuote} inner class, the constructor can access
* {@code Builder} member fields directly, no getter methods are
* required.
*
* Step 9: Complete message class definition
* public final class EquityQuote
extends ENotificationMessage
implements Serializable
{
public final PriceType priceType;
public final PriceSize bid;
public final PriceSize ask;
private static final long serialVersionUID = 1L;
private EquityQuote(final Builder builder) {
super (builder);
this.priceType = builder.mPriceType;
this.bid = builder.mBid;
this.ask = builder.mAsk;
}
public static Builder builder() {
return (new Builder());
}
public static final class Builder
extends ENotificationMessage.Builder<EquityQuote, Builder>
{
private PriceType mPriceType;
private PriceSize mBid;
private PriceSize mAsk;
private Builder() {
super (EquityQuote.class);
}
public Builder priceType(final PriceType pt) {
if (pt == null) {
throw (new NullPointerException("pt is null"));
}
mPriceType = pt;
return (this);
}
public Builder bid(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mBid = ps;
return (this);
}
public Builder ask(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mAsk = ps;
return (this);
}
@Override public Validator validate(final Validator problems) {
return (super.validate(problems)
.requireNotNull(mPriceType, "priceType")
// One-sided bids are allowed.
.requireTrue((v1, v2) -> (v1 != null || v2 != null), mBid, mAsk, "bid", "ask", "bid and ask both not set") // error message
// Bid price must be less than ask price. PriceSize implements Comparable.
.requireTrue((v1, v2) -> (v1 != null && v2 != null && mBid.compareTo(mAsk) < 0), mBid, mAsk, "bid", "ask", "bid ≥ ask"));
}
@Override protected EquityQuote buildImpl() {
return (new EquityQuote(this));
}
}
}
*
* The above example uses a {@code final} message class. But what
* if the {@code EMessage} or {@code EField} class may be
* extended? See {@link EField} for an example of how to set up
* builders for a non-{@code final} class.
*
*
* @author Charles Rapp
*/
public abstract class EMessageObject
implements Serializable
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* Serialization version identifier.
*/
private static final long serialVersionUID = 0x050200L;
//-----------------------------------------------------------
// Statics.
//
/**
* Maps the {@code EMessageObject} class to its validation
* flag: {@code null} means the class has not been validated,
* {@code true} means the class is valid, and {@code false}
* means invalid.
*/
private static final ConcurrentMap mValidMap =
new ConcurrentHashMap<>();
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new message object instance. The point behind
* this constructor is to validate a user-defined message
* or field as soon as it is instantiated.
* @throws InvalidMessageException
* if the user-defined message or field is invalid.
*/
protected EMessageObject()
throws InvalidMessageException
{
final Class extends EMessageObject> mc =
this.getClass();
final String mcName = mc.getName();
ValidationInfo info = mValidMap.get(mcName);
// Has this message been validated before?
if (info == null)
{
// No, validate it now and store away the results.
info = validate(mc);
mValidMap.put(mcName, info);
}
if (!info.isValid())
{
throw (
new InvalidMessageException(
mc, info.reason(), info.error()));
}
} // end of EMessageObject()
protected EMessageObject(final Builder> builder)
{}
//
// end of Constructors.
//-----------------------------------------------------------
/**
* Returns the validation information for the specified
* {@code EMessageObject} sub-class.
* @param mc validate this message class.
* @return the validation result.
*/
private static ValidationInfo validate(final Class extends EMessageObject> mc)
{
boolean flag = true;
String reason = null;
Throwable t = null;
try
{
// Called for effect only. This will create a
// MessageType for this class. If mc is not a valid
// message, then an InvalidMessageException is
// thrown with the appropriate reason.
DataType.findType(mc);
}
catch (InvalidMessageException msgex)
{
flag = false;
reason = msgex.getMessage();
t = msgex;
}
return (new ValidationInfo(flag, reason, t));
} // end of validate(Class)
//---------------------------------------------------------------
// Inner classes.
//
/**
* Base class for all {@link EMessageObject} builders. Used
* by eBus when de-serializing an encoded message back into
* the target message object.
*
* @param builds this target message class.
*/
@SuppressWarnings ("unchecked")
public abstract static class Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Instance message class returned by {@link #build}.
* This value is used to identify the target class when
* a build failure occurs, placed into the
* {@link ValidationException}.
*/
protected final Class extends EMessageObject> mTargetClass;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
*
* @param targetClass
*/
protected Builder(final Class extends EMessageObject> targetClass)
{
mTargetClass = targetClass;
} // end of Builder(Class)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Returns eBus message instance built from the
* configured properties. Note: the
* builder configuration was
* {@link #validate(Validator) validated} prior to
* calling this method. The message object may now be
* constructed.
* @return target message class instance.
*/
protected abstract M buildImpl();
/**
* Checks if message subject and message type are
* configured. If not, then text explaining this error
* is appended to {@code problems}.
*
* This method should be overridden by subclass message
* builders and called before doing its own
* validation. The first line in the subclass
* {@code validate} implementation should be
* {@code super.validate(problems);}.
*
*
* When overriding this method, be sure to add all
* discovered validation problems to the list. The
* validation method should consist of a series of
* individual {@code if} statements and not
* an {@code if/else if} chain. That way all problems
* are found and not just the first one.
*
* @param problems used to check field validation and
* collect names of invalid fields and text explaining
* why each field is invalid.
* @return {@code problems} to allow for method chaining.
*/
protected abstract Validator validate(final Validator problems);
//
// end of Abstract Method Declarations.
//-------------------------------------------------------
/**
* Returns the target message instance constructed from
* the configured properties. This must be the final
* method in a builder configuration call chain.
* @return target message instance.
* @throws ValidationException
* if {@code this} builder does not contain a valid
* target message configuration. This exception contains
* a list of all validation problems found.
*/
public final M build()
throws ValidationException
{
final Validator problems = new Validator();
// Check if the builder contains a valid message or
// field.
validate(problems);
// Were any problems found?
if (!problems.isEmpty())
{
// Yes, can't have that.
throw (
new ValidationException(
mTargetClass, problems.errors()));
}
return (buildImpl());
} // end of build()
} // end of class Builder
/**
* This immutable class tracks the EMessageObject validity.
* Contains reason why a message object does not meet eBus
* message format requirements.
*
* Note: this validation has nothing to do
* with message building but with message compiling.
*
*/
private static final class ValidationInfo
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* {@code true} if the message is valid and
* {@code false} otherwise.
*/
private final boolean mFlag;
/**
* Text explaining why {@code _flag} is {@code false}.
* Set to {@code null} when {@code _flag} is
* {@code true}.
*/
private final String mReason;
/**
* Thrown error which caused the exception.
*/
private final Throwable mError;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Sets the message object validation to the given flag
* and reason.
* @param flag {@code true} if the message object is
* valid and {@code false} otherwise.
* @param reason text explaining the invalidity.
* @param t error behind a {@code false flag}.
*/
private ValidationInfo(final boolean flag,
final String reason,
final Throwable t)
{
mFlag = flag;
mReason = reason;
mError = t;
} // end of ValidationInfo(boolean, String, Throwable)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if the message object is valid
* and {@code false} otherwise.
* @return {@code true} if the message object is valid.
*/
private boolean isValid()
{
return (mFlag);
} // end of isValid()
/**
* Returns text explaining why the message is invalid.
* May return {@code null}.
* @return text explaining why the message is invalid.
*/
private String reason()
{
return (mReason);
} // end of reason()
/**
* Returns the error behind an invalid message.
* @return {@code Throwable} error.
*/
private Throwable error()
{
return (mError);
} // end of error()
//
// end of Get Methods.
//-------------------------------------------------------
} // end of class ValidationInfo
} // end of class EMessageObject