Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IMessage & SimpleMessage (Relaxed Message Sending p1) #921

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions quickfixj-base/src/main/java/quickfix/IMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package quickfix;

import java.time.LocalDateTime;

/**
* Interface for FIX message implementations.
* The standard concrete implementation is {@link Message}
*/
public interface IMessage {

String toRawString();

boolean isAdmin();
String getHeaderString(int field) throws FieldNotFound;
int getHeaderInt(int field) throws FieldNotFound;

void setHeaderString(int field, String value);

void setString(int tag, String value);

void setHeaderInt(int field, int value);

void setInt(int tag, int value);

void setHeaderUtcTimeStamp(int field, LocalDateTime localDateTime, UtcTimestampPrecision timestampPrecision);

boolean isSetField(int field);

boolean getBoolean(int field) throws FieldNotFound;

boolean isSetHeaderField(int field);

int getInt(int tag) throws FieldNotFound;

String getString(int tag) throws FieldNotFound;

void removeHeaderField(int field);

/**
* Provides the first error found while parsing the message
* May indicate the resulting data is only a partial copy of the raw string **/
FieldException getException();
}
27 changes: 26 additions & 1 deletion quickfixj-base/src/main/java/quickfix/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayOutputStream;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.util.List;

/**
* Represents a FIX message.
*/
public class Message extends FieldMap {
public class Message extends FieldMap implements IMessage {

static final long serialVersionUID = -3193357271891865972L;

Expand Down Expand Up @@ -436,6 +437,30 @@ public final Header getHeader() {
return header;
}

public final int getHeaderInt(int field) throws FieldNotFound {
return header.getInt(field);
}

public final void setHeaderInt(int field, int value) {
header.setInt(field, value);
}

public final String getHeaderString(int field) throws FieldNotFound {
return header.getString(field);
}

public final void setHeaderString(int field, String value) {
header.setString(field, value);
}

public final void setHeaderUtcTimeStamp(int field, LocalDateTime value, UtcTimestampPrecision timestampPrecision) {
header.setUtcTimeStamp(field, value, timestampPrecision);
}

public final void removeHeaderField(int field) {
header.removeField(field);
}

public final Trailer getTrailer() {
return trailer;
}
Expand Down
270 changes: 270 additions & 0 deletions quickfixj-base/src/main/java/quickfix/SimpleMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package quickfix;

import quickfix.field.BodyLength;
import quickfix.field.CheckSum;
import quickfix.field.MsgType;
import quickfix.field.SessionRejectReason;
import quickfix.field.converter.BooleanConverter;
import quickfix.field.converter.IntConverter;
import quickfix.field.converter.UtcTimestampConverter;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
* SimpleMessage is designed to allow sending complex messages (e.g. repeating groups) of an arbitrary format *without* parsing the structure.
* Use cases include messages stored and then later sent.
*/
public class SimpleMessage implements IMessage {
private static final List<Integer> STRICT_ORDERING = Arrays.asList(new Integer[]{8, 9, 35});
private static final String SOH = String.valueOf('\001');
private static final String BODY_LENGTH_FIELD = SOH + String.valueOf(BodyLength.FIELD) + '=';
private static final String CHECKSUM_FIELD = SOH + String.valueOf(CheckSum.FIELD) + '=';
private final String messageData;

public static class TagPair {
public final int tag;
public String value;

public TagPair(int tag, String value) {
this.tag = tag;
this.value = value;
}

public String asString() {
return tag+"="+value+SOH;
}
}

private final List<TagPair> fields;

public SimpleMessage(String message) {
messageData = message;
fields = Arrays.stream(message.split("\u0001")).map(p -> {
String[] pairData = p.split("=", 2);
return new TagPair(Integer.parseInt(pairData[0]), pairData[1]);
}).collect(Collectors.toList());
}

public List<TagPair> getFields() {
return fields;
}

@Override
public String toString() {
setHeaderString(BodyLength.FIELD, "100");
setString(10, "000");
StringBuilder messageString = buildMessageString();
setBodyLength(messageString);
setChecksum(messageString);
return messageString.toString();
}

private static void setBodyLength(StringBuilder stringBuilder) {
int bodyLengthIndex = stringBuilder.indexOf(BODY_LENGTH_FIELD, 0);
int sohIndex = stringBuilder.indexOf(SOH, bodyLengthIndex + 1);
int checkSumIndex = stringBuilder.lastIndexOf(CHECKSUM_FIELD);
int length = checkSumIndex - sohIndex;
bodyLengthIndex += BODY_LENGTH_FIELD.length();
stringBuilder.replace(bodyLengthIndex, bodyLengthIndex + 3, NumbersCache.get(length));
}

private static void setChecksum(StringBuilder stringBuilder) {
int checkSumIndex = stringBuilder.lastIndexOf(CHECKSUM_FIELD);
int checkSum = 0;
for(int i = checkSumIndex; i-- != 0;)
checkSum += stringBuilder.charAt(i);
String checkSumValue = NumbersCache.get((checkSum + 1) & 0xFF); // better than sum % 256 since it avoids overflow issues
checkSumIndex += CHECKSUM_FIELD.length();
stringBuilder.replace(checkSumIndex + (3 - checkSumValue.length()), checkSumIndex + 3, checkSumValue);
}

private StringBuilder buildMessageString() {
StringBuilder message = new StringBuilder();
//Print strict order tags
for (Integer integer : STRICT_ORDERING) {
TagPair tagPair = getField(integer);
if (tagPair != null) {
message.append(tagPair.asString());
}
}
//Print unclaimed tags
for (TagPair tagPair : fields) {
if (tagPair.tag != 10 && !STRICT_ORDERING.contains(tagPair.tag)) {
message.append(tagPair.asString());
}
}
//Print footer tag
TagPair footer = getField(10);
if (footer != null) {
message.append(footer.asString());
}
return message;
}

private TagPair getField(int tag) {
for (TagPair field : fields) {
if (field.tag == tag) {
return field;
}
}
return null;
}

@Override
public String toRawString() {
return messageData;
}

@Override
public boolean isAdmin() {
if (isSetHeaderField(MsgType.FIELD)) {
try {
final String msgType = getHeaderString(MsgType.FIELD);
return MessageUtils.isAdminMessage(msgType);
} catch (final FieldNotFound e) {
// shouldn't happen
}
}
return false;
}

@Override
public String getHeaderString(int tag) throws FieldNotFound {
for (TagPair field : fields) {
if (field.tag == tag) {
return field.value;
}
}
throw new FieldNotFound(tag);
}

@Override
public int getHeaderInt(int tag) throws FieldNotFound {
for (TagPair field : fields) {
if (field.tag == tag) {
return Integer.parseInt(field.value);
}
}
throw new FieldNotFound(tag);
}

@Override
public void setHeaderString(int tag, String value) {
for (TagPair field : fields) {
if (field.tag == tag) {
field.value = value;
return;
}
}
fields.add(new TagPair(tag, value));
}

@Override
public void setString(int tag, String value) {
for (TagPair field : fields) {
if (field.tag == tag) {
field.value = value;
return;
}
}
fields.add(new TagPair(tag, value));
}

@Override
public void setHeaderInt(int tag, int value) {
for (TagPair field : fields) {
if (field.tag == tag) {
field.value = Integer.toString(value);
return;
}
}
fields.add(new TagPair(tag, Integer.toString(value)));
}

@Override
public void setInt(int tag, int value) {
for (TagPair field : fields) {
if (field.tag == tag) {
field.value = Integer.toString(value);
return;
}
}
fields.add(new TagPair(tag, Integer.toString(value)));
}

@Override
public void setHeaderUtcTimeStamp(int tag, LocalDateTime dateTime, UtcTimestampPrecision precision) {
for (TagPair field : fields) {
if (field.tag == tag) {
field.value = UtcTimestampConverter.convert(dateTime, precision);
return;
}
}
}

@Override
public boolean isSetHeaderField(int tag) {
for (TagPair field : fields) {
if (field.tag == tag) {
return true;
}
}
return false;
}

@Override
public boolean isSetField(int tag) {
for (TagPair field : fields) {
if (field.tag == tag) {
return true;
}
}
return false;
}

@Override
public boolean getBoolean(int tag) throws FieldNotFound {
try {
return BooleanConverter.convert(getString(tag));
} catch (FieldConvertError e) {
throw newIncorrectDataException(e, tag);
}
}

@Override
public int getInt(int tag) throws FieldNotFound {
try {
return IntConverter.convert(getString(tag));
} catch (FieldConvertError e) {
throw newIncorrectDataException(e, tag);
}
}

@Override
public String getString(int tag) throws FieldNotFound {
for (TagPair field : fields) {
if (field.tag == tag) {
return field.value;
}
}
throw new FieldNotFound(tag);
}

@Override
public void removeHeaderField(int field) {
fields.removeIf(f -> f.tag == field);
}

@Override
public FieldException getException() {
return null;
}

private FieldException newIncorrectDataException(FieldConvertError e, int tag) {
return new FieldException(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE,
e.getMessage(), tag);
}
}
Loading