messageFactories = new ConcurrentHashMap<>();
+
+ private final ApplVerID defaultApplVerID;
/**
* Constructs a DefaultMessageFactory, which dynamically loads and delegates to
@@ -39,8 +51,28 @@ public class DefaultMessageFactory implements MessageFactory {
* Callers can set the {@link Thread#setContextClassLoader context classloader},
* which will be used to load the classes if {@link Class#forName Class.forName}
* fails to do so (e.g. in an OSGi environment).
+ *
+ * Equivalent to {@link #DefaultMessageFactory(String) DefaultMessageFactory}({@link ApplVerID#FIX50 ApplVerID.FIX50}).
*/
public DefaultMessageFactory() {
+ this(ApplVerID.FIX50);
+ }
+
+ /**
+ * Constructs a DefaultMessageFactory, which dynamically loads and delegates to
+ * the default version-specific message factories, if they are available at runtime.
+ *
+ * Callers can set the {@link Thread#setContextClassLoader context classloader},
+ * which will be used to load the classes if {@link Class#forName Class.forName}
+ * fails to do so (e.g. in an OSGi environment).
+ *
+ * @param defaultApplVerID ApplVerID value used by default for {@link #create(String, ApplVerID, String)}
+ */
+ public DefaultMessageFactory(String defaultApplVerID) {
+ Objects.requireNonNull(defaultApplVerID, "defaultApplVerID");
+
+ this.defaultApplVerID = new ApplVerID(defaultApplVerID);
+
// To loosen the coupling between this factory and generated code, the
// message factories are discovered at run time using reflection
addFactory(BEGINSTRING_FIX40);
@@ -110,27 +142,23 @@ public void addFactory(String beginString, Class extends MessageFactory> facto
}
}
+ @Override
public Message create(String beginString, String msgType) {
+ return create(beginString, defaultApplVerID, msgType);
+ }
+
+ @Override
+ public Message create(String beginString, ApplVerID applVerID, String msgType) {
MessageFactory messageFactory = messageFactories.get(beginString);
- if (beginString.equals(BEGINSTRING_FIXT11)) {
- // The default message factory assumes that only FIX 5.0 will be
- // used with FIXT 1.1 sessions. A more flexible approach will require
- // an extension to the QF JNI API. Until then, you will need a custom
- // message factory if you want to use application messages prior to
- // FIX 5.0 with a FIXT 1.1 session.
- //
- // TODO: how do we support 50/50SP1/50SP2 concurrently?
- //
- // If you need to determine admin message category based on a data
- // dictionary, then use a custom message factory and don't use the
- // static method used below.
- if (!MessageUtils.isAdminMessage(msgType)) {
- messageFactory = messageFactories.get(FIX50);
+ if (beginString.equals(BEGINSTRING_FIXT11) && !MessageUtils.isAdminMessage(msgType)) {
+ if (applVerID == null) {
+ applVerID = new ApplVerID(defaultApplVerID.getValue());
}
+ messageFactory = messageFactories.get(MessageUtils.toBeginString(applVerID));
}
if (messageFactory != null) {
- return messageFactory.create(beginString, msgType);
+ return messageFactory.create(beginString, applVerID, msgType);
}
Message message = new Message();
diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java
index 50943cbb3..c7c7ba3a1 100644
--- a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java
@@ -19,26 +19,35 @@
package quickfix;
+import org.quickfixj.QFJException;
+import org.quickfixj.SimpleCache;
+import quickfix.field.ApplVerID;
+import quickfix.field.DefaultApplVerID;
+
import java.net.InetAddress;
+import java.util.Arrays;
import java.util.Enumeration;
-import java.util.Hashtable;
-import java.util.Map;
import java.util.Properties;
import java.util.Set;
-import quickfix.field.ApplVerID;
-import quickfix.field.DefaultApplVerID;
-
/**
* Factory for creating sessions. Used by the communications code (acceptors,
* initiators) for creating sessions.
*/
public class DefaultSessionFactory implements SessionFactory {
- private static final Map dictionaryCache = new Hashtable();
+ private static final SimpleCache dictionaryCache = new SimpleCache<>(path -> {
+ try {
+ return new DataDictionary(path);
+ } catch (ConfigError e) {
+ throw new QFJException(e);
+ }
+ });
+
private final Application application;
private final MessageStoreFactory messageStoreFactory;
private final LogFactory logFactory;
private final MessageFactory messageFactory;
+ private final SessionScheduleFactory sessionScheduleFactory;
public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory,
LogFactory logFactory) {
@@ -46,6 +55,7 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag
this.messageStoreFactory = messageStoreFactory;
this.logFactory = logFactory;
this.messageFactory = new DefaultMessageFactory();
+ this.sessionScheduleFactory = new DefaultSessionScheduleFactory();
}
public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory,
@@ -54,6 +64,17 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag
this.messageStoreFactory = messageStoreFactory;
this.logFactory = logFactory;
this.messageFactory = messageFactory;
+ this.sessionScheduleFactory = new DefaultSessionScheduleFactory();
+ }
+
+ public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory,
+ LogFactory logFactory, MessageFactory messageFactory,
+ SessionScheduleFactory sessionScheduleFactory) {
+ this.application = application;
+ this.messageStoreFactory = messageStoreFactory;
+ this.logFactory = logFactory;
+ this.messageFactory = messageFactory;
+ this.sessionScheduleFactory = sessionScheduleFactory;
}
public Session create(SessionID sessionID, SessionSettings settings) throws ConfigError {
@@ -137,8 +158,8 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf
Session.SETTING_TEST_REQUEST_DELAY_MULTIPLIER,
Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER);
- final boolean millisInTimestamp = getSetting(settings, sessionID,
- Session.SETTING_MILLISECONDS_IN_TIMESTAMP, true);
+ final UtcTimestampPrecision timestampPrecision = getTimestampPrecision(settings, sessionID,
+ UtcTimestampPrecision.MILLIS);
final boolean resetOnLogout = getSetting(settings, sessionID,
Session.SETTING_RESET_ON_LOGOUT, false);
@@ -180,9 +201,11 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf
final int[] logonIntervals = getLogonIntervalsInSeconds(settings, sessionID);
final Set allowedRemoteAddresses = getInetAddresses(settings, sessionID);
+ final SessionSchedule sessionSchedule = sessionScheduleFactory.create(sessionID, settings);
+
final Session session = new Session(application, messageStoreFactory, sessionID,
- dataDictionaryProvider, new SessionSchedule(settings, sessionID), logFactory,
- messageFactory, heartbeatInterval, checkLatency, maxLatency, millisInTimestamp,
+ dataDictionaryProvider, sessionSchedule, logFactory,
+ messageFactory, heartbeatInterval, checkLatency, maxLatency, timestampPrecision,
resetOnLogon, resetOnLogout, resetOnDisconnect, refreshAtLogon, checkCompID,
redundantResentRequestAllowed, persistMessages, useClosedIntervalForResend,
testRequestDelayMultiplier, senderDefaultApplVerID, validateSequenceNumbers,
@@ -320,13 +343,14 @@ private String toDictionaryPath(String beginString) {
}
private DataDictionary getDataDictionary(String path) throws ConfigError {
- synchronized (dictionaryCache) {
- DataDictionary dataDictionary = dictionaryCache.get(path);
- if (dataDictionary == null) {
- dataDictionary = new DataDictionary(path);
- dictionaryCache.put(path, dataDictionary);
+ try {
+ return dictionaryCache.computeIfAbsent(path);
+ } catch (QFJException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof ConfigError) {
+ throw (ConfigError) cause;
}
- return dataDictionary;
+ throw e;
}
}
@@ -377,4 +401,18 @@ private double getSetting(SessionSettings settings, SessionID sessionID, String
: defaultValue;
}
+ private UtcTimestampPrecision getTimestampPrecision(SessionSettings settings, SessionID sessionID,
+ UtcTimestampPrecision defaultValue) throws ConfigError, FieldConvertError {
+ if (settings.isSetting(sessionID, Session.SETTING_TIMESTAMP_PRECISION)) {
+ String string = settings.getString(sessionID, Session.SETTING_TIMESTAMP_PRECISION);
+ try {
+ return UtcTimestampPrecision.valueOf(string);
+ } catch (IllegalArgumentException e) {
+ throw new ConfigError(e.getMessage() + ". Valid values: " + Arrays.toString(UtcTimestampPrecision.values()));
+ }
+ } else {
+ return defaultValue;
+ }
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java
new file mode 100644
index 000000000..83d8bb5ea
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java
@@ -0,0 +1,407 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Corresponds to SessionTime in C++ code
+ */
+public class DefaultSessionSchedule implements SessionSchedule {
+ private static final int NOT_SET = -1;
+ private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{2}):(\\d{2}):(\\d{2})(.*)");
+ private final TimeEndPoint startTime;
+ private final TimeEndPoint endTime;
+ private final boolean isNonStopSession;
+ private final boolean isWeekdaySession;
+ private final int[] weekdayOffsets;
+ protected static final Logger LOG = LoggerFactory.getLogger(DefaultSessionSchedule.class);
+
+ public DefaultSessionSchedule(SessionSettings settings, SessionID sessionID) throws ConfigError,
+ FieldConvertError {
+
+ isNonStopSession = settings.isSetting(sessionID, Session.SETTING_NON_STOP_SESSION) && settings.getBool(sessionID, Session.SETTING_NON_STOP_SESSION);
+ TimeZone defaultTimeZone = getDefaultTimeZone(settings, sessionID);
+ if (isNonStopSession) {
+ isWeekdaySession = false;
+ weekdayOffsets = new int[0];
+ startTime = endTime = new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone);
+ return;
+ } else {
+ isWeekdaySession = settings.isSetting(sessionID, Session.SETTING_WEEKDAYS);
+ }
+
+ boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY);
+ boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY);
+
+ if (isWeekdaySession) {
+ if (startDayPresent || endDayPresent )
+ throw new ConfigError("Session " + sessionID + ": usage of StartDay or EndDay is not compatible with setting " + Session.SETTING_WEEKDAYS);
+
+ String weekdayNames = settings.getString(sessionID, Session.SETTING_WEEKDAYS);
+ if (weekdayNames.isEmpty())
+ throw new ConfigError("Session " + sessionID + ": " + Session.SETTING_WEEKDAYS + " is empty");
+
+ String[] weekdayNameArray = weekdayNames.split(",");
+ weekdayOffsets = new int[weekdayNameArray.length];
+ for (int i = 0; i < weekdayNameArray.length; i++) {
+ weekdayOffsets[i] = DayConverter.toInteger(weekdayNameArray[i]);
+ }
+ } else {
+ weekdayOffsets = new int[0];
+
+ if (startDayPresent && !endDayPresent) {
+ throw new ConfigError("Session " + sessionID + ": StartDay used without EndDay");
+ }
+
+ if (endDayPresent && !startDayPresent) {
+ throw new ConfigError("Session " + sessionID + ": EndDay used without StartDay");
+ }
+ }
+ startTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_START_TIME, Session.SETTING_START_DAY);
+ endTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_END_TIME, Session.SETTING_END_DAY);
+ LOG.info("[{}] {}", sessionID, toString());
+ }
+
+ private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID sessionID,
+ TimeZone defaultTimeZone, String timeSetting, String daySetting) throws ConfigError,
+ FieldConvertError {
+
+ Matcher matcher = TIME_PATTERN.matcher(settings.getString(sessionID, timeSetting));
+ if (!matcher.find()) {
+ throw new ConfigError("Session " + sessionID + ": could not parse time '"
+ + settings.getString(sessionID, timeSetting) + "'.");
+ }
+
+ return new TimeEndPoint(
+ getDay(settings, sessionID, daySetting, NOT_SET),
+ Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)),
+ Integer.parseInt(matcher.group(3)), getTimeZone(matcher.group(4), defaultTimeZone));
+ }
+
+ private TimeZone getDefaultTimeZone(SessionSettings settings, SessionID sessionID)
+ throws ConfigError, FieldConvertError {
+ TimeZone sessionTimeZone;
+ if (settings.isSetting(sessionID, Session.SETTING_TIMEZONE)) {
+ String sessionTimeZoneID = settings.getString(sessionID, Session.SETTING_TIMEZONE);
+ sessionTimeZone = TimeZone.getTimeZone(sessionTimeZoneID);
+ if ("GMT".equals(sessionTimeZone.getID()) && !"GMT".equals(sessionTimeZoneID)) {
+ throw new ConfigError("Unrecognized time zone '" + sessionTimeZoneID
+ + "' for session " + sessionID);
+ }
+ } else {
+ sessionTimeZone = TimeZone.getTimeZone("UTC");
+ }
+ return sessionTimeZone;
+ }
+
+ private TimeZone getTimeZone(String tz, TimeZone defaultZone) {
+ return "".equals(tz) ? defaultZone : TimeZone.getTimeZone(tz.trim());
+ }
+
+ private static class TimeEndPoint {
+ private final int weekDay;
+ private final int hour;
+ private final int minute;
+ private final int second;
+ private final int timeInSeconds;
+ private final TimeZone tz;
+
+ public TimeEndPoint(int day, int hour, int minute, int second, TimeZone tz) {
+ weekDay = day;
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+ this.tz = tz;
+ timeInSeconds = timeInSeconds(hour, minute, second);
+ }
+
+ int getHour() {
+ return hour;
+ }
+
+ int getMinute() {
+ return minute;
+ }
+
+ int getSecond() {
+ return second;
+ }
+
+ int getDay() {
+ return weekDay;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof TimeEndPoint) {
+ TimeEndPoint otherTime = (TimeEndPoint) o;
+ return timeInSeconds == otherTime.timeInSeconds;
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ assert false : "hashCode not supported";
+ return 0;
+ }
+
+ TimeZone getTimeZone() {
+ return tz;
+ }
+ }
+
+ /**
+ * find the most recent session date/time range on or before t
+ * if t is in a session then that session will be returned
+ * @param t specific date/time
+ * @return relevant session date/time range
+ */
+ private TimeInterval theMostRecentIntervalBefore(Calendar t) {
+ TimeInterval timeInterval = new TimeInterval();
+ Calendar intervalStart = timeInterval.getStart();
+ intervalStart.setTimeZone(startTime.getTimeZone());
+ intervalStart.setTimeInMillis(t.getTimeInMillis());
+ intervalStart.set(Calendar.HOUR_OF_DAY, startTime.getHour());
+ intervalStart.set(Calendar.MINUTE, startTime.getMinute());
+ intervalStart.set(Calendar.SECOND, startTime.getSecond());
+ intervalStart.set(Calendar.MILLISECOND, 0);
+
+ Calendar intervalEnd = timeInterval.getEnd();
+ intervalEnd.setTimeZone(endTime.getTimeZone());
+ intervalEnd.setTimeInMillis(t.getTimeInMillis());
+ intervalEnd.set(Calendar.HOUR_OF_DAY, endTime.getHour());
+ intervalEnd.set(Calendar.MINUTE, endTime.getMinute());
+ intervalEnd.set(Calendar.SECOND, endTime.getSecond());
+ intervalEnd.set(Calendar.MILLISECOND, 0);
+
+ if (isWeekdaySession) {
+ while (intervalStart.getTimeInMillis() > t.getTimeInMillis() ||
+ !validDayOfWeek(intervalStart)) {
+ intervalStart.add(Calendar.DAY_OF_WEEK, -1);
+ intervalEnd.add(Calendar.DAY_OF_WEEK, -1);
+ }
+
+ if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) {
+ intervalEnd.add(Calendar.DAY_OF_WEEK, 1);
+ }
+
+ } else {
+ if (isSet(startTime.getDay())) {
+ intervalStart.set(Calendar.DAY_OF_WEEK, startTime.getDay());
+ if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) {
+ intervalStart.add(Calendar.WEEK_OF_YEAR, -1);
+ intervalEnd.add(Calendar.WEEK_OF_YEAR, -1);
+ }
+ } else if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) {
+ intervalStart.add(Calendar.DAY_OF_YEAR, -1);
+ intervalEnd.add(Calendar.DAY_OF_YEAR, -1);
+ }
+
+ if (isSet(endTime.getDay())) {
+ intervalEnd.set(Calendar.DAY_OF_WEEK, endTime.getDay());
+ if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) {
+ intervalEnd.add(Calendar.WEEK_OF_MONTH, 1);
+ }
+ } else if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) {
+ intervalEnd.add(Calendar.DAY_OF_WEEK, 1);
+ }
+ }
+
+ return timeInterval;
+ }
+
+ private static class TimeInterval {
+ private final Calendar start = SystemTime.getUtcCalendar();
+ private final Calendar end = SystemTime.getUtcCalendar();
+
+ boolean isContainingTime(Calendar t) {
+ return t.compareTo(start) >= 0 && t.compareTo(end) <= 0;
+ }
+
+ public String toString() {
+ return start.getTime() + " --> " + end.getTime();
+ }
+
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof TimeInterval)) {
+ return false;
+ }
+ TimeInterval otherInterval = (TimeInterval) other;
+ return start.equals(otherInterval.start) && end.equals(otherInterval.end);
+ }
+
+ public int hashCode() {
+ assert false : "hashCode not supported";
+ return 0;
+ }
+
+ Calendar getStart() {
+ return start;
+ }
+
+ Calendar getEnd() {
+ return end;
+ }
+ }
+
+ @Override
+ public boolean isSameSession(Calendar time1, Calendar time2) {
+ if (isNonStopSession())
+ return true;
+ TimeInterval interval1 = theMostRecentIntervalBefore(time1);
+ if (!interval1.isContainingTime(time1)) {
+ return false;
+ }
+ TimeInterval interval2 = theMostRecentIntervalBefore(time2);
+ return interval2.isContainingTime(time2) && interval1.equals(interval2);
+ }
+
+ @Override
+ public boolean isNonStopSession() {
+ return isNonStopSession;
+ }
+
+ private boolean isDailySession() {
+ return !isSet(startTime.getDay()) && !isSet(endTime.getDay());
+ }
+
+ @Override
+ public boolean isSessionTime() {
+ if(isNonStopSession()) {
+ return true;
+ }
+ Calendar now = SystemTime.getUtcCalendar();
+ TimeInterval interval = theMostRecentIntervalBefore(now);
+ return interval.isContainingTime(now);
+ }
+
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+
+ SimpleDateFormat dowFormat = new SimpleDateFormat("EEEE");
+ dowFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss-z");
+ timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ TimeInterval ti = theMostRecentIntervalBefore(SystemTime.getUtcCalendar());
+
+ formatTimeInterval(buf, ti, timeFormat, false);
+
+ // Now the localized equivalents, if necessary
+ if (!startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)
+ || !endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) {
+ buf.append(" (");
+ formatTimeInterval(buf, ti, timeFormat, true);
+ buf.append(")");
+ }
+
+ return buf.toString();
+ }
+
+ private void formatTimeInterval(StringBuilder buf, TimeInterval timeInterval,
+ SimpleDateFormat timeFormat, boolean local) {
+ if (isWeekdaySession) {
+ try {
+ for (int i = 0; i < weekdayOffsets.length; i++) {
+ buf.append(DayConverter.toString(weekdayOffsets[i]));
+ buf.append(", ");
+ }
+ } catch (ConfigError ex) {
+ // this can't happen as these are created using DayConverter.toInteger
+ }
+ } else if (!isDailySession()) {
+ buf.append("weekly, ");
+ formatDayOfWeek(buf, startTime.getDay());
+ buf.append(" ");
+ } else {
+ buf.append("daily, ");
+ }
+
+ if (local) {
+ timeFormat.setTimeZone(startTime.getTimeZone());
+ }
+ buf.append(timeFormat.format(timeInterval.getStart().getTime()));
+
+ buf.append(" - ");
+
+ if (!isDailySession()) {
+ formatDayOfWeek(buf, endTime.getDay());
+ buf.append(" ");
+ }
+ if (local) {
+ timeFormat.setTimeZone(endTime.getTimeZone());
+ }
+ buf.append(timeFormat.format(timeInterval.getEnd().getTime()));
+ }
+
+ private void formatDayOfWeek(StringBuilder buf, int dayOfWeek) {
+ try {
+ String dayName = DayConverter.toString(dayOfWeek).toUpperCase();
+ if (dayName.length() > 3) {
+ dayName = dayName.substring(0, 3);
+ }
+ buf.append(dayName);
+ } catch (ConfigError e) {
+ buf.append("[Error: unknown day ").append(dayOfWeek).append("]");
+ }
+ }
+
+ private int getDay(SessionSettings settings, SessionID sessionID, String key, int defaultValue)
+ throws ConfigError, FieldConvertError {
+ return settings.isSetting(sessionID, key) ?
+ DayConverter.toInteger(settings.getString(sessionID, key))
+ : NOT_SET;
+ }
+
+ private boolean isSet(int value) {
+ return value != NOT_SET;
+ }
+
+ private static int timeInSeconds(int hour, int minute, int second) {
+ return (hour * 3600) + (minute * 60) + second;
+ }
+
+ /**
+ * is the startDateTime a valid day based on the permitted days of week
+ * @param startDateTime time to test
+ * @return flag indicating if valid
+ */
+ private boolean validDayOfWeek(Calendar startDateTime) {
+ int dow = startDateTime.get(Calendar.DAY_OF_WEEK);
+ for (int i = 0; i < weekdayOffsets.length; i++)
+ if (weekdayOffsets[i] == dow)
+ return true;
+ return false;
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java
new file mode 100644
index 000000000..b25f4debc
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+/**
+ * Factory for creating default session schedules.
+ */
+public class DefaultSessionScheduleFactory implements SessionScheduleFactory {
+
+ public SessionSchedule create(SessionID sessionID, SessionSettings settings) throws ConfigError
+ {
+ try {
+ return new DefaultSessionSchedule(settings, sessionID);
+ } catch (final FieldConvertError e) {
+ throw new ConfigError(e.getMessage());
+ }
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/Dictionary.java b/quickfixj-core/src/main/java/quickfix/Dictionary.java
index 3d3310d9e..ab7989b42 100644
--- a/quickfixj-core/src/main/java/quickfix/Dictionary.java
+++ b/quickfixj-core/src/main/java/quickfix/Dictionary.java
@@ -29,13 +29,13 @@
*/
public class Dictionary {
private String name;
- private final HashMap data = new HashMap();
+ private final HashMap data = new HashMap<>();
public Dictionary() {
}
public Dictionary(String name) {
- this(name, new HashMap());
+ this(name, new HashMap<>());
}
public Dictionary(Dictionary dictionary) {
diff --git a/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java b/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java
new file mode 100644
index 000000000..47f4e0c5b
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java
@@ -0,0 +1,32 @@
+package quickfix;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ *
+ * Two Executors are required. The {@link #getLongLivedExecutor()} is used for message processing and the
+ * {@link #getShortLivedExecutor()} is used for timer tasks. They can both be the same underlying Executor if desired.
+ * Separating them allows for additional control such as with a ResourceAdapter WorkManager where the WorkManager
+ * differentiates between short and long lived Work.
+ *
+ *
+ * By way of example, a single {@link Executors#newCachedThreadPool()} satisfies the requirements but really adds
+ * nothing over the default behavior.
+ *
+ */
+public interface ExecutorFactory {
+
+ /**
+ * The message processing activities are long-lived so this Executor must have sufficient distinct Threads available
+ * to handle all your Sessions. The exact number will depend on how many concurrent Sessions you will have and
+ * whether you are using the Threaded or non-Threaded SocketAcceptors/Initiators.
+ */
+ Executor getLongLivedExecutor();
+
+ /**
+ * The timer tasks are short-lived and only require one Thread (calls are serialized).
+ */
+ Executor getShortLivedExecutor();
+
+}
diff --git a/quickfixj-core/src/main/java/quickfix/FieldException.java b/quickfixj-core/src/main/java/quickfix/FieldException.java
index 4e07e0f3a..fc070de2e 100644
--- a/quickfixj-core/src/main/java/quickfix/FieldException.java
+++ b/quickfixj-core/src/main/java/quickfix/FieldException.java
@@ -19,7 +19,7 @@
package quickfix;
-public class FieldException extends RuntimeException {
+public class FieldException extends RuntimeException implements HasFieldAndReason {
private final int field;
@@ -30,7 +30,7 @@ public FieldException(int sessionRejectReason) {
}
public FieldException(int sessionRejectReason, int field) {
- super(SessionRejectReasonText.getMessage(sessionRejectReason) + ", field=" + field);
+ super(SessionRejectReasonText.getMessage(sessionRejectReason) + (field != -1 ? ", field=" + field : ""));
this.sessionRejectReason = sessionRejectReason;
this.field = field;
}
@@ -45,10 +45,12 @@ public boolean isFieldSpecified() {
return field != -1;
}
+ @Override
public int getField() {
return field;
}
+ @Override
public int getSessionRejectReason() {
return sessionRejectReason;
}
diff --git a/quickfixj-core/src/main/java/quickfix/FieldMap.java b/quickfixj-core/src/main/java/quickfix/FieldMap.java
index 4d26a1406..5a80be216 100644
--- a/quickfixj-core/src/main/java/quickfix/FieldMap.java
+++ b/quickfixj-core/src/main/java/quickfix/FieldMap.java
@@ -19,17 +19,6 @@
package quickfix;
-import java.io.Serializable;
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-
import quickfix.field.BeginString;
import quickfix.field.BodyLength;
import quickfix.field.CheckSum;
@@ -43,6 +32,14 @@
import quickfix.field.converter.UtcTimeOnlyConverter;
import quickfix.field.converter.UtcTimestampConverter;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.Map.Entry;
+
/**
* Field container used by messages, groups, and composites.
*/
@@ -54,7 +51,7 @@ public abstract class FieldMap implements Serializable {
private final TreeMap> fields;
- private final TreeMap> groups = new TreeMap>();
+ private final TreeMap> groups = new TreeMap<>();
/**
* Constructs a FieldMap with the given field order.
@@ -64,8 +61,7 @@ public abstract class FieldMap implements Serializable {
*/
protected FieldMap(int[] fieldOrder) {
this.fieldOrder = fieldOrder;
- fields = new TreeMap>(
- fieldOrder != null ? new FieldOrderComparator() : null);
+ fields = new TreeMap<>(fieldOrder != null ? new FieldOrderComparator() : null);
}
protected FieldMap() {
@@ -183,23 +179,31 @@ public void setDecimal(int field, BigDecimal value, int padding) {
setField(new StringField(field, DecimalConverter.convert(value, padding)));
}
- public void setUtcTimeStamp(int field, Date value) {
+ public void setUtcTimeStamp(int field, LocalDateTime value) {
setUtcTimeStamp(field, value, false);
}
- public void setUtcTimeStamp(int field, Date value, boolean includeMilliseconds) {
- setField(new StringField(field, UtcTimestampConverter.convert(value, includeMilliseconds)));
+ public void setUtcTimeStamp(int field, LocalDateTime value, boolean includeMilliseconds) {
+ setField(new StringField(field, UtcTimestampConverter.convert(value, includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS)));
+ }
+
+ public void setUtcTimeStamp(int field, LocalDateTime value, UtcTimestampPrecision precision) {
+ setField(new StringField(field, UtcTimestampConverter.convert(value, precision)));
}
- public void setUtcTimeOnly(int field, Date value) {
+ public void setUtcTimeOnly(int field, LocalTime value) {
setUtcTimeOnly(field, value, false);
}
- public void setUtcTimeOnly(int field, Date value, boolean includeMillseconds) {
- setField(new StringField(field, UtcTimeOnlyConverter.convert(value, includeMillseconds)));
+ public void setUtcTimeOnly(int field, LocalTime value, boolean includeMilliseconds) {
+ setField(new StringField(field, UtcTimeOnlyConverter.convert(value, includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS)));
}
- public void setUtcDateOnly(int field, Date value) {
+ public void setUtcTimeOnly(int field, LocalTime value, UtcTimestampPrecision precision) {
+ setField(new StringField(field, UtcTimeOnlyConverter.convert(value, precision)));
+ }
+
+ public void setUtcDateOnly(int field, LocalDate value) {
setField(new StringField(field, UtcDateOnlyConverter.convert(value)));
}
@@ -223,6 +227,15 @@ public String getString(int field) throws FieldNotFound {
return getField(field).getObject();
}
+ public Optional getOptionalString(int field) {
+ final StringField f = (StringField) fields.get(field);
+ if (f == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(f.getValue());
+ }
+ }
+
public boolean getBoolean(int field) throws FieldNotFound {
try {
return BooleanConverter.convert(getString(field));
@@ -263,25 +276,25 @@ public BigDecimal getDecimal(int field) throws FieldNotFound {
}
}
- public Date getUtcTimeStamp(int field) throws FieldNotFound {
+ public LocalDateTime getUtcTimeStamp(int field) throws FieldNotFound {
try {
- return UtcTimestampConverter.convert(getString(field));
+ return UtcTimestampConverter.convertToLocalDateTime(getString(field));
} catch (final FieldConvertError e) {
throw newIncorrectDataException(e, field);
}
}
- public Date getUtcTimeOnly(int field) throws FieldNotFound {
+ public LocalTime getUtcTimeOnly(int field) throws FieldNotFound {
try {
- return UtcTimeOnlyConverter.convert(getString(field));
+ return UtcTimeOnlyConverter.convertToLocalTime(getString(field));
} catch (final FieldConvertError e) {
throw newIncorrectDataException(e, field);
}
}
- public Date getUtcDateOnly(int field) throws FieldNotFound {
+ public LocalDate getUtcDateOnly(int field) throws FieldNotFound {
try {
- return UtcDateOnlyConverter.convert(getString(field));
+ return UtcDateOnlyConverter.convertToLocalDate(getString(field));
} catch (final FieldConvertError e) {
throw newIncorrectDataException(e, field);
}
@@ -319,11 +332,11 @@ public void setField(DecimalField field) {
}
public void setField(UtcTimeStampField field) {
- setUtcTimeStamp(field.getField(), field.getValue(), field.showMilliseconds());
+ setUtcTimeStamp(field.getField(), field.getValue(), field.getPrecision());
}
public void setField(UtcTimeOnlyField field) {
- setUtcTimeOnly(field.getField(), field.getValue(), field.showMilliseconds());
+ setUtcTimeOnly(field.getField(), field.getValue(), field.getPrecision());
}
public void setField(UtcDateOnlyField field) {
@@ -412,7 +425,7 @@ protected void initializeFrom(FieldMap source) {
fields.clear();
fields.putAll(source.fields);
for (Entry> entry : source.groups.entrySet()) {
- final List clones = new ArrayList();
+ final List clones = new ArrayList<>();
for (final Group group : entry.getValue()) {
final Group clone = new Group(group.getFieldTag(),
group.delim(), group.getFieldOrder());
@@ -570,12 +583,7 @@ protected void setGroupCount(int countTag, int groupSize) {
}
public List getGroups(int field) {
- List groupList = groups.get(field);
- if (groupList == null) {
- groupList = new ArrayList();
- groups.put(field, groupList);
- }
- return groupList;
+ return groups.computeIfAbsent(field, k -> new ArrayList<>());
}
public Group getGroup(int num, Group group) throws FieldNotFound {
diff --git a/quickfixj-core/src/main/java/quickfix/FieldNotFound.java b/quickfixj-core/src/main/java/quickfix/FieldNotFound.java
index 1b08d3f10..63d8f2b3d 100644
--- a/quickfixj-core/src/main/java/quickfix/FieldNotFound.java
+++ b/quickfixj-core/src/main/java/quickfix/FieldNotFound.java
@@ -27,7 +27,7 @@
public class FieldNotFound extends Exception {
public FieldNotFound(int field) {
- super("Field [" + field + "] was not found in message.");
+ super("Field was not found in message, field=" + field);
this.field = field;
}
diff --git a/quickfixj-core/src/main/java/quickfix/FieldType.java b/quickfixj-core/src/main/java/quickfix/FieldType.java
index 802e4879d..767099543 100644
--- a/quickfixj-core/src/main/java/quickfix/FieldType.java
+++ b/quickfixj-core/src/main/java/quickfix/FieldType.java
@@ -19,7 +19,9 @@
package quickfix;
-import java.util.Date;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.LocalDateTime;
/**
* A field type enum class.
@@ -37,7 +39,7 @@ public enum FieldType {
MULTIPLEVALUESTRING,
MULTIPLESTRINGVALUE, // QFJ-881
EXCHANGE,
- UTCTIMESTAMP(Date.class),
+ UTCTIMESTAMP(LocalDateTime.class),
BOOLEAN(Boolean.class),
LOCALMKTDATE,
DATA,
@@ -45,9 +47,9 @@ public enum FieldType {
PRICEOFFSET(Double.class),
MONTHYEAR,
DAYOFMONTH(Integer.class),
- UTCDATEONLY(Date.class),
- UTCDATE(Date.class),
- UTCTIMEONLY(Date.class),
+ UTCDATEONLY(LocalDate.class),
+ UTCDATE(LocalDate.class),
+ UTCTIMEONLY(LocalTime.class),
TIME,
NUMINGROUP(Integer.class),
PERCENTAGE(Double.class),
diff --git a/quickfixj-core/src/main/java/quickfix/FileLog.java b/quickfixj-core/src/main/java/quickfix/FileLog.java
index de2a06454..ecd84fe5a 100644
--- a/quickfixj-core/src/main/java/quickfix/FileLog.java
+++ b/quickfixj-core/src/main/java/quickfix/FileLog.java
@@ -50,6 +50,8 @@ public class FileLog extends AbstractLog {
private final String messagesFileName;
private final String eventFileName;
private boolean syncAfterWrite;
+ private final Object messagesLock = new Object();
+ private final Object eventsLock = new Object();
private FileOutputStream messages;
private FileOutputStream events;
@@ -83,23 +85,25 @@ private void openLogStreams(boolean append) throws FileNotFoundException {
}
protected void logIncoming(String message) {
- writeMessage(messages, message, false);
+ writeMessage(messages, messagesLock, message, false);
}
protected void logOutgoing(String message) {
- writeMessage(messages, message, false);
+ writeMessage(messages, messagesLock, message, false);
}
- private void writeMessage(FileOutputStream stream, String message, boolean forceTimestamp) {
+ private void writeMessage(FileOutputStream stream, Object lock, String message, boolean forceTimestamp) {
try {
- if (forceTimestamp || includeTimestampForMessages) {
- writeTimeStamp(stream);
- }
- stream.write(message.getBytes(CharsetSupport.getCharset()));
- stream.write('\n');
- stream.flush();
- if (syncAfterWrite) {
- stream.getFD().sync();
+ synchronized(lock) {
+ if (forceTimestamp || includeTimestampForMessages) {
+ writeTimeStamp(stream);
+ }
+ stream.write(message.getBytes(CharsetSupport.getCharset()));
+ stream.write('\n');
+ stream.flush();
+ if (syncAfterWrite) {
+ stream.getFD().sync();
+ }
}
} catch (IOException e) {
// QFJ-459: no point trying to log the error in the file if we had an IOException
@@ -110,11 +114,11 @@ private void writeMessage(FileOutputStream stream, String message, boolean force
}
public void onEvent(String message) {
- writeMessage(events, message, true);
+ writeMessage(events, eventsLock, message, true);
}
public void onErrorEvent(String message) {
- writeMessage(events, message, true);
+ writeMessage(events, eventsLock, message, true);
}
private void writeTimeStamp(OutputStream out) throws IOException {
@@ -135,16 +139,6 @@ public void setSyncAfterWrite(boolean syncAfterWrite) {
this.syncAfterWrite = syncAfterWrite;
}
- /**
- * Closes the messages and events files.
- *
- * @deprecated Use close instead.
- * @throws IOException
- */
- public void closeFiles() throws IOException {
- close();
- }
-
/**
* Closed the messages and events files.
*
diff --git a/quickfixj-core/src/main/java/quickfix/FileLogFactory.java b/quickfixj-core/src/main/java/quickfix/FileLogFactory.java
index 18f0dbd32..9c0cc850c 100644
--- a/quickfixj-core/src/main/java/quickfix/FileLogFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/FileLogFactory.java
@@ -90,7 +90,4 @@ public Log create(SessionID sessionID) {
}
}
- public Log create() {
- throw new UnsupportedOperationException();
- }
}
diff --git a/quickfixj-core/src/main/java/quickfix/FileStore.java b/quickfixj-core/src/main/java/quickfix/FileStore.java
index b965d05ac..20ec17f58 100644
--- a/quickfixj-core/src/main/java/quickfix/FileStore.java
+++ b/quickfixj-core/src/main/java/quickfix/FileStore.java
@@ -19,6 +19,9 @@
package quickfix;
+import org.quickfixj.CharsetSupport;
+import quickfix.field.converter.UtcTimestampConverter;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
@@ -37,10 +40,6 @@
import java.util.Set;
import java.util.TreeMap;
-import org.quickfixj.CharsetSupport;
-
-import quickfix.field.converter.UtcTimestampConverter;
-
/**
* File store implementation. THIS CLASS IS PUBLIC ONLY TO MAINTAIN
* COMPATIBILITY WITH THE QUICKFIX JNI. IT SHOULD ONLY BE CREATED USING A
@@ -77,7 +76,7 @@ public class FileStore implements MessageStore, Closeable {
this.syncWrites = syncWrites;
this.maxCachedMsgs = maxCachedMsgs;
- messageIndex = maxCachedMsgs > 0 ? new TreeMap() : null;
+ messageIndex = maxCachedMsgs > 0 ? new TreeMap<>() : null;
final String fullPath = new File(path == null ? "." : path).getAbsolutePath();
final String sessionName = FileUtil.sessionIdFileName(sessionID);
@@ -98,10 +97,10 @@ public class FileStore implements MessageStore, Closeable {
}
void initialize(boolean deleteFiles) throws IOException {
- close();
-
if (deleteFiles) {
- deleteFiles();
+ closeAndDeleteFiles();
+ } else {
+ close();
}
String mode = READ_OPTION + WRITE_OPTION + (syncWrites ? SYNC_OPTION : NOSYNC_OPTION);
@@ -124,16 +123,13 @@ private void initializeCache() throws IOException {
private void initializeSessionCreateTime() throws IOException {
final File sessionTimeFile = new File(sessionFileName);
if (sessionTimeFile.exists() && sessionTimeFile.length() > 0) {
- final DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream(
- new FileInputStream(sessionTimeFile)));
- try {
+ try (DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream(
+ new FileInputStream(sessionTimeFile)))) {
final Calendar c = SystemTime.getUtcCalendar(UtcTimestampConverter
.convert(sessionTimeInput.readUTF()));
cache.setCreationTime(c);
} catch (final Exception e) {
throw new IOException(e.getMessage());
- } finally {
- sessionTimeInput.close();
}
} else {
storeSessionTimeStamp();
@@ -141,14 +137,11 @@ private void initializeSessionCreateTime() throws IOException {
}
private void storeSessionTimeStamp() throws IOException {
- final DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream(
- new FileOutputStream(sessionFileName, false)));
- try {
+ try (DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream(
+ new FileOutputStream(sessionFileName, false)))) {
final Date date = SystemTime.getDate();
cache.setCreationTime(SystemTime.getUtcCalendar(date));
sessionTimeOutput.writeUTF(UtcTimestampConverter.convert(date, true));
- } finally {
- sessionTimeOutput.close();
}
}
@@ -180,17 +173,14 @@ private void initializeMessageIndex() throws IOException {
messageIndex.clear();
final File headerFile = new File(headerFileName);
if (headerFile.exists()) {
- final DataInputStream headerDataInputStream = new DataInputStream(
- new BufferedInputStream(new FileInputStream(headerFile)));
- try {
+ try (DataInputStream headerDataInputStream = new DataInputStream(
+ new BufferedInputStream(new FileInputStream(headerFile)))) {
while (headerDataInputStream.available() > 0) {
final int sequenceNumber = headerDataInputStream.readInt();
final long offset = headerDataInputStream.readLong();
final int size = headerDataInputStream.readInt();
updateMessageIndex(sequenceNumber, offset, size);
}
- } finally {
- headerDataInputStream.close();
}
}
}
@@ -229,7 +219,7 @@ private static void close(Closeable closeable) throws IOException {
}
}
- public void deleteFiles() throws IOException {
+ public void closeAndDeleteFiles() throws IOException {
close();
deleteFile(headerFileName);
deleteFile(msgFileName);
@@ -303,9 +293,9 @@ public void incrNextTargetMsgSeqNum() throws IOException {
@Override
public void get(int startSequence, int endSequence, Collection messages)
throws IOException {
- final Set uncachedOffsetMsgIds = new HashSet();
+ final Set uncachedOffsetMsgIds = new HashSet<>();
// Use a treemap to make sure the messages are sorted by sequence num
- final TreeMap messagesFound = new TreeMap();
+ final TreeMap messagesFound = new TreeMap<>();
for (int i = startSequence; i <= endSequence; i++) {
final String message = getMessage(i);
if (message != null) {
@@ -318,9 +308,8 @@ public void get(int startSequence, int endSequence, Collection messages)
if (!uncachedOffsetMsgIds.isEmpty()) {
// parse the header file to find missing messages
final File headerFile = new File(headerFileName);
- final DataInputStream headerDataInputStream = new DataInputStream(
- new BufferedInputStream(new FileInputStream(headerFile)));
- try {
+ try (DataInputStream headerDataInputStream = new DataInputStream(
+ new BufferedInputStream(new FileInputStream(headerFile)))) {
while (!uncachedOffsetMsgIds.isEmpty() && headerDataInputStream.available() > 0) {
final int sequenceNumber = headerDataInputStream.readInt();
final long offset = headerDataInputStream.readLong();
@@ -330,8 +319,6 @@ public void get(int startSequence, int endSequence, Collection messages)
messagesFound.put(sequenceNumber, message);
}
}
- } finally {
- headerDataInputStream.close();
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/FileUtil.java b/quickfixj-core/src/main/java/quickfix/FileUtil.java
index e732ee562..c8159f778 100644
--- a/quickfixj-core/src/main/java/quickfix/FileUtil.java
+++ b/quickfixj-core/src/main/java/quickfix/FileUtil.java
@@ -24,7 +24,6 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.net.MalformedURLException;
import java.net.URL;
public class FileUtil {
@@ -143,8 +142,6 @@ public static InputStream open(Class> clazz, String name, Location... location
case URL:
try {
in = new URL(name).openStream();
- } catch (MalformedURLException e) {
- // ignore
} catch (IOException e) {
// ignore
}
diff --git a/quickfixj-core/src/main/java/quickfix/FixVersions.java b/quickfixj-core/src/main/java/quickfix/FixVersions.java
index 16503b6e1..7cb7f9818 100644
--- a/quickfixj-core/src/main/java/quickfix/FixVersions.java
+++ b/quickfixj-core/src/main/java/quickfix/FixVersions.java
@@ -23,21 +23,21 @@
* Constants containing the BeginString field values for various FIX versions.
*/
public interface FixVersions {
- public static final String BEGINSTRING_FIX40 = "FIX.4.0";
- public static final String BEGINSTRING_FIX41 = "FIX.4.1";
- public static final String BEGINSTRING_FIX42 = "FIX.4.2";
- public static final String BEGINSTRING_FIX43 = "FIX.4.3";
- public static final String BEGINSTRING_FIX44 = "FIX.4.4";
+ String BEGINSTRING_FIX40 = "FIX.4.0";
+ String BEGINSTRING_FIX41 = "FIX.4.1";
+ String BEGINSTRING_FIX42 = "FIX.4.2";
+ String BEGINSTRING_FIX43 = "FIX.4.3";
+ String BEGINSTRING_FIX44 = "FIX.4.4";
/**
* FIX 5.0 does not have a begin string.
*/
- public static final String FIX50 = "FIX.5.0";
- public static final String FIX50SP1 = "FIX.5.0SP1";
- public static final String FIX50SP2 = "FIX.5.0SP2";
+ String FIX50 = "FIX.5.0";
+ String FIX50SP1 = "FIX.5.0SP1";
+ String FIX50SP2 = "FIX.5.0SP2";
// FIXT.x.x support
- public static final String FIXT_SESSION_PREFIX = "FIXT.";
- public static final String BEGINSTRING_FIXT11 = FIXT_SESSION_PREFIX + "1.1";
+ String FIXT_SESSION_PREFIX = "FIXT.";
+ String BEGINSTRING_FIXT11 = FIXT_SESSION_PREFIX + "1.1";
}
diff --git a/quickfixj-core/src/main/java/quickfix/Group.java b/quickfixj-core/src/main/java/quickfix/Group.java
index 3600315f5..da4520da9 100644
--- a/quickfixj-core/src/main/java/quickfix/Group.java
+++ b/quickfixj-core/src/main/java/quickfix/Group.java
@@ -75,11 +75,4 @@ public int getFieldTag() {
return field.getTag();
}
- /**
- * @deprecated Use getFieldTag
- * @return the field's tag number
- */
- public int field() {
- return getFieldTag();
- }
}
diff --git a/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java b/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java
new file mode 100644
index 000000000..7d30c60b7
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+interface HasFieldAndReason {
+
+ int getField();
+ int getSessionRejectReason();
+ String getMessage();
+}
diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java
index 5068196cf..b1ae253f4 100644
--- a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java
+++ b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java
@@ -19,19 +19,22 @@
package quickfix;
+import quickfix.field.SessionRejectReason;
+
/**
* Field has a badly formatted value. (From the C++ API documentation.)
*/
-public class IncorrectDataFormat extends Exception {
- public final int field;
- public final String data;
+public class IncorrectDataFormat extends Exception implements HasFieldAndReason {
+ private final int field;
+ private final String data;
+ private final int sessionRejectReason;
/**
* @param field the tag number with the incorrect data
* @param data the incorrect data
*/
public IncorrectDataFormat(final int field, final String data) {
- this(field, data, "Field [" + field + "] contains badly formatted data.");
+ this(field, data, SessionRejectReasonText.getMessage(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE) + ", field=" + field);
}
/**
@@ -51,10 +54,25 @@ public IncorrectDataFormat(final int field) {
public IncorrectDataFormat(final String message) {
this(0, null, message);
}
+
+ @Override
+ public int getSessionRejectReason() {
+ return sessionRejectReason;
+ }
+ @Override
+ public int getField() {
+ return field;
+ }
+
+ public String getData() {
+ return data;
+ }
+
private IncorrectDataFormat(final int field, final String data, final String message) {
super(message);
this.field = field;
this.data = data;
+ this.sessionRejectReason = SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE;
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java
index 1d55ea7c4..9558a9fbf 100644
--- a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java
+++ b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java
@@ -19,24 +19,35 @@
package quickfix;
+import quickfix.field.SessionRejectReason;
+
/**
* An exception thrown when a tags value is not valid according to the data dictionary.
*/
-public class IncorrectTagValue extends Exception {
+public class IncorrectTagValue extends Exception implements HasFieldAndReason {
+
+ private String value;
+ private final int field;
+ private final int sessionRejectReason;
public IncorrectTagValue(int field) {
- super("Field [" + field + "] contains an incorrect tag value.");
+ super(SessionRejectReasonText.getMessage(SessionRejectReason.VALUE_IS_INCORRECT) + ", field=" + field);
this.field = field;
+ this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT;
}
public IncorrectTagValue(int field, String value) {
- super();
+ super(SessionRejectReasonText.getMessage(SessionRejectReason.VALUE_IS_INCORRECT) + ", field=" + field + (value != null ? ", value=" + value : ""));
this.field = field;
this.value = value;
+ this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT;
}
- public IncorrectTagValue(String s) {
- super(s);
+ public IncorrectTagValue(int field, String value, String message) {
+ super(message);
+ this.field = field;
+ this.value = value;
+ this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT;
}
@Override
@@ -49,7 +60,14 @@ public String toString() {
return str;
}
- public int field;
+ @Override
+ public int getField() {
+ return field;
+ }
- public String value;
+ @Override
+ public int getSessionRejectReason() {
+ return sessionRejectReason;
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/Initiator.java b/quickfixj-core/src/main/java/quickfix/Initiator.java
index 21899c60a..ffb39bf63 100644
--- a/quickfixj-core/src/main/java/quickfix/Initiator.java
+++ b/quickfixj-core/src/main/java/quickfix/Initiator.java
@@ -30,12 +30,12 @@ public interface Initiator extends Connector {
*
* @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE
*/
- public static final String SETTING_RECONNECT_INTERVAL = "ReconnectInterval";
+ String SETTING_RECONNECT_INTERVAL = "ReconnectInterval";
/**
* Initiator setting for connection protocol (defaults to "tcp").
*/
- public static final String SETTING_SOCKET_CONNECT_PROTOCOL = "SocketConnectProtocol";
+ String SETTING_SOCKET_CONNECT_PROTOCOL = "SocketConnectProtocol";
/**
* Initiator setting for connection host. Only valid when session connection
@@ -43,7 +43,7 @@ public interface Initiator extends Connector {
*
* @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE
*/
- public static final String SETTING_SOCKET_CONNECT_HOST = "SocketConnectHost";
+ String SETTING_SOCKET_CONNECT_HOST = "SocketConnectHost";
/**
* Initiator setting for connection port. Only valid when session connection
@@ -51,7 +51,7 @@ public interface Initiator extends Connector {
*
* @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE
*/
- public static final String SETTING_SOCKET_CONNECT_PORT = "SocketConnectPort";
+ String SETTING_SOCKET_CONNECT_PORT = "SocketConnectPort";
/**
* Initiator setting for local/bind host. Only valid when session connection
@@ -59,7 +59,7 @@ public interface Initiator extends Connector {
*
* @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE
*/
- public static final String SETTING_SOCKET_LOCAL_HOST = "SocketLocalHost";
+ String SETTING_SOCKET_LOCAL_HOST = "SocketLocalHost";
/**
* Initiator setting for local/bind port. Only valid when session connection
@@ -67,5 +67,53 @@ public interface Initiator extends Connector {
*
* @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE
*/
- public static final String SETTING_SOCKET_LOCAL_PORT = "SocketLocalPort";
+ String SETTING_SOCKET_LOCAL_PORT = "SocketLocalPort";
+
+ /**
+ * Initiator setting for proxy type. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_TYPE = "ProxyType";
+
+ /**
+ * Initiator setting for proxy version. Only valid when session connection
+ * type is "initiator". - http 1.0 / 1.1
+ */
+ String SETTING_PROXY_VERSION = "ProxyVersion";
+
+ /**
+ * Initiator setting for proxy host. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_HOST = "ProxyHost";
+
+ /**
+ * Initiator setting for proxy port. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_PORT = "ProxyPort";
+
+ /**
+ * Initiator setting for proxy port. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_USER = "ProxyUser";
+
+ /**
+ * Initiator setting for proxy port. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_PASSWORD = "ProxyPassword";
+
+ /**
+ * Initiator setting for proxy domain. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_DOMAIN = "ProxyDomain";
+
+ /**
+ * Initiator setting for proxy workstation. Only valid when session connection
+ * type is "initiator".
+ */
+ String SETTING_PROXY_WORKSTATION = "ProxyWorkstation";
}
diff --git a/quickfixj-core/src/main/java/quickfix/InvalidMessage.java b/quickfixj-core/src/main/java/quickfix/InvalidMessage.java
index aa6a92090..4b2b4cc3b 100644
--- a/quickfixj-core/src/main/java/quickfix/InvalidMessage.java
+++ b/quickfixj-core/src/main/java/quickfix/InvalidMessage.java
@@ -32,4 +32,9 @@ public InvalidMessage() {
public InvalidMessage(String message) {
super(message);
}
+
+ public InvalidMessage(String message, Throwable cause) {
+ super(message, cause);
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLog.java b/quickfixj-core/src/main/java/quickfix/JdbcLog.java
index 2dfefcda6..308e37fad 100644
--- a/quickfixj-core/src/main/java/quickfix/JdbcLog.java
+++ b/quickfixj-core/src/main/java/quickfix/JdbcLog.java
@@ -19,9 +19,7 @@
package quickfix;
-import static quickfix.JdbcSetting.*;
-import static quickfix.JdbcUtil.*;
-
+import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@@ -29,7 +27,15 @@
import java.util.HashMap;
import java.util.Map;
-import javax.sql.DataSource;
+import static quickfix.JdbcSetting.SETTING_JDBC_LOG_HEARTBEATS;
+import static quickfix.JdbcSetting.SETTING_JDBC_SESSION_ID_DEFAULT_PROPERTY_VALUE;
+import static quickfix.JdbcSetting.SETTING_LOG_EVENT_TABLE;
+import static quickfix.JdbcSetting.SETTING_LOG_INCOMING_TABLE;
+import static quickfix.JdbcSetting.SETTING_LOG_OUTGOING_TABLE;
+import static quickfix.JdbcUtil.determineSessionIdSupport;
+import static quickfix.JdbcUtil.getIDColumns;
+import static quickfix.JdbcUtil.getIDPlaceholders;
+import static quickfix.JdbcUtil.getIDWhereClause;
class JdbcLog extends AbstractLog {
private static final String DEFAULT_MESSAGES_LOG_TABLE = "messages_log";
@@ -45,8 +51,8 @@ class JdbcLog extends AbstractLog {
private Throwable recursiveException = null;
- private final Map insertItemSqlCache = new HashMap();
- private final Map deleteItemsSqlCache = new HashMap();
+ private final Map insertItemSqlCache = new HashMap<>();
+ private final Map deleteItemsSqlCache = new HashMap<>();
public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds)
throws SQLException, ClassNotFoundException, ConfigError, FieldConvertError {
@@ -55,22 +61,23 @@ public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds)
? JdbcUtil.getDataSource(settings, sessionID)
: ds;
- logHeartbeats = !settings.isSetting(SETTING_JDBC_LOG_HEARTBEATS) || settings.getBool(SETTING_JDBC_LOG_HEARTBEATS);
+ logHeartbeats = !settings.isSetting(sessionID, SETTING_JDBC_LOG_HEARTBEATS)
+ || settings.getBool(sessionID, SETTING_JDBC_LOG_HEARTBEATS);
setLogHeartbeats(logHeartbeats);
- if (settings.isSetting(SETTING_LOG_OUTGOING_TABLE)) {
+ if (settings.isSetting(sessionID, SETTING_LOG_OUTGOING_TABLE)) {
outgoingMessagesTableName = settings.getString(sessionID, SETTING_LOG_OUTGOING_TABLE);
} else {
outgoingMessagesTableName = DEFAULT_MESSAGES_LOG_TABLE;
}
- if (settings.isSetting(SETTING_LOG_INCOMING_TABLE)) {
+ if (settings.isSetting(sessionID, SETTING_LOG_INCOMING_TABLE)) {
incomingMessagesTableName = settings.getString(sessionID, SETTING_LOG_INCOMING_TABLE);
} else {
incomingMessagesTableName = DEFAULT_MESSAGES_LOG_TABLE;
}
- if (settings.isSetting(SETTING_LOG_EVENT_TABLE)) {
+ if (settings.isSetting(sessionID, SETTING_LOG_EVENT_TABLE)) {
eventTableName = settings.getString(sessionID, SETTING_LOG_EVENT_TABLE);
} else {
eventTableName = DEFAULT_EVENT_LOG_TABLE;
diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java b/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java
index b5092ba85..78939176f 100644
--- a/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java
@@ -57,10 +57,6 @@ protected SessionSettings getSettings() {
return settings;
}
- public Log create() {
- throw new UnsupportedOperationException();
- }
-
/**
* Set a data source to be used by the JdbcLog to access
* the database.
diff --git a/quickfixj-core/src/main/java/quickfix/JdbcStore.java b/quickfixj-core/src/main/java/quickfix/JdbcStore.java
index 46571aec1..1874d0953 100644
--- a/quickfixj-core/src/main/java/quickfix/JdbcStore.java
+++ b/quickfixj-core/src/main/java/quickfix/JdbcStore.java
@@ -192,7 +192,7 @@ public void reset() throws IOException {
setSessionIdParameters(updateTime, 4);
updateTime.execute();
} catch (SQLException e) {
- throw (IOException) new IOException(e.getMessage()).initCause(e);
+ throw new IOException(e.getMessage(), e);
} finally {
JdbcUtil.close(sessionID, deleteMessages);
JdbcUtil.close(sessionID, updateTime);
@@ -217,7 +217,7 @@ public void get(int startSequence, int endSequence, Collection messages)
messages.add(message);
}
} catch (SQLException e) {
- throw (IOException) new IOException(e.getMessage()).initCause(e);
+ throw new IOException(e.getMessage(), e);
} finally {
JdbcUtil.close(sessionID, rs);
JdbcUtil.close(sessionID, query);
@@ -247,7 +247,7 @@ public boolean set(int sequence, String message) throws IOException {
boolean status = update.execute();
return !status && update.getUpdateCount() > 0;
} catch (SQLException e) {
- throw (IOException) new IOException(e.getMessage()).initCause(e);
+ throw new IOException(e.getMessage(), e);
} finally {
JdbcUtil.close(sessionID, update);
}
@@ -281,7 +281,7 @@ private void storeSequenceNumbers() throws IOException {
setSessionIdParameters(update, 3);
update.execute();
} catch (SQLException e) {
- throw (IOException) new IOException(e.getMessage()).initCause(e);
+ throw new IOException(e.getMessage(), e);
} finally {
JdbcUtil.close(sessionID, update);
JdbcUtil.close(sessionID, connection);
@@ -292,7 +292,7 @@ public void refresh() throws IOException {
try {
loadCache();
} catch (SQLException e) {
- throw (IOException) new IOException(e.getMessage()).initCause(e);
+ throw new IOException(e.getMessage(), e);
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java
index bd8f24b3f..14ffc259e 100644
--- a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java
+++ b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java
@@ -19,6 +19,11 @@
package quickfix;
+import org.logicalcobwebs.proxool.ProxoolDataSource;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
@@ -26,19 +31,15 @@
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import javax.sql.DataSource;
-
-import org.logicalcobwebs.proxool.ProxoolDataSource;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
class JdbcUtil {
static final String CONNECTION_POOL_ALIAS = "quickfixj";
- private static final Map dataSources = new ConcurrentHashMap();
- private static int dataSourceCounter = 1;
+ private static final Map dataSources = new ConcurrentHashMap<>();
+ private static final AtomicInteger dataSourceCounter = new AtomicInteger();
static DataSource getDataSource(SessionSettings settings, SessionID sessionID)
throws ConfigError, FieldConvertError {
@@ -82,35 +83,35 @@ static DataSource getDataSource(String jdbcDriver, String connectionURL, String
}
/**
- * This is typically called from a single thread, but just in case we are synchronizing modification
- * of the cache. The cache itself is thread safe.
+ * This is typically called from a single thread, but just in case we are using an atomic loading function
+ * to avoid the creation of two data sources simultaneously. The cache itself is thread safe.
*/
- static synchronized DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password,
+ static DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password,
boolean cache, int maxConnCount, int simultaneousBuildThrottle,
long maxActiveTime, int maxConnLifetime) {
String key = jdbcDriver + "#" + connectionURL + "#" + user + "#" + password;
ProxoolDataSource ds = cache ? dataSources.get(key) : null;
if (ds == null) {
- ds = new ProxoolDataSource(JdbcUtil.CONNECTION_POOL_ALIAS + "-" + dataSourceCounter++);
-
- ds.setDriver(jdbcDriver);
- ds.setDriverUrl(connectionURL);
-
- // Bug in Proxool 0.9RC2. Must set both delegate properties and individual setters. :-(
- ds.setDelegateProperties("user=" + user + ","
- + (password != null && !"".equals(password) ? "password=" + password : ""));
- ds.setUser(user);
- ds.setPassword(password);
-
- ds.setMaximumActiveTime(maxActiveTime);
- ds.setMaximumConnectionLifetime(maxConnLifetime);
- ds.setMaximumConnectionCount(maxConnCount);
- ds.setSimultaneousBuildThrottle(simultaneousBuildThrottle);
-
- if (cache) {
- dataSources.put(key, ds);
- }
+ final Function loadingFunction = dataSourceKey -> {
+ final ProxoolDataSource dataSource = new ProxoolDataSource(CONNECTION_POOL_ALIAS + "-" + dataSourceCounter.incrementAndGet());
+
+ dataSource.setDriver(jdbcDriver);
+ dataSource.setDriverUrl(connectionURL);
+
+ // Bug in Proxool 0.9RC2. Must set both delegate properties and individual setters. :-(
+ dataSource.setDelegateProperties("user=" + user + ","
+ + (password != null && !"".equals(password) ? "password=" + password : ""));
+ dataSource.setUser(user);
+ dataSource.setPassword(password);
+
+ dataSource.setMaximumActiveTime(maxActiveTime);
+ dataSource.setMaximumConnectionLifetime(maxConnLifetime);
+ dataSource.setMaximumConnectionCount(maxConnCount);
+ dataSource.setSimultaneousBuildThrottle(simultaneousBuildThrottle);
+ return dataSource;
+ };
+ ds = cache ? dataSources.computeIfAbsent(key, loadingFunction) : loadingFunction.apply(key);
}
return ds;
}
@@ -146,24 +147,18 @@ static void close(SessionID sessionID, ResultSet rs) {
}
static boolean determineSessionIdSupport(DataSource dataSource, String tableName) throws SQLException {
- Connection connection = dataSource.getConnection();
- try {
+ try (Connection connection = dataSource.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
String columnName = "sendersubid";
return isColumn(metaData, tableName.toUpperCase(), columnName.toUpperCase())
|| isColumn(metaData, tableName, columnName);
- } finally {
- connection.close();
}
}
private static boolean isColumn(DatabaseMetaData metaData, String tableName, String columnName)
throws SQLException {
- ResultSet columns = metaData.getColumns(null, null, tableName, columnName);
- try {
+ try (ResultSet columns = metaData.getColumns(null, null, tableName, columnName)) {
return columns.next();
- } finally {
- columns.close();
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/ListenerSupport.java b/quickfixj-core/src/main/java/quickfix/ListenerSupport.java
index 99041a21e..56146e1d6 100644
--- a/quickfixj-core/src/main/java/quickfix/ListenerSupport.java
+++ b/quickfixj-core/src/main/java/quickfix/ListenerSupport.java
@@ -26,7 +26,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
public class ListenerSupport {
- private final List listeners = new CopyOnWriteArrayList();
+ private final List listeners = new CopyOnWriteArrayList<>();
private final Object multicaster;
public ListenerSupport(Class> listenerClass) {
diff --git a/quickfixj-core/src/main/java/quickfix/LogFactory.java b/quickfixj-core/src/main/java/quickfix/LogFactory.java
index 9681f7e38..301a65e36 100644
--- a/quickfixj-core/src/main/java/quickfix/LogFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/LogFactory.java
@@ -24,14 +24,6 @@
*/
public interface LogFactory {
- /**
- * Create a log using default/global settings.
- *
- * @deprecated This method is not needed by QFJ and is generally not implemented.
- * @return the log implementation
- */
- Log create();
-
/**
* Create a log implementation.
*
diff --git a/quickfixj-core/src/main/java/quickfix/MemoryStore.java b/quickfixj-core/src/main/java/quickfix/MemoryStore.java
index 553e3d6dc..f10205651 100644
--- a/quickfixj-core/src/main/java/quickfix/MemoryStore.java
+++ b/quickfixj-core/src/main/java/quickfix/MemoryStore.java
@@ -19,21 +19,21 @@
package quickfix;
+import org.slf4j.LoggerFactory;
+
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
-import org.slf4j.LoggerFactory;
-
/**
* In-memory message store implementation.
*
* @see quickfix.MemoryStoreFactory
*/
public class MemoryStore implements MessageStore {
- private final HashMap messages = new HashMap();
+ private final HashMap messages = new HashMap<>();
private int nextSenderMsgSeqNum;
private int nextTargetMsgSeqNum;
private SessionID sessionID;
@@ -43,8 +43,9 @@ public MemoryStore() throws IOException {
reset();
}
- public MemoryStore(SessionID sessionID) {
+ public MemoryStore(SessionID sessionID) throws IOException {
this.sessionID = sessionID;
+ reset();
}
public void get(int startSequence, int endSequence, Collection messages) throws IOException {
@@ -111,8 +112,8 @@ public void setNextTargetMsgSeqNum(int next) throws IOException {
public void refresh() throws IOException {
// IOException is declared to maintain strict compatibility with QF JNI
final String text = "memory store does not support refresh!";
- if (sessionID != null) {
- Session session = Session.lookupSession(sessionID);
+ final Session session = sessionID != null ? Session.lookupSession(sessionID) : null;
+ if (session != null) {
session.getLog().onErrorEvent("ERROR: " + text);
} else {
LoggerFactory.getLogger(MemoryStore.class).error(text);
diff --git a/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java b/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java
index 890365166..09b670b73 100644
--- a/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java
@@ -30,7 +30,7 @@ public class MemoryStoreFactory implements MessageStoreFactory {
public MessageStore create(SessionID sessionID) {
try {
- return new MemoryStore();
+ return new MemoryStore(sessionID);
} catch (IOException e) {
throw new RuntimeError(e);
}
diff --git a/quickfixj-core/src/main/java/quickfix/Message.java b/quickfixj-core/src/main/java/quickfix/Message.java
index d39e8592b..f227808d6 100644
--- a/quickfixj-core/src/main/java/quickfix/Message.java
+++ b/quickfixj-core/src/main/java/quickfix/Message.java
@@ -23,19 +23,18 @@
import java.text.DecimalFormat;
import java.util.Iterator;
import java.util.List;
-
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
-
import org.quickfixj.CharsetSupport;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import quickfix.field.ApplExtID;
import quickfix.field.ApplVerID;
import quickfix.field.BeginString;
import quickfix.field.BodyLength;
@@ -79,8 +78,7 @@ public class Message extends FieldMap {
protected Header header = new Header();
protected Trailer trailer = new Trailer();
- // @GuardedBy("this")
- private FieldException exception;
+ private volatile FieldException exception;
public Message() {
// empty
@@ -115,9 +113,7 @@ public Object clone() {
try {
final Message message = getClass().newInstance();
return cloneTo(message);
- } catch (final InstantiationException e) {
- throw new RuntimeException(e);
- } catch (final IllegalAccessException e) {
+ } catch (final InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@@ -129,23 +125,146 @@ private Object cloneTo(Message message) {
return message;
}
+ private static final class Context {
+ private final BodyLength bodyLength = new BodyLength(100);
+ private final CheckSum checkSum = new CheckSum("000");
+ private final StringBuilder stringBuilder = new StringBuilder(1024);
+ }
+
+ private static final ThreadLocal stringContexts = new ThreadLocal() {
+ @Override
+ protected Context initialValue() {
+ return new Context();
+ }
+ };
+
+ protected static boolean IS_STRING_EQUIVALENT = CharsetSupport.isStringEquivalent(CharsetSupport.getCharsetInstance());
+
/**
* Do not call this method concurrently while modifying the contents of the message.
* This is likely to produce unexpected results or will fail with a ConcurrentModificationException
* since FieldMap.calculateString() is iterating over the TreeMap of fields.
+ *
+ * Use toRawString() to get the raw message data.
+ *
+ * @return Message as String with calculated body length and checksum.
*/
@Override
public String toString() {
- final int bodyLength = bodyLength();
- header.setInt(BodyLength.FIELD, bodyLength);
- trailer.setString(CheckSum.FIELD, checksum());
-
- final StringBuilder sb = new StringBuilder(bodyLength);
- header.calculateString(sb, null, null);
- calculateString(sb, null, null);
- trailer.calculateString(sb, null, null);
+ Context context = stringContexts.get();
+ if (IS_STRING_EQUIVALENT) { // length & checksum can easily be calculated after message is built
+ header.setField(context.bodyLength);
+ trailer.setField(context.checkSum);
+ } else {
+ header.setInt(BodyLength.FIELD, bodyLength());
+ trailer.setString(CheckSum.FIELD, checksum());
+ }
+ StringBuilder stringBuilder = context.stringBuilder;
+ try {
+ header.calculateString(stringBuilder, null, null);
+ calculateString(stringBuilder, null, null);
+ trailer.calculateString(stringBuilder, null, null);
+ if (IS_STRING_EQUIVALENT) {
+ setBodyLength(stringBuilder);
+ setChecksum(stringBuilder);
+ }
+ return stringBuilder.toString();
+ } finally {
+ stringBuilder.setLength(0);
+ }
+ }
+
+ 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 static void setBodyLength(StringBuilder stringBuilder) {
+ int bodyLengthIndex = indexOf(stringBuilder, BODY_LENGTH_FIELD, 0);
+ int sohIndex = indexOf(stringBuilder, SOH, bodyLengthIndex + 1);
+ int checkSumIndex = lastIndexOf(stringBuilder, 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 = lastIndexOf(stringBuilder, 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);
+ }
+
+ // return index of a string in a stringbuilder without performing allocations
+ private static int indexOf(StringBuilder source, String target, int fromIndex) {
+ if (fromIndex >= source.length())
+ return (target.length() == 0 ? source.length() : -1);
+ if (fromIndex < 0)
+ fromIndex = 0;
+ if (target.length() == 0)
+ return fromIndex;
+ char first = target.charAt(0);
+ int max = source.length() - target.length();
+ for (int i = fromIndex; i <= max; i++) {
+ if (source.charAt(i) != first)
+ while (++i <= max && source.charAt(i) != first);
+ if (i <= max) {
+ int j = i + 1;
+ int end = j + target.length() - 1;
+ for (int k = 1; j < end && source.charAt(j)
+ == target.charAt(k); j++, k++);
+ if (j == end)
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // return last index of a string in a stringbuilder without performing allocations
+ private static int lastIndexOf(StringBuilder source, String target) {
+ int rightIndex = source.length() - target.length();
+ int fromIndex = source.length();
+ if (fromIndex > rightIndex)
+ fromIndex = rightIndex;
+ if (target.length() == 0)
+ return fromIndex;
+ int strLastIndex = target.length() - 1;
+ char strLastChar = target.charAt(strLastIndex);
+ int min = target.length() - 1;
+ int i = min + fromIndex;
+ startSearchForLastChar:
+ while (true) {
+ while (i >= min && source.charAt(i) != strLastChar)
+ i--;
+ if (i < min)
+ return -1;
+ int j = i - 1;
+ int start = j - (target.length() - 1);
+ int k = strLastIndex - 1;
+ while (j > start)
+ if (source.charAt(j--) != target.charAt(k--)) {
+ i--;
+ continue startSearchForLastChar;
+ }
+ return start + 1;
+ }
+ }
- return sb.toString();
+ /**
+ * Return the raw message data as it was passed to the Message class.
+ *
+ * This is only available after Message has been parsed via constructor or Message.fromString().
+ * Otherwise this method will return NULL.
+ *
+ * This method neither does change fields nor calculate body length or checksum.
+ * Use toString() for that purpose.
+ *
+ * @return Message as String without recalculating body length and checksum.
+ */
+ public String toRawString() {
+ return messageData;
}
public int bodyLength() {
@@ -514,7 +633,7 @@ && isNextField(dd, header, BodyLength.FIELD)
header.setField(field);
if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) {
- parseGroup(DataDictionary.HEADER_ID, field, dd, header);
+ parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation);
}
field = extractField(dd, header);
@@ -553,7 +672,7 @@ private void parseBody(DataDictionary dd, boolean doValidation) throws InvalidMe
setField(header, field);
// Group case
if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) {
- parseGroup(DataDictionary.HEADER_ID, field, dd, header);
+ parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation);
}
if (doValidation && dd != null && dd.isCheckFieldsOutOfOrder())
throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER,
@@ -562,7 +681,7 @@ private void parseBody(DataDictionary dd, boolean doValidation) throws InvalidMe
setField(this, field);
// Group case
if (dd != null && dd.isGroup(getMsgType(), field.getField())) {
- parseGroup(getMsgType(), field, dd, this);
+ parseGroup(getMsgType(), field, dd, dd, this, doValidation);
}
}
@@ -577,14 +696,20 @@ private void setField(FieldMap fields, StringField field) {
fields.setField(field);
}
- private void parseGroup(String msgType, StringField field, DataDictionary dd, FieldMap parent)
+ private void parseGroup(String msgType, StringField field, DataDictionary dd, DataDictionary parentDD, FieldMap parent, boolean doValidation)
throws InvalidMessage {
final DataDictionary.GroupInfo rg = dd.getGroup(msgType, field.getField());
final DataDictionary groupDataDictionary = rg.getDataDictionary();
final int[] fieldOrder = groupDataDictionary.getOrderedFields();
int previousOffset = -1;
final int groupCountTag = field.getField();
- final int declaredGroupCount = Integer.parseInt(field.getValue());
+ // QFJ-533
+ int declaredGroupCount = 0;
+ try {
+ declaredGroupCount = Integer.parseInt(field.getValue());
+ } catch (final NumberFormatException e) {
+ throw new InvalidMessage("Repeating group count requires an Integer but found: " + field.getValue(), e);
+ }
parent.setField(groupCountTag, field);
final int firstField = rg.getDelimiterField();
boolean firstFieldFound = false;
@@ -598,33 +723,28 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Fi
}
int tag = field.getTag();
if (tag == firstField) {
- if (group != null) {
- parent.addGroupRef(group);
- }
+ addGroupRefToParent(group, parent);
group = new Group(groupCountTag, firstField, groupDataDictionary.getOrderedFields());
group.setField(field);
firstFieldFound = true;
previousOffset = -1;
// QFJ-742
if (groupDataDictionary.isGroup(msgType, tag)) {
- parseGroup(msgType, field, groupDataDictionary, group);
+ parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation);
}
} else if (groupDataDictionary.isGroup(msgType, tag)) {
- if (!firstFieldFound) {
- throw new InvalidMessage("The group " + groupCountTag
- + " must set the delimiter field " + firstField + " in " + messageData);
- }
- parseGroup(msgType, field, groupDataDictionary, group);
+ // QFJ-934: message should be rejected and not ignored when first field not found
+ checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag);
+ parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation);
} else if (groupDataDictionary.isField(tag)) {
- if (!firstFieldFound) {
- throw new FieldException(
- SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag);
- }
-
+ checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag);
if (fieldOrder != null && dd.isCheckUnorderedGroupFields()) {
final int offset = indexOf(tag, fieldOrder);
if (offset > -1) {
if (offset <= previousOffset) {
+ // QFJ-792: add what we've already got and leave the rest to the validation (if enabled)
+ group.setField(field);
+ addGroupRefToParent(group, parent);
throw new FieldException(
SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag);
}
@@ -633,16 +753,49 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Fi
}
group.setField(field);
} else {
+ // QFJ-169/QFJ-791: handle unknown repeating group fields in the body
+ if (!isTrailerField(tag) && !(DataDictionary.HEADER_ID.equals(msgType))) {
+ if (checkFieldValidation(parent, parentDD, field, msgType, doValidation, group)) {
+ continue;
+ }
+ }
pushBack(field);
inGroupParse = false;
}
}
// add what we've already got and leave the rest to the validation (if enabled)
+ addGroupRefToParent(group, parent);
+ // For later validation that the group size matches the parsed group count
+ parent.setGroupCount(groupCountTag, declaredGroupCount);
+ }
+
+ private void addGroupRefToParent(Group group, FieldMap parent) {
if (group != null) {
parent.addGroupRef(group);
}
- // For later validation that the group size matches the parsed group count
- parent.setGroupCount(groupCountTag, declaredGroupCount);
+ }
+
+ private void checkFirstFieldFound(boolean firstFieldFound, final int groupCountTag, final int firstField, int tag) throws FieldException {
+ if (!firstFieldFound) {
+ throw new FieldException(
+ SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, "The group " + groupCountTag
+ + " must set the delimiter field " + firstField, tag);
+ }
+ }
+
+ private boolean checkFieldValidation(FieldMap parent, DataDictionary parentDD, StringField field, String msgType, boolean doValidation, Group group) throws FieldException {
+ boolean isField = (parent instanceof Group) ? parentDD.isField(field.getTag()) : parentDD.isMsgField(msgType, field.getTag());
+ if (!isField) {
+ if (doValidation) {
+ boolean fail = parentDD.checkFieldFailure(field.getTag(), false);
+ if (fail) {
+ throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getTag());
+ }
+ }
+ group.setField(field);
+ return true;
+ }
+ return false;
}
private void parseTrailer(DataDictionary dd) throws InvalidMessage {
@@ -692,6 +845,7 @@ static boolean isHeaderField(int field) {
case OnBehalfOfSendingTime.FIELD:
case ApplVerID.FIELD:
case CstmApplVerID.FIELD:
+ case ApplExtID.FIELD:
case NoHops.FIELD:
return true;
default:
@@ -769,7 +923,7 @@ private StringField extractField(DataDictionary dataDictionary, FieldMap fields)
try {
fieldLength = fields.getInt(lengthField);
} catch (final FieldNotFound e) {
- throw new InvalidMessage("Tag " + e.field + " not found in " + messageData);
+ throw new InvalidMessage("Did not find length field " + e.field + " required to parse data field " + tag + " in " + messageData);
}
// since length is in bytes but data is a string, and it may also contain an SOH,
@@ -794,11 +948,11 @@ private StringField extractField(DataDictionary dataDictionary, FieldMap fields)
*
* @return flag indicating whether the message has a valid structure
*/
- synchronized boolean hasValidStructure() {
+ boolean hasValidStructure() {
return exception == null;
}
- public synchronized FieldException getException() {
+ public FieldException getException() {
return exception;
}
@@ -808,7 +962,7 @@ public synchronized FieldException getException() {
*
* @return the first invalid tag
*/
- synchronized int getInvalidTag() {
+ int getInvalidTag() {
return exception != null ? exception.getField() : 0;
}
@@ -827,4 +981,5 @@ public static MsgType identifyType(String message) throws MessageParseError {
}
}
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/MessageCracker.java b/quickfixj-core/src/main/java/quickfix/MessageCracker.java
index 3874d6ea0..28b65dac1 100644
--- a/quickfixj-core/src/main/java/quickfix/MessageCracker.java
+++ b/quickfixj-core/src/main/java/quickfix/MessageCracker.java
@@ -34,7 +34,7 @@
* type-safe onMessage methods.
*/
public class MessageCracker {
- private final Map, Invoker> invokers = new HashMap, Invoker>();
+ private final Map, Invoker> invokers = new HashMap<>();
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@@ -128,11 +128,7 @@ public void crack(quickfix.Message message, SessionID sessionID) throws Unsuppor
} catch (InvocationTargetException ite) {
try {
throw ite.getTargetException();
- } catch (UnsupportedMessageType e) {
- throw e;
- } catch (FieldNotFound e) {
- throw e;
- } catch (IncorrectTagValue e) {
+ } catch (UnsupportedMessageType | IncorrectTagValue | FieldNotFound e) {
throw e;
} catch (Throwable t) {
propagate(t);
diff --git a/quickfixj-core/src/main/java/quickfix/MessageFactory.java b/quickfixj-core/src/main/java/quickfix/MessageFactory.java
index eb90d23f3..cf734f831 100644
--- a/quickfixj-core/src/main/java/quickfix/MessageFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/MessageFactory.java
@@ -19,6 +19,8 @@
package quickfix;
+import quickfix.field.ApplVerID;
+
/**
* Used by a Session to create a Message.
*
@@ -35,6 +37,18 @@ public interface MessageFactory {
*/
Message create(String beginString, String msgType);
+ /**
+ * Creates a message for a specified type, FIX version, and ApplVerID.
+ *
+ * @param beginString the FIX version (for example, "FIX.4.2")
+ * @param applVerID the ApplVerID (for example "6" for FIX44)
+ * @param msgType the FIX message type (for example, "D" for an order)
+ * @return a message instance
+ */
+ default Message create(String beginString, ApplVerID applVerID, String msgType) {
+ return create(beginString, msgType);
+ }
+
/**
* Creates a group for the specified parent message type and
* for the fields with the corresponding field ID
@@ -50,5 +64,5 @@ public interface MessageFactory {
* @param correspondingFieldID the fieldID of the field in the group
* @return group, or null if the group can't be created.
*/
- public Group create(String beginString, String msgType, int correspondingFieldID);
+ Group create(String beginString, String msgType, int correspondingFieldID);
}
diff --git a/quickfixj-core/src/main/java/quickfix/MessageUtils.java b/quickfixj-core/src/main/java/quickfix/MessageUtils.java
index d22766945..01ade452a 100644
--- a/quickfixj-core/src/main/java/quickfix/MessageUtils.java
+++ b/quickfixj-core/src/main/java/quickfix/MessageUtils.java
@@ -142,7 +142,7 @@ public static Message parse(Session session, String messageString) throws Invali
final DataDictionary applicationDataDictionary = ddProvider == null ? null : ddProvider
.getApplicationDataDictionary(applVerID);
- final quickfix.Message message = messageFactory.create(beginString, msgType);
+ final quickfix.Message message = messageFactory.create(beginString, applVerID, msgType);
final DataDictionary payloadDictionary = MessageUtils.isAdminMessage(msgType)
? sessionDataDictionary
: applicationDataDictionary;
diff --git a/quickfixj-core/src/main/java/quickfix/NoopStore.java b/quickfixj-core/src/main/java/quickfix/NoopStore.java
new file mode 100644
index 000000000..ddc33b54c
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/NoopStore.java
@@ -0,0 +1,80 @@
+/*
+ ******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+import java.util.Collection;
+import java.util.Date;
+
+/**
+ * No-op message store implementation.
+ *
+ * @see quickfix.MemoryStoreFactory
+ */
+public class NoopStore implements MessageStore {
+
+ private Date creationTime = new Date();
+ private int nextSenderMsgSeqNum = 1;
+ private int nextTargetMsgSeqNum = 1;
+
+ public void get(int startSequence, int endSequence, Collection messages) {
+ }
+
+ public Date getCreationTime() {
+ return creationTime;
+ }
+
+ public int getNextSenderMsgSeqNum() {
+ return nextSenderMsgSeqNum;
+ }
+
+ public int getNextTargetMsgSeqNum() {
+ return nextTargetMsgSeqNum;
+ }
+
+ public void incrNextSenderMsgSeqNum() {
+ nextSenderMsgSeqNum++;
+ }
+
+ public void incrNextTargetMsgSeqNum() {
+ nextTargetMsgSeqNum++;
+ }
+
+ public void reset() {
+ creationTime = new Date();
+ nextSenderMsgSeqNum = 1;
+ nextTargetMsgSeqNum = 1;
+ }
+
+ public boolean set(int sequence, String message) {
+ return true;
+ }
+
+ public void setNextSenderMsgSeqNum(int next) {
+ nextSenderMsgSeqNum = next;
+ }
+
+ public void setNextTargetMsgSeqNum(int next) {
+ nextTargetMsgSeqNum = next;
+ }
+
+ public void refresh() {
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java b/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java
new file mode 100644
index 000000000..2ec3cec08
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java
@@ -0,0 +1,34 @@
+/*
+ ******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+/**
+ * Creates a no-op message store.
+ *
+ * @see MessageStore
+ */
+public final class NoopStoreFactory implements MessageStoreFactory {
+
+ @Override
+ public MessageStore create(SessionID sessionID) {
+ return new NoopStore();
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/NumbersCache.java b/quickfixj-core/src/main/java/quickfix/NumbersCache.java
new file mode 100644
index 000000000..1a3725884
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/NumbersCache.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+import java.util.ArrayList;
+
+/**
+ * A cache for commonly used strings representing numbers.
+ * Holds values from 0 to 99999.
+ */
+public final class NumbersCache {
+
+ private static final int LITTLE_NUMBERS_LENGTH = 100000;
+ private static final ArrayList LITTLE_NUMBERS;
+
+ static {
+ LITTLE_NUMBERS = new ArrayList<>(LITTLE_NUMBERS_LENGTH);
+ for (int i = 0; i < LITTLE_NUMBERS_LENGTH; i++)
+ LITTLE_NUMBERS.add(Integer.toString(i));
+ }
+
+ /**
+ * Get the String representing the given number
+ *
+ * @param i the long to convert
+ * @return the String representing the long
+ */
+ public static String get(int i) {
+ if (i < LITTLE_NUMBERS_LENGTH)
+ return LITTLE_NUMBERS.get(i);
+ return String.valueOf(i);
+ }
+
+ /**
+ * Get the string representing the given double if it's an integer
+ *
+ * @param d the double to convert
+ * @return the String representing the double or null if the double is not an integer
+ */
+ public static String get(double d) {
+ long l = (long)d;
+ if (d == (double)l)
+ return get(l);
+ return null;
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java
index 33fa52cd9..58c1a0798 100644
--- a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java
+++ b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java
@@ -47,26 +47,24 @@ public class SLF4JLog extends AbstractLog {
private final Logger outgoingMsgLog;
- private final String logPrefix;
-
private final String callerFQCN;
public SLF4JLog(SessionID sessionID, String eventCategory, String errorEventCategory,
- String incomingMsgCategory, String outgoingMsgCategory, boolean prependSessionID,
- boolean logHeartbeats, String inCallerFQCN) {
+ String incomingMsgCategory, String outgoingMsgCategory, boolean prependSessionID,
+ boolean logHeartbeats, String inCallerFQCN) {
setLogHeartbeats(logHeartbeats);
- logPrefix = prependSessionID ? (sessionID + ": ") : null;
- eventLog = getLogger(sessionID, eventCategory, DEFAULT_EVENT_CATEGORY);
- errorEventLog = getLogger(sessionID, errorEventCategory, DEFAULT_ERROR_EVENT_CATEGORY);
- incomingMsgLog = getLogger(sessionID, incomingMsgCategory, DEFAULT_INCOMING_MSG_CATEGORY);
- outgoingMsgLog = getLogger(sessionID, outgoingMsgCategory, DEFAULT_OUTGOING_MSG_CATEGORY);
+ String logPrefix = prependSessionID ? (sessionID + ": ") : "";
+ eventLog = getLogger(sessionID, eventCategory, DEFAULT_EVENT_CATEGORY, logPrefix);
+ errorEventLog = getLogger(sessionID, errorEventCategory, DEFAULT_ERROR_EVENT_CATEGORY, logPrefix);
+ incomingMsgLog = getLogger(sessionID, incomingMsgCategory, DEFAULT_INCOMING_MSG_CATEGORY, logPrefix);
+ outgoingMsgLog = getLogger(sessionID, outgoingMsgCategory, DEFAULT_OUTGOING_MSG_CATEGORY, logPrefix);
callerFQCN = inCallerFQCN;
}
- private Logger getLogger(SessionID sessionID, String category, String defaultCategory) {
- return LoggerFactory.getLogger(category != null
+ private Logger getLogger(SessionID sessionID, String category, String defaultCategory, String logPrefix) {
+ return LoggerFactory.getLogger((category != null
? substituteVariables(sessionID, category)
- : defaultCategory);
+ : defaultCategory) + logPrefix);
}
private static final String FIX_MAJOR_VERSION_VAR = "\\$\\{fixMajorVersion}";
@@ -134,23 +132,23 @@ protected void logOutgoing(String message) {
*/
protected void log(org.slf4j.Logger log, String text) {
if (log.isInfoEnabled()) {
- final String message = logPrefix != null ? (logPrefix + text) : text;
if (log instanceof LocationAwareLogger) {
final LocationAwareLogger la = (LocationAwareLogger) log;
- la.log(null, callerFQCN, LocationAwareLogger.INFO_INT, message, null, null);
+ la.log(null, callerFQCN, LocationAwareLogger.INFO_INT, text, null, null);
} else {
- log.info(message);
+ log.info(text);
}
}
}
protected void logError(org.slf4j.Logger log, String text) {
- final String message = logPrefix != null ? (logPrefix + text) : text;
- log.error(message);
+ log.error(text);
}
+ private final String clearString = "Log clear operation is not supported: " + getClass().getName();
+
public void clear() {
- onEvent("Log clear operation is not supported: " + getClass().getName());
+ onEvent(clearString);
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java b/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java
index 635d7447d..104b87c64 100644
--- a/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java
@@ -104,8 +104,4 @@ public Log create(SessionID sessionID, String callerFQCN) {
prependSessionID, logHeartbeats, callerFQCN);
}
- public Log create() {
- throw new UnsupportedOperationException();
- }
-
}
diff --git a/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java b/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java
index e2b8dec63..b6d97e1b9 100644
--- a/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java
@@ -126,9 +126,7 @@ public Log create(SessionID sessionID) {
includeMillis = getBooleanSetting(sessionID,
ScreenLogFactory.SETTING_INCLUDE_MILLIS_IN_TIMESTAMP, false);
return new ScreenLog(incoming, outgoing, events, heartBeats, includeMillis, sessionID, System.out);
- } catch (FieldConvertError e) {
- throw new RuntimeError(e);
- } catch (ConfigError e) {
+ } catch (FieldConvertError | ConfigError e) {
throw new RuntimeError(e);
}
}
@@ -141,8 +139,4 @@ private boolean getBooleanSetting(SessionID sessionID, String key, boolean incom
return incoming;
}
- public Log create() {
- throw new UnsupportedOperationException();
- }
-
}
diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java
index b30c87652..23f7665ca 100644
--- a/quickfixj-core/src/main/java/quickfix/Session.java
+++ b/quickfixj-core/src/main/java/quickfix/Session.java
@@ -19,23 +19,8 @@
package quickfix;
-import static quickfix.LogUtil.logThrowable;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import quickfix.Message.Header;
import quickfix.SessionState.ResendRange;
import quickfix.field.ApplVerID;
@@ -71,6 +56,22 @@
import quickfix.field.Text;
import quickfix.mina.EventHandlingStrategy;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static quickfix.LogUtil.logThrowable;
+
/**
* The Session is the primary FIX abstraction for message communication.
*
@@ -146,9 +147,14 @@ public class Session implements Closeable {
*/
public static final String SETTING_END_TIME = "EndTime";
+ /**
+ * Session scheduling setting to specify active days of the week.
+ */
+ public static final String SETTING_WEEKDAYS = "Weekdays";
+
/**
* Session setting to indicate whether a data dictionary should be used. If
- * a data dictionary is not used then message validation is not possble.
+ * a data dictionary is not used then message validation is not possible.
*/
public static final String SETTING_USE_DATA_DICTIONARY = "UseDataDictionary";
@@ -253,10 +259,11 @@ public class Session implements Closeable {
public static final String SETTING_DISCONNECT_ON_ERROR = "DisconnectOnError";
/**
- * Session setting to enable milliseconds in message timestamps. Valid
- * values are "Y" or "N". Default is "Y". Only valid for FIX version >= 4.2.
+ * Session setting to control precision in message timestamps.
+ * Valid values are "SECONDS", "MILLIS", "MICROS", "NANOS". Default is "MILLIS".
+ * Only valid for FIX version >= 4.2.
*/
- public static final String SETTING_MILLISECONDS_IN_TIMESTAMP = "MillisecondsInTimeStamp";
+ public static final String SETTING_TIMESTAMP_PRECISION = "TimeStampPrecision";
/**
* Controls validation of user-defined fields.
@@ -344,7 +351,7 @@ public class Session implements Closeable {
public static final String SETTING_MAX_SCHEDULED_WRITE_REQUESTS = "MaxScheduledWriteRequests";
- private static final ConcurrentMap sessions = new ConcurrentHashMap();
+ private static final ConcurrentMap sessions = new ConcurrentHashMap<>();
private final Application application;
private final SessionID sessionID;
@@ -375,7 +382,7 @@ public class Session implements Closeable {
private final boolean resetOnDisconnect;
private final boolean resetOnError;
private final boolean disconnectOnError;
- private final boolean millisecondsInTimeStamp;
+ private final UtcTimestampPrecision timestampPrecision;
private final boolean refreshMessageStoreAtLogon;
private final boolean redundantResentRequestsAllowed;
private final boolean persistMessages;
@@ -392,30 +399,39 @@ public class Session implements Closeable {
private int maxScheduledWriteRequests = 0;
private final AtomicBoolean isResetting = new AtomicBoolean();
+ private final AtomicBoolean isResettingState = new AtomicBoolean();
private final ListenerSupport stateListeners = new ListenerSupport(SessionStateListener.class);
private final SessionStateListener stateListener = (SessionStateListener) stateListeners
.getMulticaster();
- private final AtomicReference targetDefaultApplVerID = new AtomicReference();
+ private final AtomicReference targetDefaultApplVerID = new AtomicReference<>();
private final DefaultApplVerID senderDefaultApplVerID;
private boolean validateSequenceNumbers = true;
private boolean validateIncomingMessage = true;
private final int[] logonIntervals;
private final Set allowedRemoteAddresses;
-
+
public static final int DEFAULT_MAX_LATENCY = 120;
public static final int DEFAULT_RESEND_RANGE_CHUNK_SIZE = 0; // no resend range
public static final double DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER = 0.5;
private static final String ENCOUNTERED_END_OF_STREAM = "Encountered END_OF_STREAM";
- protected final static Logger log = LoggerFactory.getLogger(Session.class);
+
+ private static final int BAD_COMPID_REJ_REASON = SessionRejectReason.COMPID_PROBLEM;
+ private static final String BAD_COMPID_TEXT = new FieldException(BAD_COMPID_REJ_REASON).getMessage();
+ private static final int BAD_TIME_REJ_REASON = SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM;
+ private static final String BAD_ORIG_TIME_TEXT = new FieldException(BAD_TIME_REJ_REASON, OrigSendingTime.FIELD).getMessage();
+ private static final String BAD_TIME_TEXT = new FieldException(BAD_TIME_REJ_REASON, SendingTime.FIELD).getMessage();
+
+ protected static final Logger LOG = LoggerFactory.getLogger(Session.class);
+
Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID,
DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule,
LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval) {
this(application, messageStoreFactory, sessionID, dataDictionaryProvider, sessionSchedule,
- logFactory, messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, true,
+ logFactory, messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS,
false, false, false, false, true, false, true, false,
DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] { 5 }, false, false,
false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, false);
@@ -424,7 +440,7 @@ public class Session implements Closeable {
Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID,
DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule,
LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval,
- boolean checkLatency, int maxLatency, boolean millisecondsInTimeStamp,
+ boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision,
boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect,
boolean refreshMessageStoreAtLogon, boolean checkCompID,
boolean redundantResentRequestsAllowed, boolean persistMessages,
@@ -444,7 +460,7 @@ public class Session implements Closeable {
this.resetOnLogon = resetOnLogon;
this.resetOnLogout = resetOnLogout;
this.resetOnDisconnect = resetOnDisconnect;
- this.millisecondsInTimeStamp = millisecondsInTimeStamp;
+ this.timestampPrecision = timestampPrecision;
this.refreshMessageStoreAtLogon = refreshMessageStoreAtLogon;
this.dataDictionaryProvider = dataDictionaryProvider;
this.messageFactory = messageFactory;
@@ -579,14 +595,24 @@ public static boolean sendToTarget(Message message) throws SessionNotFound {
*/
public static boolean sendToTarget(Message message, String qualifier) throws SessionNotFound {
try {
- final String senderCompID = message.getHeader().getString(SenderCompID.FIELD);
- final String targetCompID = message.getHeader().getString(TargetCompID.FIELD);
+ final String senderCompID = getSenderCompIDFromMessage(message);
+ final String targetCompID = getTargetCompIDFromMessage(message);
return sendToTarget(message, senderCompID, targetCompID, qualifier);
} catch (final FieldNotFound e) {
throw new SessionNotFound("missing sender or target company ID");
}
}
+ private static String getTargetCompIDFromMessage(final Message message) throws FieldNotFound {
+ final String targetCompID = message.getHeader().getString(TargetCompID.FIELD);
+ return targetCompID;
+ }
+
+ private static String getSenderCompIDFromMessage(final Message message) throws FieldNotFound {
+ final String senderCompID = message.getHeader().getString(SenderCompID.FIELD);
+ return senderCompID;
+ }
+
/**
* Send a message to the session specified by the provided target company
* ID. The sender company ID is provided as an argument rather than from the
@@ -650,15 +676,23 @@ static void registerSession(Session session) {
sessions.put(session.getSessionID(), session);
}
- static void unregisterSessions(List sessionIds) {
+ static void unregisterSessions(List sessionIds, boolean doClose) {
for (final SessionID sessionId : sessionIds) {
- final Session session = sessions.remove(sessionId);
- if (session != null) {
- try {
+ unregisterSession(sessionId, doClose);
+ }
+ }
+
+ static void unregisterSession(SessionID sessionId, boolean doClose) {
+ final Session session = sessions.get(sessionId);
+ if (session != null) {
+ try {
+ if (doClose) {
session.close();
- } catch (final IOException e) {
- log.error("Failed to close session resources", e);
}
+ } catch (final IOException e) {
+ LOG.error("Failed to close session resources", e);
+ } finally {
+ sessions.remove(sessionId);
}
}
}
@@ -705,12 +739,15 @@ private void optionallySetID(Header header, int field, String value) {
}
private void insertSendingTime(Message.Header header) {
- header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getDate(), includeMillis());
+ header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getLocalDateTime(), getTimestampPrecision());
}
- private boolean includeMillis() {
- return millisecondsInTimeStamp
- && sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX42) >= 0;
+ private UtcTimestampPrecision getTimestampPrecision() {
+ if (sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) {
+ return timestampPrecision;
+ } else {
+ return UtcTimestampPrecision.SECONDS;
+ }
}
/**
@@ -900,7 +937,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
// QFJ-650
if (!header.isSetField(MsgSeqNum.FIELD)) {
generateLogout("Received message without MsgSeqNum");
- disconnect("Received message without MsgSeqNum: " + message, true);
+ disconnect("Received message without MsgSeqNum: " + getMessageToLog(message), true);
return;
}
@@ -946,7 +983,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
if (rejectInvalidMessage) {
throw e;
} else {
- getLog().onErrorEvent("Warn: incoming message with " + e + ": " + message);
+ getLog().onErrorEvent("Warn: incoming message with " + e + ": " + getMessageToLog(message));
}
} catch (final FieldException e) {
if (message.isSetField(e.getField())) {
@@ -955,7 +992,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
} else {
getLog().onErrorEvent(
"Warn: incoming message with incorrect field: "
- + message.getField(e.getField()) + ": " + message);
+ + message.getField(e.getField()) + ": " + getMessageToLog(message));
}
} else {
if (rejectInvalidMessage) {
@@ -963,47 +1000,54 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
} else {
getLog().onErrorEvent(
"Warn: incoming message with missing field: " + e.getField()
- + ": " + e.getMessage() + ": " + message);
+ + ": " + e.getMessage() + ": " + getMessageToLog(message));
}
}
} catch (final FieldNotFound e) {
if (rejectInvalidMessage) {
throw e;
} else {
- getLog().onErrorEvent("Warn: incoming " + e + ": " + message);
+ getLog().onErrorEvent("Warn: incoming " + e + ": " + getMessageToLog(message));
}
}
}
- if (msgType.equals(MsgType.LOGON)) {
- nextLogon(message);
- } else if (msgType.equals(MsgType.HEARTBEAT)) {
- nextHeartBeat(message);
- } else if (msgType.equals(MsgType.TEST_REQUEST)) {
- nextTestRequest(message);
- } else if (msgType.equals(MsgType.SEQUENCE_RESET)) {
- nextSequenceReset(message);
- } else if (msgType.equals(MsgType.LOGOUT)) {
- nextLogout(message);
- } else if (msgType.equals(MsgType.RESEND_REQUEST)) {
- nextResendRequest(message);
- } else if (msgType.equals(MsgType.REJECT)) {
- nextReject(message);
- } else {
- if (!verify(message)) {
- return;
- }
- state.incrNextTargetMsgSeqNum();
+ switch (msgType) {
+ case MsgType.LOGON:
+ nextLogon(message);
+ break;
+ case MsgType.HEARTBEAT:
+ nextHeartBeat(message);
+ break;
+ case MsgType.TEST_REQUEST:
+ nextTestRequest(message);
+ break;
+ case MsgType.SEQUENCE_RESET:
+ nextSequenceReset(message);
+ break;
+ case MsgType.LOGOUT:
+ nextLogout(message);
+ break;
+ case MsgType.RESEND_REQUEST:
+ nextResendRequest(message);
+ break;
+ case MsgType.REJECT:
+ nextReject(message);
+ break;
+ default:
+ if (!verify(message)) {
+ return;
+ }
+ state.incrNextTargetMsgSeqNum();
+ break;
}
- } catch (final FieldException e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- if (resetOrDisconnectIfRequired(message)) {
+ } catch (final FieldException | IncorrectDataFormat | IncorrectTagValue e) {
+ if (logErrorAndDisconnectIfRequired(e, message)) {
return;
}
- generateReject(message, e.getSessionRejectReason(), e.getField());
+ handleExceptionAndRejectMessage(msgType, message, e);
} catch (final FieldNotFound e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- if (resetOrDisconnectIfRequired(message)) {
+ if (logErrorAndDisconnectIfRequired(e, message)) {
return;
}
if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0
@@ -1018,17 +1062,12 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
generateReject(message, SessionRejectReason.REQUIRED_TAG_MISSING, e.field);
}
}
- } catch (final IncorrectDataFormat e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- if (resetOrDisconnectIfRequired(message)) {
- return;
- }
- generateReject(message, SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE, e.field);
- } catch (final IncorrectTagValue e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- generateReject(message, SessionRejectReason.VALUE_IS_INCORRECT, e.field);
} catch (final InvalidMessage e) {
- getLog().onErrorEvent("Skipping invalid message: " + e + ": " + message);
+ /* InvalidMessage means a low-level error (e.g. checksum problem) and we should
+ ignore the message and let the problem correct itself (optimistic approach).
+ Target sequence number is not incremented, so it will trigger a ResendRequest
+ on the next message that is received. */
+ getLog().onErrorEvent("Skipping invalid message: " + e + ": " + getMessageToLog(message));
if (resetOrDisconnectIfRequired(message)) {
return;
}
@@ -1042,11 +1081,13 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
generateLogout(e.getMessage());
}
}
- state.incrNextTargetMsgSeqNum();
+ // Only increment seqnum if we are at the expected seqnum
+ if (getExpectedTargetNum() == header.getInt(MsgSeqNum.FIELD)) {
+ state.incrNextTargetMsgSeqNum();
+ }
disconnect("Logon rejected: " + e, true);
} catch (final UnsupportedMessageType e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- if (resetOrDisconnectIfRequired(message)) {
+ if (logErrorAndDisconnectIfRequired(e, message)) {
return;
}
if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) {
@@ -1055,8 +1096,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
generateReject(message, "Unsupported message type");
}
} catch (final UnsupportedVersion e) {
- getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message);
- if (resetOrDisconnectIfRequired(message)) {
+ if (logErrorAndDisconnectIfRequired(e, message)) {
return;
}
if (msgType.equals(MsgType.LOGOUT)) {
@@ -1070,7 +1110,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
disconnect("Incorrect BeginString: " + e, true);
}
} catch (final IOException e) {
- LogUtil.logThrowable(sessionID, "Error processing message: " + message, e);
+ LogUtil.logThrowable(sessionID, "Error processing message: " + getMessageToLog(message), e);
if (resetOrDisconnectIfRequired(message)) {
return;
}
@@ -1078,7 +1118,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
// If there are any other Throwables we might catch them here if desired.
// They were most probably thrown out of fromCallback().
if (rejectMessageOnUnhandledException) {
- getLog().onErrorEvent("Rejecting message: " + t + ": " + message);
+ getLog().onErrorEvent("Rejecting message: " + t + ": " + getMessageToLog(message));
if (resetOrDisconnectIfRequired(message)) {
return;
}
@@ -1111,6 +1151,30 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi
}
}
+ private void handleExceptionAndRejectMessage(final String msgType, final Message message, final HasFieldAndReason e) throws FieldNotFound, IOException {
+ if (MsgType.LOGON.equals(msgType)) {
+ logoutWithErrorMessage(e.getMessage());
+ } else {
+ getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + getMessageToLog(message));
+ generateReject(message, e.getMessage(), e.getSessionRejectReason(), e.getField());
+ }
+ }
+
+ private void logoutWithErrorMessage(final String reason) throws IOException {
+ final String errorMessage = "Invalid Logon message: " + (reason != null ? reason : "unspecific reason");
+ generateLogout(errorMessage);
+ state.incrNextTargetMsgSeqNum();
+ disconnect(errorMessage, true);
+ }
+
+ private boolean logErrorAndDisconnectIfRequired(final Exception e, Message message) {
+ final boolean resetOrDisconnectIfRequired = resetOrDisconnectIfRequired(message);
+ if (resetOrDisconnectIfRequired) {
+ getLog().onErrorEvent("Encountered invalid message: " + e + ": " + getMessageToLog(message));
+ }
+ return resetOrDisconnectIfRequired;
+ }
+
/**
* (Internal use only)
*/
@@ -1136,7 +1200,7 @@ private boolean resetOrDisconnectIfRequired(Message msg) {
getLog().onErrorEvent("Auto reset");
reset();
} catch (final IOException e) {
- log.error("Failed reseting: " + e);
+ LOG.error("Failed resetting: {}", e);
}
return true;
}
@@ -1144,7 +1208,7 @@ private boolean resetOrDisconnectIfRequired(Message msg) {
try {
disconnect("Auto disconnect", false);
} catch (final IOException e) {
- log.error("Failed disconnecting: " + e);
+ LOG.error("Failed disconnecting: {}", e);
}
return true;
}
@@ -1257,7 +1321,7 @@ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int
header.setBoolean(PossDupFlag.FIELD, true);
initializeHeader(header);
header.setUtcTimeStamp(OrigSendingTime.FIELD, header.getUtcTimeStamp(SendingTime.FIELD),
- includeMillis());
+ getTimestampPrecision());
header.setInt(MsgSeqNum.FIELD, beginSeqNo);
sequenceReset.setInt(NewSeqNo.FIELD, newSeqNo);
sequenceReset.setBoolean(GapFillFlag.FIELD, true);
@@ -1267,7 +1331,7 @@ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int
receivedMessage.getHeader().getInt(MsgSeqNum.FIELD));
} catch (final FieldNotFound e) {
// should not happen as MsgSeqNum must be present
- getLog().onErrorEvent("Received message without MsgSeqNum " + receivedMessage);
+ getLog().onErrorEvent("Received message without MsgSeqNum " + getMessageToLog(receivedMessage));
}
}
sendRaw(sequenceReset, beginSeqNo);
@@ -1289,8 +1353,8 @@ private boolean resendApproved(Message message) throws FieldNotFound {
private void initializeResendFields(Message message) throws FieldNotFound {
final Message.Header header = message.getHeader();
- final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD);
- header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime, includeMillis());
+ final LocalDateTime sendingTime = header.getUtcTimeStamp(SendingTime.FIELD);
+ header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime, getTimestampPrecision());
header.setBoolean(PossDupFlag.FIELD, true);
insertSendingTime(header);
}
@@ -1452,7 +1516,7 @@ private void generateReject(Message message, String str) throws FieldNotFound, I
reject.setString(Text.FIELD, str);
sendRaw(reject, 0);
- getLog().onErrorEvent("Reject sent for Message " + msgSeqNum + ": " + str);
+ getLog().onErrorEvent("Reject sent for message " + msgSeqNum + ": " + str);
}
private boolean isPossibleDuplicate(Message message) throws FieldNotFound {
@@ -1462,10 +1526,20 @@ private boolean isPossibleDuplicate(Message message) throws FieldNotFound {
private void generateReject(Message message, int err, int field) throws IOException,
FieldNotFound {
- final String reason = SessionRejectReasonText.getMessage(err);
+ generateReject(message, null, err, field);
+ }
+
+ private void generateReject(Message message, String text, int err, int field) throws IOException,
+ FieldNotFound {
+ final String reason;
+ if (text != null) {
+ reason = text;
+ } else {
+ reason = SessionRejectReasonText.getMessage(err);
+ }
if (!state.isLogonReceived()) {
final String errorMessage = "Tried to send a reject while not logged on: " + reason
- + " (field " + field + ")";
+ + (reason.endsWith("" + field) ? "" : " (field " + field + ")");
throw new SessionException(errorMessage);
}
@@ -1522,16 +1596,15 @@ private void generateReject(Message message, int err, int field) throws IOExcept
} finally {
state.unlockTargetMsgSeqNum();
}
-
+ final String logMessage = "Reject sent for message " + msgSeqNum;
if (reason != null && (field > 0 || err == SessionRejectReason.INVALID_TAG_NUMBER)) {
setRejectReason(reject, field, reason, true);
- getLog().onErrorEvent(
- "Reject sent for Message " + msgSeqNum + ": " + reason + ":" + field);
+ getLog().onErrorEvent(logMessage + ": " + reason + (reason.endsWith("" + field) ? "" : ":" + field));
} else if (reason != null) {
setRejectReason(reject, reason);
- getLog().onErrorEvent("Reject sent for Message " + msgSeqNum + ": " + reason);
+ getLog().onErrorEvent(logMessage + ": " + reason);
} else {
- getLog().onErrorEvent("Reject sent for Message " + msgSeqNum);
+ getLog().onErrorEvent(logMessage);
}
if (enableLastMsgSeqNumProcessed) {
@@ -1559,7 +1632,11 @@ private void setRejectReason(Message reject, int field, String reason,
reject.setInt(RefTagID.FIELD, field);
reject.setString(Text.FIELD, reason);
} else {
- reject.setString(Text.FIELD, reason + (includeFieldInReason ? " (" + field + ")" : ""));
+ String rejectReason = reason;
+ if (includeFieldInReason && !rejectReason.endsWith("" + field) ) {
+ rejectReason = rejectReason + ", field=" + field;
+ }
+ reject.setString(Text.FIELD, rejectReason);
}
}
@@ -1581,7 +1658,7 @@ private void generateBusinessReject(Message message, int err, int field) throws
final String reason = BusinessRejectReasonText.getMessage(err);
setRejectReason(reject, field, reason, field != 0);
getLog().onErrorEvent(
- "Reject sent for Message " + msgSeqNum + (reason != null ? (": " + reason) : "")
+ "Reject sent for message " + msgSeqNum + (reason != null ? (": " + reason) : "")
+ (field != 0 ? (": tag=" + field) : ""));
sendRaw(reject, 0);
@@ -1699,7 +1776,6 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow)
private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException {
if (!isPossibleDuplicate(msg)) {
final int msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD);
-
final String text = "MsgSeqNum too low, expecting " + getExpectedTargetNum()
+ " but received " + msgSeqNum;
generateLogout(text);
@@ -1709,14 +1785,22 @@ private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException {
}
private void doBadCompID(Message msg) throws IOException, FieldNotFound {
- generateReject(msg, SessionRejectReason.COMPID_PROBLEM, 0);
- generateLogout();
+ if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) {
+ generateReject(msg, BAD_COMPID_REJ_REASON, 0);
+ generateLogout(BAD_COMPID_TEXT);
+ } else {
+ logoutWithErrorMessage(BAD_COMPID_TEXT);
+ }
}
private void doBadTime(Message msg) throws IOException, FieldNotFound {
try {
- generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0);
- generateLogout();
+ if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) {
+ generateReject(msg, BAD_TIME_REJ_REASON, SendingTime.FIELD);
+ generateLogout(BAD_TIME_TEXT);
+ } else {
+ logoutWithErrorMessage(BAD_TIME_TEXT);
+ }
} catch (final SessionException ex) {
generateLogout(ex.getMessage());
throw ex;
@@ -1727,8 +1811,8 @@ private boolean isGoodTime(Message message) throws FieldNotFound {
if (!checkLatency) {
return true;
}
- final Date sendingTime = message.getHeader().getUtcTimeStamp(SendingTime.FIELD);
- return Math.abs(SystemTime.currentTimeMillis() - sendingTime.getTime()) / 1000 <= maxLatency;
+ final LocalDateTime sendingTime = message.getHeader().getUtcTimeStamp(SendingTime.FIELD);
+ return Math.abs(SystemTime.currentTimeMillis() - sendingTime.toInstant(ZoneOffset.UTC).toEpochMilli()) / 1000 <= maxLatency;
}
private void fromCallback(String msgType, Message msg, SessionID sessionID2)
@@ -1790,6 +1874,7 @@ public void next() throws IOException {
}
return; // since we are outside of session time window
} else {
+ // reset when session becomes active
resetIfSessionNotCurrent(sessionID, now);
}
}
@@ -1809,6 +1894,8 @@ public void next() throws IOException {
return;
}
}
+ // QFJ-926 - reset session before initiating Logon
+ resetIfSessionNotCurrent(sessionID, SystemTime.currentTimeMillis());
if (generateLogon()) {
getLog().onEvent("Initiated logon request");
} else {
@@ -1834,7 +1921,7 @@ public void next() throws IOException {
disconnect("Timed out waiting for heartbeat", true);
stateListener.onHeartBeatTimeout();
} else {
- log.warn("Heartbeat failure detected but deactivated");
+ LOG.warn("Heartbeat failure detected but deactivated");
}
} else {
if (state.isTestRequestNeeded()) {
@@ -1912,25 +1999,23 @@ private boolean generateLogon() throws IOException {
return sendRaw(logon, 0);
}
- /**
- * Use disconnect(reason, logError) instead.
- *
- * @deprecated
- */
- @Deprecated
- public void disconnect() throws IOException {
- disconnect("Other reason", true);
- }
-
/**
* Logs out from session and closes the network connection.
- *
+ *
+ * This method should not be called from user-code since it is likely
+ * to deadlock when called from a different thread than the Session thread
+ * and messages are sent/received concurrently.
+ * Instead the logout() method should be used where possible.
+ *
* @param reason the reason why the session is disconnected
* @param logError set to true if this disconnection is an error
* @throws IOException IO error
*/
public void disconnect(String reason, boolean logError) throws IOException {
try {
+ final boolean logonReceived = state.isLogonReceived();
+ final boolean logonSent = state.isLogonSent();
+
synchronized (responderLock) {
if (!hasResponder()) {
if (!ENCOUNTERED_END_OF_STREAM.equals(reason)) {
@@ -1942,14 +2027,12 @@ public void disconnect(String reason, boolean logError) throws IOException {
if (logError) {
getLog().onErrorEvent(msg);
} else {
- log.info("[" + getSessionID() + "] " + msg);
+ getLog().onEvent(msg);
}
responder.disconnect();
setResponder(null);
}
- final boolean logonReceived = state.isLogonReceived();
- final boolean logonSent = state.isLogonSent();
if (logonReceived || logonSent) {
try {
application.onLogout(sessionID);
@@ -1959,11 +2042,12 @@ public void disconnect(String reason, boolean logError) throws IOException {
stateListener.onLogout();
}
+ } finally {
// QFJ-457 now enabled again if acceptor
if (!state.isInitiator()) {
setEnabled(true);
}
- } finally {
+
state.setLogonReceived(false);
state.setLogonSent(false);
state.setLogoutSent(false);
@@ -1990,6 +2074,9 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre
throw new RejectLogon("Logon attempt not within session time");
}
+ // QFJ-926 - reset session before accepting Logon
+ resetIfSessionNotCurrent(sessionID, SystemTime.currentTimeMillis());
+
if (isStateRefreshNeeded(MsgType.LOGON)) {
getLog().onEvent("Refreshing message/state store at logon");
getStore().refresh();
@@ -2042,7 +2129,13 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre
if (logon.isSetField(NextExpectedMsgSeqNum.FIELD) && enableNextExpectedMsgSeqNum) {
final int targetWantsNextSeqNumToBe = logon.getInt(NextExpectedMsgSeqNum.FIELD);
- final int actualNextNum = state.getMessageStore().getNextSenderMsgSeqNum();
+ state.lockSenderMsgSeqNum();
+ final int actualNextNum;
+ try {
+ actualNextNum = state.getNextSenderMsgSeqNum();
+ } finally {
+ state.unlockSenderMsgSeqNum();
+ }
// Is the 789 we received too high ??
if (targetWantsNextSeqNumToBe > actualNextNum) {
// barf! we can't resend what we never sent! something unrecoverable has happened.
@@ -2143,12 +2236,12 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre
private void resendMessages(Message receivedMessage, int beginSeqNo, int endSeqNo)
throws IOException, InvalidMessage, FieldNotFound {
- final ArrayList messages = new ArrayList();
+ final ArrayList messages = new ArrayList<>();
try {
state.get(beginSeqNo, endSeqNo, messages);
} catch (final IOException e) {
if (forceResendWhenCorruptedStore) {
- log.error("Cannot read messages from stores, resend HeartBeats", e);
+ LOG.error("Cannot read messages from stores, resend HeartBeats", e);
for (int i = beginSeqNo; i < endSeqNo; i++) {
final Message heartbeat = messageFactory.create(sessionID.getBeginString(),
MsgType.HEARTBEAT);
@@ -2197,7 +2290,7 @@ private void resendMessages(Message receivedMessage, int beginSeqNo, int endSeqN
if (begin != 0) {
generateSequenceReset(receivedMessage, begin, msgSeqNum);
}
- getLog().onEvent("Resending Message: " + msgSeqNum);
+ getLog().onEvent("Resending message: " + msgSeqNum);
send(msg.toString());
begin = 0;
appMessageJustSent = true;
@@ -2351,11 +2444,11 @@ private boolean validatePossDup(Message msg) throws FieldNotFound, IOException {
if (!msgType.equals(MsgType.SEQUENCE_RESET)) {
if (header.isSetField(OrigSendingTime.FIELD)) {
- final Date origSendingTime = header.getUtcTimeStamp(OrigSendingTime.FIELD);
- final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD);
+ final LocalDateTime origSendingTime = header.getUtcTimeStamp(OrigSendingTime.FIELD);
+ final LocalDateTime sendingTime = header.getUtcTimeStamp(SendingTime.FIELD);
if (origSendingTime.compareTo(sendingTime) > 0) {
- generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0);
- generateLogout();
+ generateReject(msg, BAD_TIME_REJ_REASON, OrigSendingTime.FIELD);
+ generateLogout(BAD_ORIG_TIME_TEXT);
return false;
}
} else {
@@ -2491,7 +2584,7 @@ private boolean sendRaw(Message message, int num) {
return result;
} catch (final IOException e) {
- logThrowable(getLog(), "Error Reading/Writing in MessageStore", e);
+ logThrowable(getLog(), "Error reading/writing in MessageStore", e);
return false;
} catch (final FieldNotFound e) {
logThrowable(state.getLog(), "Error accessing message fields", e);
@@ -2507,8 +2600,15 @@ private void enqueueMessage(final Message msg, final int msgSeqNum) {
}
private void resetState() {
- state.reset();
- stateListener.onReset();
+ if (!isResettingState.compareAndSet(false, true)) {
+ return;
+ }
+ try {
+ state.reset();
+ stateListener.onReset();
+ } finally {
+ isResettingState.set(false);
+ }
}
/**
@@ -2548,24 +2648,12 @@ private boolean isCorrectCompID(Message message) throws FieldNotFound {
if (!checkCompID) {
return true;
}
- final String senderCompID = message.getHeader().getString(SenderCompID.FIELD);
- final String targetCompID = message.getHeader().getString(TargetCompID.FIELD);
+ final String senderCompID = getSenderCompIDFromMessage(message);
+ final String targetCompID = getTargetCompIDFromMessage(message);
return sessionID.getSenderCompID().equals(targetCompID)
&& sessionID.getTargetCompID().equals(senderCompID);
}
- /**
- * Set the data dictionary. (QF Compatibility)
- *
- * @deprecated
- * @param dataDictionary
- */
- @Deprecated
- public void setDataDictionary(DataDictionary dataDictionary) {
- throw new UnsupportedOperationException(
- "Modification of session dictionary is not supported in QFJ");
- }
-
public DataDictionary getDataDictionary() {
if (!sessionID.isFIXT()) {
// For pre-FIXT sessions, the session data dictionary is the same as the application
@@ -2758,18 +2846,18 @@ public void setTargetDefaultApplicationVersionID(ApplVerID applVerID) {
}
private static String extractNumber(String txt, int from) {
- String ret = "";
+ final StringBuilder ret = new StringBuilder(txt.length() - from);
for (int i = from; i != txt.length(); ++i) {
final char c = txt.charAt(i);
if (c >= '0' && c <= '9') {
- ret += c;
+ ret.append(c);
} else {
if (ret.length() != 0) {
break;
}
}
}
- return ret.trim();
+ return ret.toString();
}
protected static Integer extractExpectedSequenceNumber(String txt) {
@@ -2831,13 +2919,15 @@ public boolean isAllowedForSession(InetAddress remoteInetAddress) {
}
/**
- * Closes session resources. This is for internal use and should typically
- * not be called by an user application.
+ * Closes session resources and unregisters session. This is for internal
+ * use and should typically not be called by an user application.
*/
@Override
public void close() throws IOException {
closeIfCloseable(getLog());
closeIfCloseable(getStore());
+ // clean up session just in case close() was not called from Session.unregisterSession()
+ unregisterSession(this.sessionID, false);
}
private void closeIfCloseable(Object resource) throws IOException {
@@ -2853,4 +2943,8 @@ private void resetIfSessionNotCurrent(SessionID sessionID, long time) throws IOE
}
}
+ private String getMessageToLog(final Message message) {
+ return (message.toRawString() != null ? message.toRawString() : message.toString());
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/SessionFactory.java b/quickfixj-core/src/main/java/quickfix/SessionFactory.java
index 98d6e8ec5..234f26836 100644
--- a/quickfixj-core/src/main/java/quickfix/SessionFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/SessionFactory.java
@@ -28,17 +28,17 @@ public interface SessionFactory {
* Specifies the connection type for a session. Valid values are "initiator"
* and "acceptor".
*/
- public static final String SETTING_CONNECTION_TYPE = "ConnectionType";
+ String SETTING_CONNECTION_TYPE = "ConnectionType";
/**
* Instructs the connection-related code to continue if there is an error
* creating or initializing a session. In other words, one bad session won't
* stop the initialization of other sessions.
*/
- public static final String SETTING_CONTINUE_INIT_ON_ERROR = "ContinueInitializationOnError";
+ String SETTING_CONTINUE_INIT_ON_ERROR = "ContinueInitializationOnError";
- public static final String ACCEPTOR_CONNECTION_TYPE = "acceptor";
- public static final String INITIATOR_CONNECTION_TYPE = "initiator";
+ String ACCEPTOR_CONNECTION_TYPE = "acceptor";
+ String INITIATOR_CONNECTION_TYPE = "initiator";
Session create(SessionID sessionID, SessionSettings settings) throws ConfigError;
diff --git a/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java b/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java
index f790f6b28..39cbe1bf4 100644
--- a/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java
+++ b/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java
@@ -19,12 +19,12 @@
package quickfix;
-import java.util.HashMap;
-
import quickfix.field.SessionRejectReason;
+import java.util.HashMap;
+
class SessionRejectReasonText extends SessionRejectReason {
- private static final HashMap rejectReasonText = new HashMap();
+ private static final HashMap rejectReasonText = new HashMap<>();
static {
rejectReasonText.put(INVALID_TAG_NUMBER, "Invalid tag number");
diff --git a/quickfixj-core/src/main/java/quickfix/SessionSchedule.java b/quickfixj-core/src/main/java/quickfix/SessionSchedule.java
index 269640c4c..a19d612da 100644
--- a/quickfixj-core/src/main/java/quickfix/SessionSchedule.java
+++ b/quickfixj-core/src/main/java/quickfix/SessionSchedule.java
@@ -19,337 +19,28 @@
package quickfix;
-import java.text.SimpleDateFormat;
import java.util.Calendar;
-import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
- * Corresponds to SessionTime in C++ code
+ * Used to decide when to login and out of FIX sessions
*/
-public class SessionSchedule {
- private static final int NOT_SET = -1;
- private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{2}):(\\d{2}):(\\d{2})(.*)");
- private final TimeEndPoint startTime;
- private final TimeEndPoint endTime;
- private final boolean nonStopSession;
- protected final static Logger log = LoggerFactory.getLogger(SessionSchedule.class);
-
- public SessionSchedule(SessionSettings settings, SessionID sessionID) throws ConfigError,
- FieldConvertError {
-
- nonStopSession = settings.isSetting(sessionID, Session.SETTING_NON_STOP_SESSION) && settings.getBool(sessionID, Session.SETTING_NON_STOP_SESSION);
- TimeZone defaultTimeZone = getDefaultTimeZone(settings, sessionID);
- if (nonStopSession) {
- startTime = endTime = new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone);
- return;
- }
-
- boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY);
- boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY);
-
- if (startDayPresent && !endDayPresent) {
- throw new ConfigError("Session " + sessionID + ": StartDay used without EndDay");
- }
-
- if (endDayPresent && !startDayPresent) {
- throw new ConfigError("Session " + sessionID + ": EndDay used without StartDay");
- }
-
- startTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_START_TIME, Session.SETTING_START_DAY);
- endTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_END_TIME, Session.SETTING_END_DAY);
- log.info("[" + sessionID + "] " + toString());
- }
-
- private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID sessionID,
- TimeZone defaultTimeZone, String timeSetting, String daySetting) throws ConfigError,
- FieldConvertError {
-
- Matcher matcher = TIME_PATTERN.matcher(settings.getString(sessionID, timeSetting));
- if (!matcher.find()) {
- throw new ConfigError("Session " + sessionID + ": could not parse time '"
- + settings.getString(sessionID, timeSetting) + "'.");
- }
-
- return new TimeEndPoint(getDay(settings, sessionID, daySetting, NOT_SET),
- Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)),
- Integer.parseInt(matcher.group(3)), getTimeZone(matcher.group(4), defaultTimeZone));
- }
-
- private TimeZone getDefaultTimeZone(SessionSettings settings, SessionID sessionID)
- throws ConfigError, FieldConvertError {
- TimeZone sessionTimeZone;
- if (settings.isSetting(sessionID, Session.SETTING_TIMEZONE)) {
- String sessionTimeZoneID = settings.getString(sessionID, Session.SETTING_TIMEZONE);
- sessionTimeZone = TimeZone.getTimeZone(sessionTimeZoneID);
- if ("GMT".equals(sessionTimeZone.getID()) && !"GMT".equals(sessionTimeZoneID)) {
- throw new ConfigError("Unrecognized time zone '" + sessionTimeZoneID
- + "' for session " + sessionID);
- }
- } else {
- sessionTimeZone = TimeZone.getTimeZone("UTC");
- }
- return sessionTimeZone;
- }
-
- private TimeZone getTimeZone(String tz, TimeZone defaultZone) {
- return "".equals(tz) ? defaultZone : TimeZone.getTimeZone(tz.trim());
- }
-
- private class TimeEndPoint {
- private final int weekDay;
- private final int hour;
- private final int minute;
- private final int second;
- private final int timeInSeconds;
- private final TimeZone tz;
-
- public TimeEndPoint(int day, int hour, int minute, int second, TimeZone tz) {
- weekDay = day;
- this.hour = hour;
- this.minute = minute;
- this.second = second;
- this.tz = tz;
- timeInSeconds = timeInSeconds(hour, minute, second);
- }
-
- public int getHour() {
- return hour;
- }
-
- public int getMinute() {
- return minute;
- }
-
- public int getSecond() {
- return second;
- }
-
- public String toString() {
- try {
- Calendar calendar = Calendar.getInstance(tz);
- calendar.set(Calendar.HOUR_OF_DAY, hour);
- calendar.set(Calendar.MINUTE, minute);
- calendar.set(Calendar.SECOND, second);
- final SimpleDateFormat utc = new SimpleDateFormat("HH:mm:ss");
- utc.setTimeZone(TimeZone.getTimeZone("UTC"));
- return (isSet(weekDay) ? DayConverter.toString(weekDay) + "," : "")
- + utc.format(calendar.getTime()) + "-" + utc.getTimeZone().getID();
- } catch (ConfigError e) {
- return "ERROR: " + e;
- }
- }
-
- public int getDay() {
- return weekDay;
- }
-
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o instanceof TimeEndPoint) {
- TimeEndPoint otherTime = (TimeEndPoint) o;
- return timeInSeconds == otherTime.timeInSeconds;
- }
- return false;
- }
-
- public int hashCode() {
- assert false : "hashCode not supported";
- return 0;
- }
-
- public TimeZone getTimeZone() {
- return tz;
- }
- }
-
- private TimeInterval theMostRecentIntervalBefore(Calendar t) {
- TimeInterval timeInterval = new TimeInterval();
- Calendar intervalStart = timeInterval.getStart();
- intervalStart.setTimeZone(startTime.getTimeZone());
- intervalStart.setTimeInMillis(t.getTimeInMillis());
- intervalStart.set(Calendar.HOUR_OF_DAY, startTime.getHour());
- intervalStart.set(Calendar.MINUTE, startTime.getMinute());
- intervalStart.set(Calendar.SECOND, startTime.getSecond());
- intervalStart.set(Calendar.MILLISECOND, 0);
-
- Calendar intervalEnd = timeInterval.getEnd();
- intervalEnd.setTimeZone(endTime.getTimeZone());
- intervalEnd.setTimeInMillis(t.getTimeInMillis());
- intervalEnd.set(Calendar.HOUR_OF_DAY, endTime.getHour());
- intervalEnd.set(Calendar.MINUTE, endTime.getMinute());
- intervalEnd.set(Calendar.SECOND, endTime.getSecond());
- intervalEnd.set(Calendar.MILLISECOND, 0);
-
- if (isSet(startTime.getDay())) {
- intervalStart.set(Calendar.DAY_OF_WEEK, startTime.getDay());
- if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) {
- intervalStart.add(Calendar.WEEK_OF_YEAR, -1);
- intervalEnd.add(Calendar.WEEK_OF_YEAR, -1);
- }
- } else if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) {
- intervalStart.add(Calendar.DAY_OF_YEAR, -1);
- intervalEnd.add(Calendar.DAY_OF_YEAR, -1);
- }
-
- if (isSet(endTime.getDay())) {
- intervalEnd.set(Calendar.DAY_OF_WEEK, endTime.getDay());
- if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) {
- intervalEnd.add(Calendar.WEEK_OF_MONTH, 1);
- }
- } else if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) {
- intervalEnd.add(Calendar.DAY_OF_WEEK, 1);
- }
-
- return timeInterval;
- }
-
- private static class TimeInterval {
- private final Calendar start = SystemTime.getUtcCalendar();
- private final Calendar end = SystemTime.getUtcCalendar();
-
- public boolean isContainingTime(Calendar t) {
- return t.compareTo(start) >= 0 && t.compareTo(end) <= 0;
- }
-
- public String toString() {
- return start.getTime() + " --> " + end.getTime();
- }
-
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof TimeInterval)) {
- return false;
- }
- TimeInterval otherInterval = (TimeInterval) other;
- return start.equals(otherInterval.start) && end.equals(otherInterval.end);
- }
-
- public int hashCode() {
- assert false : "hashCode not supported";
- return 0;
- }
-
- public Calendar getStart() {
- return start;
- }
-
- public Calendar getEnd() {
- return end;
- }
- }
-
- public boolean isSameSession(Calendar time1, Calendar time2) {
- if (nonStopSession)
- return true;
- TimeInterval interval1 = theMostRecentIntervalBefore(time1);
- if (!interval1.isContainingTime(time1)) {
- return false;
- }
- TimeInterval interval2 = theMostRecentIntervalBefore(time2);
- return interval2.isContainingTime(time2) && interval1.equals(interval2);
- }
-
- public boolean isNonStopSession() {
- return nonStopSession;
- }
-
- private boolean isDailySession() {
- return !isSet(startTime.getDay()) && !isSet(endTime.getDay());
- }
-
- public boolean isSessionTime() {
- if(nonStopSession) {
- return true;
- }
- Calendar now = SystemTime.getUtcCalendar();
- TimeInterval interval = theMostRecentIntervalBefore(now);
- return interval.isContainingTime(now);
- }
-
- public String toString() {
- StringBuilder buf = new StringBuilder();
-
- SimpleDateFormat dowFormat = new SimpleDateFormat("EEEE");
- dowFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-
- SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss-z");
- timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-
- TimeInterval ti = theMostRecentIntervalBefore(SystemTime.getUtcCalendar());
-
- formatTimeInterval(buf, ti, timeFormat, false);
-
- // Now the localized equivalents, if necessary
- if (!startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)
- || !endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) {
- buf.append(" (");
- formatTimeInterval(buf, ti, timeFormat, true);
- buf.append(")");
- }
-
- return buf.toString();
- }
-
- private void formatTimeInterval(StringBuilder buf, TimeInterval timeInterval,
- SimpleDateFormat timeFormat, boolean local) {
- if (!isDailySession()) {
- buf.append("weekly, ");
- formatDayOfWeek(buf, startTime.getDay());
- buf.append(" ");
- } else {
- buf.append("daily, ");
- }
-
- if (local) {
- timeFormat.setTimeZone(startTime.getTimeZone());
- }
- buf.append(timeFormat.format(timeInterval.getStart().getTime()));
-
- buf.append(" - ");
-
- if (!isDailySession()) {
- formatDayOfWeek(buf, endTime.getDay());
- buf.append(" ");
- }
- if (local) {
- timeFormat.setTimeZone(endTime.getTimeZone());
- }
- buf.append(timeFormat.format(timeInterval.getEnd().getTime()));
- }
+public interface SessionSchedule {
- private void formatDayOfWeek(StringBuilder buf, int dayOfWeek) {
- try {
- String dayName = DayConverter.toString(dayOfWeek).toUpperCase();
- if (dayName.length() > 3) {
- dayName = dayName.substring(0, 3);
- }
- buf.append(dayName);
- } catch (ConfigError e) {
- buf.append("[Error: unknown day ").append(dayOfWeek).append("]");
- }
- }
+ /**
+ * Predicate for determining if the two times are in the same session
+ * @param time1 test time 1
+ * @param time2 test time 2
+ * @return return true if in the same session
+ */
+ boolean isSameSession(Calendar time1, Calendar time2);
- private int getDay(SessionSettings settings, SessionID sessionID, String key, int defaultValue)
- throws ConfigError, FieldConvertError {
- return settings.isSetting(sessionID, key) ?
- DayConverter.toInteger(settings.getString(sessionID, key))
- : NOT_SET;
- }
+ boolean isNonStopSession();
- private boolean isSet(int value) {
- return value != NOT_SET;
- }
+ /**
+ * Predicate for determining if the session should be active at the current time.
+ *
+ * @return true if session should be active, false otherwise.
+ */
+ boolean isSessionTime();
- private int timeInSeconds(int hour, int minute, int second) {
- return (hour * 3600) + (minute * 60) + second;
- }
}
diff --git a/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java b/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java
new file mode 100644
index 000000000..e4a83547a
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+/**
+ * Creates a SessionSchedule based on the specified settings.
+ */
+public interface SessionScheduleFactory {
+ SessionSchedule create(SessionID sessionID, SessionSettings settings) throws ConfigError;
+}
diff --git a/quickfixj-core/src/main/java/quickfix/SessionSettings.java b/quickfixj-core/src/main/java/quickfix/SessionSettings.java
index 0b68e3f66..6d9788e28 100644
--- a/quickfixj-core/src/main/java/quickfix/SessionSettings.java
+++ b/quickfixj-core/src/main/java/quickfix/SessionSettings.java
@@ -19,6 +19,10 @@
package quickfix;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import quickfix.field.converter.BooleanConverter;
+
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -42,11 +46,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import quickfix.field.converter.BooleanConverter;
-
/**
* Settings for sessions. Settings are grouped by FIX version and target company
* ID. There is also a default settings section that is inherited by the
@@ -263,12 +262,7 @@ public int getInt(SessionID sessionID, String key) throws ConfigError, FieldConv
}
private Properties getOrCreateSessionProperties(SessionID sessionID) {
- Properties p = sections.get(sessionID);
- if (p == null) {
- p = new Properties(sections.get(DEFAULT_SESSION_ID));
- sections.put(sessionID, p);
- }
- return p;
+ return sections.computeIfAbsent(sessionID, k -> new Properties(sections.get(DEFAULT_SESSION_ID)));
}
/**
@@ -373,10 +367,10 @@ public void setBool(SessionID sessionID, String key, boolean value) {
getOrCreateSessionProperties(sessionID).setProperty(key, BooleanConverter.convert(value));
}
- private final HashMap sections = new HashMap();
+ private final HashMap sections = new HashMap<>();
public Iterator sectionIterator() {
- final HashSet nondefaultSessions = new HashSet(sections.keySet());
+ final HashSet nondefaultSessions = new HashSet<>(sections.keySet());
nondefaultSessions.remove(DEFAULT_SESSION_ID);
return nondefaultSessions.iterator();
}
@@ -729,7 +723,7 @@ public static int[] parseSettingReconnectInterval(String raw) {
}
final String multiplierCharacter = raw.contains("*") ? "\\*" : "x";
final String[] data = raw.split(";");
- final List result = new ArrayList();
+ final List result = new ArrayList<>();
for (final String multi : data) {
final String[] timesSec = multi.split(multiplierCharacter);
int times;
@@ -767,7 +761,7 @@ public static Set parseRemoteAddresses(String raw) {
return null;
}
final String[] data = raw.split(",");
- final Set result = new HashSet();
+ final Set result = new HashSet<>();
for (final String multi : data) {
try {
result.add(InetAddress.getByName(multi));
diff --git a/quickfixj-core/src/main/java/quickfix/SessionState.java b/quickfixj-core/src/main/java/quickfix/SessionState.java
index c0f11fae7..ef666c852 100644
--- a/quickfixj-core/src/main/java/quickfix/SessionState.java
+++ b/quickfixj-core/src/main/java/quickfix/SessionState.java
@@ -72,7 +72,7 @@ public final class SessionState {
private final AtomicInteger nextExpectedMsgSeqNum = new AtomicInteger(0);
// The messageQueue should be accessed from a single thread
- private final Map messageQueue = new LinkedHashMap();
+ private final Map messageQueue = new LinkedHashMap<>();
public SessionState(Object lock, Log log, int heartBeatInterval, boolean initiator, MessageStore messageStore,
double testRequestDelayMultiplier) {
diff --git a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java b/quickfixj-core/src/main/java/quickfix/SleepycatStore.java
index 34966a4cf..e9e7aae1c 100644
--- a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java
+++ b/quickfixj-core/src/main/java/quickfix/SleepycatStore.java
@@ -221,16 +221,14 @@ public synchronized void get(int startSequence, int endSequence, Collection"
- + new String(messageBytes.getData(), charsetEncoding) + " for search key/data: "
- + sequenceKey + "=>" + messageBytes);
+ log.debug("Found record {}=>{} for search key/data: {}=>{}",
+ sequenceNumber, new String(messageBytes.getData(), charsetEncoding), sequenceKey, messageBytes);
}
cursor.getNext(sequenceKey, messageBytes, LockMode.DEFAULT);
sequenceNumber = (Integer) sequenceBinding.entryToObject(sequenceKey);
diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java
index bda6ae8ee..b64f807d5 100644
--- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java
+++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java
@@ -28,10 +28,37 @@
* sessions.
*/
public class SocketAcceptor extends AbstractSocketAcceptor {
- private Boolean isStarted = Boolean.FALSE;
- private final Object lock = new Object();
+ private volatile Boolean isStarted = Boolean.FALSE;
private final SingleThreadedEventHandlingStrategy eventHandlingStrategy;
+ private SocketAcceptor(Builder builder) throws ConfigError {
+ super(builder.application, builder.messageStoreFactory, builder.settings,
+ builder.logFactory, builder.messageFactory);
+
+ if (builder.queueCapacity >= 0) {
+ eventHandlingStrategy
+ = new SingleThreadedEventHandlingStrategy(this, builder.queueCapacity);
+ } else {
+ eventHandlingStrategy
+ = new SingleThreadedEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark);
+ }
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends AbstractSessionConnectorBuilder {
+ private Builder() {
+ super(Builder.class);
+ }
+
+ @Override
+ protected SocketAcceptor doBuild() throws ConfigError {
+ return new SocketAcceptor(this);
+ }
+ }
+
public SocketAcceptor(Application application, MessageStoreFactory messageStoreFactory,
SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory,
int queueCapacity)
@@ -70,30 +97,23 @@ public SocketAcceptor(SessionFactory sessionFactory, SessionSettings settings) t
eventHandlingStrategy = new SingleThreadedEventHandlingStrategy(this, DEFAULT_QUEUE_CAPACITY);
}
- @Override
- public void block() throws ConfigError, RuntimeError {
- initialize(false);
- }
-
@Override
public void start() throws ConfigError, RuntimeError {
initialize(true);
}
private void initialize(boolean blockInThread) throws ConfigError {
- synchronized (lock) {
- if (isStarted.equals(Boolean.FALSE)) {
- startAcceptingConnections();
- if (blockInThread) {
- eventHandlingStrategy.blockInThread();
- isStarted = Boolean.TRUE;
- } else {
- isStarted = Boolean.TRUE;
- eventHandlingStrategy.block();
- }
+ if (isStarted.equals(Boolean.FALSE)) {
+ eventHandlingStrategy.setExecutor(longLivedExecutor);
+ startAcceptingConnections();
+ isStarted = Boolean.TRUE;
+ if (blockInThread) {
+ eventHandlingStrategy.blockInThread();
} else {
- log.warn("Ignored attempt to start already running SocketAcceptor.");
+ eventHandlingStrategy.block();
}
+ } else {
+ log.warn("Ignored attempt to start already running SocketAcceptor.");
}
}
@@ -104,18 +124,19 @@ public void stop() {
@Override
public void stop(boolean forceDisconnect) {
- eventHandlingStrategy.stopHandlingMessages();
- synchronized (lock) {
+ if (isStarted.equals(Boolean.TRUE)) {
try {
try {
+ logoutAllSessions(forceDisconnect);
stopAcceptingConnections();
} catch (ConfigError e) {
log.error("Error when stopping acceptor.", e);
}
- logoutAllSessions(forceDisconnect);
stopSessionTimer();
} finally {
- Session.unregisterSessions(getSessions());
+ eventHandlingStrategy.stopHandlingMessages();
+ Session.unregisterSessions(getSessions(), true);
+ clearConnectorSessions();
isStarted = Boolean.FALSE;
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java
index df10417ba..efad4f404 100644
--- a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java
+++ b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java
@@ -28,10 +28,37 @@
* sessions.
*/
public class SocketInitiator extends AbstractSocketInitiator {
- private Boolean isStarted = Boolean.FALSE;
- private final Object lock = new Object();
+ private volatile Boolean isStarted = Boolean.FALSE;
private final SingleThreadedEventHandlingStrategy eventHandlingStrategy;
+ private SocketInitiator(Builder builder) throws ConfigError {
+ super(builder.application, builder.messageStoreFactory, builder.settings,
+ builder.logFactory, builder.messageFactory);
+
+ if (builder.queueCapacity >= 0) {
+ eventHandlingStrategy
+ = new SingleThreadedEventHandlingStrategy(this, builder.queueCapacity);
+ } else {
+ eventHandlingStrategy
+ = new SingleThreadedEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark);
+ }
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends AbstractSessionConnectorBuilder {
+ private Builder() {
+ super(Builder.class);
+ }
+
+ @Override
+ protected SocketInitiator doBuild() throws ConfigError {
+ return new SocketInitiator(this);
+ }
+ }
+
public SocketInitiator(Application application, MessageStoreFactory messageStoreFactory,
SessionSettings settings, MessageFactory messageFactory, int queueCapacity) throws ConfigError {
super(application, messageStoreFactory, settings, new ScreenLogFactory(settings),
@@ -79,11 +106,6 @@ public SocketInitiator(SessionFactory sessionFactory, SessionSettings settings,
eventHandlingStrategy = new SingleThreadedEventHandlingStrategy(this, queueCapacity);
}
- @Override
- public void block() throws ConfigError, RuntimeError {
- initialize(false);
- }
-
@Override
public void start() throws ConfigError, RuntimeError {
initialize(true);
@@ -96,36 +118,35 @@ public void stop() {
@Override
public void stop(boolean forceDisconnect) {
- eventHandlingStrategy.stopHandlingMessages();
- synchronized (lock) {
+ if (isStarted.equals(Boolean.TRUE)) {
try {
logoutAllSessions(forceDisconnect);
stopInitiators();
} finally {
- Session.unregisterSessions(getSessions());
+ eventHandlingStrategy.stopHandlingMessages();
+ Session.unregisterSessions(getSessions(), true);
+ clearConnectorSessions();
isStarted = Boolean.FALSE;
}
}
}
private void initialize(boolean blockInThread) throws ConfigError {
- synchronized (lock) {
- if (isStarted.equals(Boolean.FALSE)) {
- createSessionInitiators();
- for (Session session : getSessionMap().values()) {
- Session.registerSession(session);
- }
- startInitiators();
- if (blockInThread) {
- eventHandlingStrategy.blockInThread();
- isStarted = Boolean.TRUE;
- } else {
- isStarted = Boolean.TRUE;
- eventHandlingStrategy.block();
- }
+ if (isStarted.equals(Boolean.FALSE)) {
+ eventHandlingStrategy.setExecutor(longLivedExecutor);
+ createSessionInitiators();
+ for (Session session : getSessionMap().values()) {
+ Session.registerSession(session);
+ }
+ startInitiators();
+ isStarted = Boolean.TRUE;
+ if (blockInThread) {
+ eventHandlingStrategy.blockInThread();
} else {
- log.warn("Ignored attempt to start already running SocketInitiator.");
+ eventHandlingStrategy.block();
}
+ } else {
+ log.warn("Ignored attempt to start already running SocketInitiator.");
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/SystemTime.java b/quickfixj-core/src/main/java/quickfix/SystemTime.java
index 6687f569c..85d73c47b 100644
--- a/quickfixj-core/src/main/java/quickfix/SystemTime.java
+++ b/quickfixj-core/src/main/java/quickfix/SystemTime.java
@@ -19,6 +19,8 @@
package quickfix;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
@@ -30,9 +32,15 @@ public class SystemTime {
public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
private static final SystemTimeSource DEFAULT_TIME_SOURCE = new SystemTimeSource() {
+ @Override
public long getTime() {
return System.currentTimeMillis();
}
+
+ @Override
+ public LocalDateTime getNow() {
+ return LocalDateTime.now(ZoneOffset.UTC);
+ }
};
private static volatile SystemTimeSource systemTimeSource = DEFAULT_TIME_SOURCE;
@@ -40,11 +48,19 @@ public long getTime() {
public static long currentTimeMillis() {
return systemTimeSource.getTime();
}
+
+ public static LocalDateTime now() {
+ return systemTimeSource.getNow();
+ }
public static Date getDate() {
return new Date(currentTimeMillis());
}
+ public static LocalDateTime getLocalDateTime() {
+ return now();
+ }
+
public static void setTimeSource(SystemTimeSource systemTimeSource) {
SystemTime.systemTimeSource = systemTimeSource != null ? systemTimeSource
: DEFAULT_TIME_SOURCE;
diff --git a/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java b/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java
index f2e2b104f..c14af90f8 100644
--- a/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java
+++ b/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java
@@ -19,6 +19,8 @@
package quickfix;
+import java.time.LocalDateTime;
+
/**
* Interface for obtaining system time. A system time source should be used
* instead of direct system time to facilitate unit testing.
@@ -31,4 +33,11 @@ public interface SystemTimeSource {
* @return current (possible simulated) time
*/
long getTime();
+
+ /**
+ * Obtain current LocalDateTime.
+ *
+ * @return current (possible simulated) time up to nanosecond precision.
+ */
+ LocalDateTime getNow();
}
diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java
index f5d65ada8..8f35bb801 100644
--- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java
+++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java
@@ -29,6 +29,34 @@
public class ThreadedSocketAcceptor extends AbstractSocketAcceptor {
private final ThreadPerSessionEventHandlingStrategy eventHandlingStrategy;
+ private ThreadedSocketAcceptor(Builder builder) throws ConfigError {
+ super(builder.application, builder.messageStoreFactory, builder.settings,
+ builder.logFactory, builder.messageFactory);
+
+ if (builder.queueCapacity >= 0) {
+ eventHandlingStrategy
+ = new ThreadPerSessionEventHandlingStrategy(this, builder.queueCapacity);
+ } else {
+ eventHandlingStrategy
+ = new ThreadPerSessionEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark);
+ }
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends AbstractSessionConnectorBuilder {
+ private Builder() {
+ super(Builder.class);
+ }
+
+ @Override
+ protected ThreadedSocketAcceptor doBuild() throws ConfigError {
+ return new ThreadedSocketAcceptor(this);
+ }
+ }
+
public ThreadedSocketAcceptor(Application application, MessageStoreFactory messageStoreFactory,
SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory,
int queueCapacity )
@@ -70,6 +98,7 @@ public ThreadedSocketAcceptor(SessionFactory sessionFactory, SessionSettings set
}
public void start() throws ConfigError, RuntimeError {
+ eventHandlingStrategy.setExecutor(longLivedExecutor);
startAcceptingConnections();
}
@@ -79,14 +108,15 @@ public void stop() {
public void stop(boolean forceDisconnect) {
try {
+ logoutAllSessions(forceDisconnect);
stopAcceptingConnections();
} catch (ConfigError e) {
log.error("Error when stopping acceptor.", e);
}
- logoutAllSessions(forceDisconnect);
stopSessionTimer();
eventHandlingStrategy.stopDispatcherThreads();
- Session.unregisterSessions(getSessions());
+ Session.unregisterSessions(getSessions(), true);
+ clearConnectorSessions();
}
public void block() throws ConfigError, RuntimeError {
diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java
index 1bdb4ed8a..bf42ce20a 100644
--- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java
+++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java
@@ -29,6 +29,34 @@
public class ThreadedSocketInitiator extends AbstractSocketInitiator {
private final ThreadPerSessionEventHandlingStrategy eventHandlingStrategy;
+ private ThreadedSocketInitiator(Builder builder) throws ConfigError {
+ super(builder.application, builder.messageStoreFactory, builder.settings,
+ builder.logFactory, builder.messageFactory);
+
+ if (builder.queueCapacity >= 0) {
+ eventHandlingStrategy
+ = new ThreadPerSessionEventHandlingStrategy(this, builder.queueCapacity);
+ } else {
+ eventHandlingStrategy
+ = new ThreadPerSessionEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark);
+ }
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder extends AbstractSessionConnectorBuilder {
+ private Builder() {
+ super(Builder.class);
+ }
+
+ @Override
+ protected ThreadedSocketInitiator doBuild() throws ConfigError {
+ return new ThreadedSocketInitiator(this);
+ }
+ }
+
public ThreadedSocketInitiator(Application application,
MessageStoreFactory messageStoreFactory, SessionSettings settings,
LogFactory logFactory, MessageFactory messageFactory, int queueCapacity) throws ConfigError {
@@ -72,6 +100,7 @@ public ThreadedSocketInitiator(SessionFactory sessionFactory, SessionSettings se
}
public void start() throws ConfigError, RuntimeError {
+ eventHandlingStrategy.setExecutor(longLivedExecutor);
createSessionInitiators();
startInitiators();
}
@@ -81,13 +110,11 @@ public void stop() {
}
public void stop(boolean forceDisconnect) {
- stopInitiators();
logoutAllSessions(forceDisconnect);
- if (!forceDisconnect) {
- waitForLogout();
- }
+ stopInitiators();
eventHandlingStrategy.stopDispatcherThreads();
- Session.unregisterSessions(getSessions());
+ Session.unregisterSessions(getSessions(), true);
+ clearConnectorSessions();
}
public void block() throws ConfigError, RuntimeError {
diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateField.java b/quickfixj-core/src/main/java/quickfix/UtcDateField.java
new file mode 100644
index 000000000..353e42463
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/UtcDateField.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+import java.time.LocalDate;
+
+/**
+ * A LocalDate-valued message field with up to nanosecond precision.
+ */
+public class UtcDateField extends Field {
+
+ protected UtcDateField(int field) {
+ super(field, LocalDate.now());
+ }
+
+ protected UtcDateField(int field, LocalDate data) {
+ super(field, data);
+ }
+
+ public void setValue(LocalDate value) {
+ setObject(value);
+ }
+
+ public LocalDate getValue() {
+ return getObject();
+ }
+
+ public boolean valueEquals(LocalDate value) {
+ return getValue().equals(value);
+ }
+
+}
diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java b/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java
index 427b02c26..9926a51fb 100644
--- a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java
+++ b/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java
@@ -19,17 +19,31 @@
package quickfix;
-import java.util.Date;
+import java.time.LocalDate;
/**
* A date-valued message field.
*/
-public class UtcDateOnlyField extends DateField {
+public class UtcDateOnlyField extends Field {
+
public UtcDateOnlyField(int field) {
- super(field);
+ super(field, LocalDate.now());
}
- protected UtcDateOnlyField(int field, Date data) {
+ protected UtcDateOnlyField(int field, LocalDate data) {
super(field, data);
}
+
+ public void setValue(LocalDate value) {
+ setObject(value);
+ }
+
+ public LocalDate getValue() {
+ return getObject();
+ }
+
+ public boolean valueEquals(LocalDate value) {
+ return getValue().equals(value);
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeField.java
new file mode 100644
index 000000000..e913e3e41
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/UtcTimeField.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+import java.time.LocalTime;
+
+/**
+ * A LocalTime-valued message field with up to nanosecond precision.
+ */
+public class UtcTimeField extends Field {
+
+ protected UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS;
+
+ protected UtcTimeField(int field) {
+ super(field, LocalTime.now());
+ }
+
+ protected UtcTimeField(int field, LocalTime data) {
+ super(field, data);
+ }
+
+ protected UtcTimeField(int field, LocalTime data, UtcTimestampPrecision precision) {
+ super(field, data);
+ this.precision = precision;
+ }
+
+ public void setValue(LocalTime value) {
+ setObject(value);
+ }
+
+ public LocalTime getValue() {
+ return getObject();
+ }
+
+ public boolean valueEquals(LocalTime value) {
+ return getValue().equals(value);
+ }
+
+ public UtcTimestampPrecision getPrecision() {
+ return precision;
+ }
+}
diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java
index 73f73c670..f7ea1d73c 100644
--- a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java
+++ b/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java
@@ -19,34 +19,52 @@
package quickfix;
-import java.util.Date;
+import java.time.LocalTime;
/*
* A time-valued message field.
*/
-public class UtcTimeOnlyField extends DateField {
- private boolean includeMilliseconds = true;
+public class UtcTimeOnlyField extends Field {
+
+ private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS;
public UtcTimeOnlyField(int field) {
- super(field);
+ super(field, LocalTime.now());
}
- protected UtcTimeOnlyField(int field, Date data) {
+ protected UtcTimeOnlyField(int field, LocalTime data) {
super(field, data);
}
+ protected UtcTimeOnlyField(int field, LocalTime data, UtcTimestampPrecision precision) {
+ super(field, data);
+ this.precision = precision;
+ }
+
public UtcTimeOnlyField(int field, boolean includeMilliseconds) {
- super(field);
- this.includeMilliseconds = includeMilliseconds;
+ super(field, LocalTime.now());
+ this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS;
}
- protected UtcTimeOnlyField(int field, Date data, boolean includeMilliseconds) {
+ protected UtcTimeOnlyField(int field, LocalTime data, boolean includeMilliseconds) {
super(field, data);
- this.includeMilliseconds = includeMilliseconds;
+ this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS;
+ }
+
+ public UtcTimestampPrecision getPrecision() {
+ return precision;
+ }
+
+ public void setValue(LocalTime value) {
+ setObject(value);
+ }
+
+ public LocalTime getValue() {
+ return getObject();
}
- boolean showMilliseconds() {
- return includeMilliseconds;
+ public boolean valueEquals(LocalTime value) {
+ return getValue().equals(value);
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java
index fd09564e6..92c391630 100644
--- a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java
+++ b/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java
@@ -19,33 +19,57 @@
package quickfix;
-import java.util.Date;
+import java.time.LocalDateTime;
/**
* A timestamp-valued message field (a timestamp has both a date and a time).
*/
-public class UtcTimeStampField extends DateField {
- private boolean includeMilliseconds = true;
+public class UtcTimeStampField extends Field {
+
+ private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS;
public UtcTimeStampField(int field) {
- super(field);
+ super(field, LocalDateTime.now());
+ }
+
+ protected UtcTimeStampField(int field, LocalDateTime data) {
+ super(field, data);
}
- protected UtcTimeStampField(int field, Date data) {
+ protected UtcTimeStampField(int field, LocalDateTime data, UtcTimestampPrecision precision) {
super(field, data);
+ this.precision = precision;
}
public UtcTimeStampField(int field, boolean includeMilliseconds) {
- super(field);
- this.includeMilliseconds = includeMilliseconds;
+ super(field, LocalDateTime.now());
+ this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS;
+ }
+
+ public UtcTimeStampField(int field, UtcTimestampPrecision precision) {
+ super(field, LocalDateTime.now());
+ this.precision = precision;
}
- protected UtcTimeStampField(int field, Date data, boolean includeMilliseconds) {
+ protected UtcTimeStampField(int field, LocalDateTime data, boolean includeMilliseconds) {
super(field, data);
- this.includeMilliseconds = includeMilliseconds;
+ this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS;
+ }
+
+ public UtcTimestampPrecision getPrecision() {
+ return precision;
+ }
+
+ public void setValue(LocalDateTime value) {
+ setObject(value);
}
- boolean showMilliseconds() {
- return includeMilliseconds;
+ public LocalDateTime getValue() {
+ return getObject();
}
+
+ public boolean valueEquals(LocalDateTime value) {
+ return getValue().equals(value);
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java b/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java
new file mode 100644
index 000000000..fba92a6ad
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * Copyright (c) quickfixengine.org All rights reserved.
+ *
+ * This file is part of the QuickFIX FIX Engine
+ *
+ * This file may be distributed under the terms of the quickfixengine.org
+ * license as defined by quickfixengine.org and appearing in the file
+ * LICENSE included in the packaging of this file.
+ *
+ * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
+ * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ *
+ * See http://www.quickfixengine.org/LICENSE for licensing information.
+ *
+ * Contact ask@quickfixengine.org if any conditions of this licensing
+ * are not clear to you.
+ ******************************************************************************/
+
+package quickfix;
+
+/**
+ *
+ * @author chrjohn
+ */
+public enum UtcTimestampPrecision {
+ SECONDS, MILLIS, MICROS, NANOS
+}
diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java
index cc1d2bdcf..c19e85a4d 100644
--- a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java
+++ b/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java
@@ -22,6 +22,7 @@
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.TimeZone;
@@ -34,6 +35,15 @@ protected static void assertLength(String value, int i, String type) throws Fiel
}
}
+ protected static void assertLength(String value, String type, int... lengths) throws FieldConvertError {
+ for (int length : lengths) {
+ if (value.length() == length) {
+ return;
+ }
+ }
+ throwFieldConvertError(value, type);
+ }
+
protected static void assertDigitSequence(String value, int i, int j, String type)
throws FieldConvertError {
for (int offset = i; offset < j; offset++) {
@@ -69,5 +79,8 @@ protected DateFormat createDateFormat(String format) {
sdf.setDateFormatSymbols(new DateFormatSymbols(Locale.US));
return sdf;
}
-
+
+ protected static DateTimeFormatter createDateTimeFormat(String format) {
+ return DateTimeFormatter.ofPattern(format);
+ }
}
diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java
index f3bc31a1d..ee8c05a88 100644
--- a/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java
+++ b/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java
@@ -19,21 +19,18 @@
package quickfix.field.converter;
+import quickfix.FieldConvertError;
+import quickfix.RuntimeError;
+
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import quickfix.FieldConvertError;
-import quickfix.RuntimeError;
/**
* Converts between a double and a String.
*/
public class DoubleConverter {
- private static final Pattern DECIMAL_PATTERN = Pattern.compile("-?\\d*(\\.\\d*)?");
- private static final ThreadLocal THREAD_DECIMAL_FORMATS = new ThreadLocal();
+ private static final ThreadLocal THREAD_DECIMAL_FORMATS = new ThreadLocal<>();
/**
* Converts a double to a string with no padding.
@@ -92,13 +89,25 @@ public static String convert(double d, int padding) {
*/
public static double convert(String value) throws FieldConvertError {
try {
- Matcher matcher = DECIMAL_PATTERN.matcher(value);
- if (!matcher.matches()) {
- throw new NumberFormatException();
- }
- return Double.parseDouble(value);
+ return parseDouble(value);
} catch (NumberFormatException e) {
throw new FieldConvertError("invalid double value: " + value);
}
}
+
+ private static double parseDouble(String value) {
+ if(value.length() == 0) throw new NumberFormatException(value);
+ boolean dot = false; int i = 0;
+ char c = value.charAt(i);
+ switch (c) {
+ case '-': i++; break;
+ case '+': throw new NumberFormatException(value);
+ }
+ for (; i < value.length(); i++) {
+ c = value.charAt(i);
+ if (!dot && c == '.') dot = true;
+ else if (c < '0' || c > '9') throw new NumberFormatException(value);
+ }
+ return Double.parseDouble(value);
+ }
}
diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java
index 593bad044..cdaa67786 100644
--- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java
+++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java
@@ -19,20 +19,28 @@
package quickfix.field.converter;
+import quickfix.FieldConvertError;
+
import java.text.DateFormat;
import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
import java.util.Date;
-import quickfix.FieldConvertError;
-
/**
* Convert between a date and a String
*/
public class UtcDateOnlyConverter extends AbstractDateTimeConverter {
+
+ static final String TYPE = "date";
+ static final int DATE_LENGTH = 8;
// SimpleDateFormats are not thread safe. A thread local is being
// used to maintain high concurrency among multiple session threads
- private static final ThreadLocal utcDateConverter = new ThreadLocal();
+ private static final ThreadLocal UTC_DATE_CONVERTER = new ThreadLocal<>();
private final DateFormat dateFormat = createDateFormat("yyyyMMdd");
+ private static final DateTimeFormatter FORMATTER_DATE = createDateTimeFormat("yyyyMMdd");
/**
* Convert a date to a String ("YYYYMMDD")
@@ -44,11 +52,15 @@ public static String convert(Date d) {
return getFormatter().format(d);
}
+ public static String convert(LocalDate d) {
+ return d.format(FORMATTER_DATE);
+ }
+
private static DateFormat getFormatter() {
- UtcDateOnlyConverter converter = utcDateConverter.get();
+ UtcDateOnlyConverter converter = UTC_DATE_CONVERTER.get();
if (converter == null) {
converter = new UtcDateOnlyConverter();
- utcDateConverter.set(converter);
+ UTC_DATE_CONVERTER.set(converter);
}
return converter.dateFormat;
}
@@ -62,15 +74,39 @@ private static DateFormat getFormatter() {
*/
public static Date convert(String value) throws FieldConvertError {
Date d = null;
- String type = "date";
- assertLength(value, 8, type);
- assertDigitSequence(value, 0, 8, type);
+ checkString(value);
try {
d = getFormatter().parse(value);
} catch (ParseException e) {
- throwFieldConvertError(value, type);
+ throwFieldConvertError(value, TYPE);
}
return d;
}
+ public static LocalDate convertToLocalDate(String value) throws FieldConvertError {
+ checkString(value);
+ try {
+ return LocalDate.parse(value.substring(0, DATE_LENGTH), FORMATTER_DATE);
+ } catch (DateTimeParseException e) {
+ throwFieldConvertError(value, TYPE);
+ }
+ return null;
+ }
+
+ private static void checkString(String value) throws FieldConvertError {
+ assertLength(value, DATE_LENGTH, TYPE);
+ assertDigitSequence(value, 0, DATE_LENGTH, TYPE);
+ }
+
+ /**
+ * @param localDate
+ * @return a java.util.Date with date part filled from LocalDate.
+ */
+ public static Date getDate(LocalDate localDate) {
+ if (localDate != null) {
+ return Date.from(localDate.atStartOfDay().atZone(ZoneOffset.UTC).toInstant());
+ }
+ return null;
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java
index 797bb36e2..fbc2d609f 100644
--- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java
+++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java
@@ -19,21 +19,39 @@
package quickfix.field.converter;
+import quickfix.UtcTimestampPrecision;
+import quickfix.FieldConvertError;
+
import java.text.DateFormat;
import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
import java.util.Date;
-import quickfix.FieldConvertError;
-
/**
* Convert between a time and a String.
*/
public class UtcTimeOnlyConverter extends AbstractDateTimeConverter {
+
+ static final String TYPE = "time";
+ static final int LENGTH_INCL_SECONDS = 8;
+ static final int LENGTH_INCL_MILLIS = 12;
+ static final int LENGTH_INCL_MICROS = 15;
+ static final int LENGTH_INCL_NANOS = 18;
+ static final int LENGTH_INCL_PICOS = 21;
+
// SimpleDateFormats are not thread safe. A thread local is being
// used to maintain high concurrency among multiple session threads
- private static final ThreadLocal utcTimeConverter = new ThreadLocal();
+ private static final ThreadLocal UTC_TIME_CONVERTER = new ThreadLocal<>();
private final DateFormat utcTimeFormat = createDateFormat("HH:mm:ss");
private final DateFormat utcTimeFormatMillis = createDateFormat("HH:mm:ss.SSS");
+ private static final DateTimeFormatter FORMATTER_SECONDS = createDateTimeFormat("HH:mm:ss");
+ private static final DateTimeFormatter FORMATTER_MILLIS = createDateTimeFormat("HH:mm:ss.SSS");
+ private static final DateTimeFormatter FORMATTER_MICROS = createDateTimeFormat("HH:mm:ss.SSSSSS");
+ private static final DateTimeFormatter FORMATTER_NANOS = createDateTimeFormat("HH:mm:ss.SSSSSSSSS");
/**
* Convert a time (represented as a Date) to a String (HH:MM:SS or HH:MM:SS.SSS)
@@ -46,11 +64,34 @@ public static String convert(Date d, boolean includeMilliseconds) {
return getFormatter(includeMilliseconds).format(d);
}
+ /**
+ * Convert a time (represented as LocalTime) to a String
+ *
+ * @param d the LocalTime with the time to convert
+ * @param precision controls whether seconds, milliseconds, microseconds or
+ * nanoseconds are included in the result
+ * @return a String representing the time.
+ */
+ public static String convert(LocalTime d, UtcTimestampPrecision precision) {
+ switch (precision) {
+ case SECONDS:
+ return d.format(FORMATTER_SECONDS);
+ case MILLIS:
+ return d.format(FORMATTER_MILLIS);
+ case MICROS:
+ return d.format(FORMATTER_MICROS);
+ case NANOS:
+ return d.format(FORMATTER_NANOS);
+ default:
+ return d.format(FORMATTER_MILLIS);
+ }
+ }
+
private static DateFormat getFormatter(boolean includeMillis) {
- UtcTimeOnlyConverter converter = utcTimeConverter.get();
+ UtcTimeOnlyConverter converter = UTC_TIME_CONVERTER.get();
if (converter == null) {
converter = new UtcTimeOnlyConverter();
- utcTimeConverter.set(converter);
+ UTC_TIME_CONVERTER.set(converter);
}
return includeMillis ? converter.utcTimeFormatMillis : converter.utcTimeFormat;
}
@@ -64,12 +105,49 @@ private static DateFormat getFormatter(boolean includeMillis) {
*/
public static Date convert(String value) throws FieldConvertError {
Date d = null;
+ assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS);
try {
- d = getFormatter(value.length() == 12).parse(value);
+ final boolean includeMillis = (value.length() >= LENGTH_INCL_MILLIS);
+ d = getFormatter(includeMillis).parse(includeMillis ? value.substring(0, LENGTH_INCL_MILLIS) : value);
} catch (ParseException e) {
- throwFieldConvertError(value, "time");
+ throwFieldConvertError(value, TYPE);
}
return d;
}
+ public static LocalTime convertToLocalTime(String value) throws FieldConvertError {
+ assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS);
+ try {
+ int length = value.length();
+ switch (length) {
+ case LENGTH_INCL_SECONDS:
+ return LocalTime.parse(value, FORMATTER_SECONDS);
+ case LENGTH_INCL_MILLIS:
+ return LocalTime.parse(value, FORMATTER_MILLIS);
+ case LENGTH_INCL_MICROS:
+ return LocalTime.parse(value, FORMATTER_MICROS);
+ case LENGTH_INCL_NANOS:
+ return LocalTime.parse(value, FORMATTER_NANOS);
+ case LENGTH_INCL_PICOS:
+ return LocalTime.parse(value.substring(0, LENGTH_INCL_NANOS), FORMATTER_NANOS);
+ default:
+ throwFieldConvertError(value, TYPE);
+ }
+ } catch (DateTimeParseException e) {
+ throwFieldConvertError(value, TYPE);
+ }
+ return null;
+ }
+
+ /**
+ * @param localTime
+ * @return a java.util.Date with time part filled from LocalTime (truncated to milliseconds).
+ */
+ public static Date getDate(LocalTime localTime) {
+ if (localTime != null) {
+ return Date.from(localTime.atDate(LocalDate.ofEpochDay(0)).atZone(ZoneOffset.UTC).toInstant());
+ }
+ return null;
+ }
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java
index 8b2cfcc91..d792ad2e0 100644
--- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java
+++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java
@@ -1,4 +1,4 @@
-/*******************************************************************************
+/** *****************************************************************************
* Copyright (c) quickfixengine.org All rights reserved.
*
* This file is part of the QuickFIX FIX Engine
@@ -15,57 +15,101 @@
*
* Contact ask@quickfixengine.org if any conditions of this licensing
* are not clear to you.
- ******************************************************************************/
-
+ ***************************************************************************** */
package quickfix.field.converter;
+import quickfix.UtcTimestampPrecision;
+import org.quickfixj.SimpleCache;
+import quickfix.FieldConvertError;
+import quickfix.SystemTime;
+
import java.text.DateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
-import java.util.concurrent.*;
-
-import quickfix.FieldConvertError;
-import quickfix.SystemTime;
/**
* Convert between a timestamp and a String. A timestamp includes both a date
* and a time.
*/
public class UtcTimestampConverter extends AbstractDateTimeConverter {
- private static final ThreadLocal utcTimestampConverter = new ThreadLocal();
+
+ static final String TYPE = "timestamp";
+ static final int LENGTH_INCL_SECONDS = 17;
+ static final int LENGTH_INCL_MILLIS = 21;
+ static final int LENGTH_INCL_MICROS = 24;
+ static final int LENGTH_INCL_NANOS = 27;
+ static final int LENGTH_INCL_PICOS = 30;
+
+ private static final ThreadLocal UTC_TIMESTAMP_CONVERTER = new ThreadLocal<>();
+ private static final SimpleCache DATE_CACHE = new SimpleCache<>(dateString -> {
+ final Calendar c = new GregorianCalendar(1970, 0, 1, 0, 0, 0);
+ c.setTimeZone(SystemTime.UTC_TIMEZONE);
+ final int year = Integer.parseInt(dateString.substring(0, 4));
+ final int month = Integer.parseInt(dateString.substring(4, 6));
+ final int day = Integer.parseInt(dateString.substring(6, 8));
+ c.set(year, month - 1, day);
+ return c.getTimeInMillis();
+ });
+
private final DateFormat utcTimestampFormat = createDateFormat("yyyyMMdd-HH:mm:ss");
private final DateFormat utcTimestampFormatMillis = createDateFormat("yyyyMMdd-HH:mm:ss.SSS");
- private final static ConcurrentHashMap dateCache = new ConcurrentHashMap();
+ private static final DateTimeFormatter FORMATTER_SECONDS = createDateTimeFormat("yyyyMMdd-HH:mm:ss");
+ private static final DateTimeFormatter FORMATTER_MILLIS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSS");
+ private static final DateTimeFormatter FORMATTER_MICROS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSSSSS");
+ private static final DateTimeFormatter FORMATTER_NANOS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSSSSSSSS");
/**
* Convert a timestamp (represented as a Date) to a String.
*
* @param d the date to convert
- * @param includeMilliseconds controls whether milliseconds are included in the result
+ * @param includeMilliseconds controls whether milliseconds are included in
+ * the result
* @return the formatted timestamp
*/
public static String convert(Date d, boolean includeMilliseconds) {
return getFormatter(includeMilliseconds).format(d);
}
+ /**
+ * Convert a timestamp (represented as a LocalDateTime) to a String.
+ *
+ * @param d the date to convert
+ * @param precision controls whether seconds, milliseconds, microseconds or
+ * nanoseconds are included in the result
+ * @return the formatted timestamp
+ */
+ public static String convert(LocalDateTime d, UtcTimestampPrecision precision) {
+ switch (precision) {
+ case SECONDS:
+ return d.format(FORMATTER_SECONDS);
+ case MILLIS:
+ return d.format(FORMATTER_MILLIS);
+ case MICROS:
+ return d.format(FORMATTER_MICROS);
+ case NANOS:
+ return d.format(FORMATTER_NANOS);
+ default:
+ return d.format(FORMATTER_MILLIS);
+ }
+ }
+
private static DateFormat getFormatter(boolean includeMillis) {
- UtcTimestampConverter converter = utcTimestampConverter.get();
+ UtcTimestampConverter converter = UTC_TIMESTAMP_CONVERTER.get();
if (converter == null) {
converter = new UtcTimestampConverter();
- utcTimestampConverter.set(converter);
+ UTC_TIMESTAMP_CONVERTER.set(converter);
}
return includeMillis ? converter.utcTimestampFormatMillis : converter.utcTimestampFormat;
}
- //
- // Performance optimization: the calendar for the start of the day is cached.
- // The time is converted to millis and then added to the millis specified by
- // the base calendar.
- //
-
/**
* Convert a timestamp string into a Date.
+ * Date has up to millisecond precision.
*
* @param value the timestamp String
* @return the parsed timestamp
@@ -73,48 +117,92 @@ private static DateFormat getFormatter(boolean includeMillis) {
*/
public static Date convert(String value) throws FieldConvertError {
verifyFormat(value);
- long timeOffset = (parseLong(value.substring(9, 11)) * 3600000L)
- + (parseLong(value.substring(12, 14)) * 60000L)
- + (parseLong(value.substring(15, 17)) * 1000L);
- if (value.length() == 21) {
- timeOffset += parseLong(value.substring(18, 21));
+ long timeOffset = getTimeOffsetSeconds(value);
+ if (value.length() >= LENGTH_INCL_MILLIS) { // format has already been verified
+ // accept up to picosenconds but parse only up to milliseconds
+ timeOffset += parseLong(value.substring(18, LENGTH_INCL_MILLIS));
}
return new Date(getMillisForDay(value) + timeOffset);
}
- private static Long getMillisForDay(String value) {
- String dateString = value.substring(0, 8);
- Long millis = dateCache.get(dateString);
- if (millis == null) {
- Calendar c = new GregorianCalendar(1970, 0, 1, 0, 0, 0);
- c.setTimeZone(SystemTime.UTC_TIMEZONE);
- int year = Integer.parseInt(value.substring(0, 4));
- int month = Integer.parseInt(value.substring(4, 6));
- int day = Integer.parseInt(value.substring(6, 8));
- c.set(year, month - 1, day);
- millis = c.getTimeInMillis();
- dateCache.put(dateString, c.getTimeInMillis());
+ /**
+ * Convert a timestamp string into a LocalDateTime object.
+ * LocalDateTime has up to nanosecond precision.
+ *
+ * @param value the timestamp String
+ * @return the parsed timestamp
+ * @exception FieldConvertError raised if timestamp is an incorrect format.
+ */
+ public static LocalDateTime convertToLocalDateTime(String value) throws FieldConvertError {
+ verifyFormat(value);
+ int length = value.length();
+ try {
+ switch (length) {
+ case LENGTH_INCL_SECONDS:
+ return LocalDateTime.parse(value, FORMATTER_SECONDS);
+ case LENGTH_INCL_MILLIS:
+ return LocalDateTime.parse(value, FORMATTER_MILLIS);
+ case LENGTH_INCL_MICROS:
+ return LocalDateTime.parse(value, FORMATTER_MICROS);
+ case LENGTH_INCL_NANOS:
+ case LENGTH_INCL_PICOS:
+ return LocalDateTime.parse(value.substring(0, LENGTH_INCL_NANOS), FORMATTER_NANOS);
+ default:
+ throwFieldConvertError(value, TYPE);
+ }
+ } catch (DateTimeParseException e) {
+ throwFieldConvertError(value, TYPE);
}
- return millis;
+ return null;
+ }
+
+ private static Long getMillisForDay(String value) {
+ // Performance optimization: the calendar for the start of the day is cached.
+ return DATE_CACHE.computeIfAbsent(value.substring(0, 8));
+ }
+
+ private static long getTimeOffsetSeconds(String value) {
+ long timeOffset = (parseLong(value.substring(9, 11)) * 3600000L)
+ + (parseLong(value.substring(12, 14)) * 60000L)
+ + (parseLong(value.substring(15, LENGTH_INCL_SECONDS)) * 1000L);
+ return timeOffset;
}
private static void verifyFormat(String value) throws FieldConvertError {
- String type = "timestamp";
- if (value.length() != 17 && value.length() != 21) {
- throwFieldConvertError(value, type);
+ assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS);
+ assertDigitSequence(value, 0, 8, TYPE);
+ assertSeparator(value, 8, '-', TYPE);
+ assertDigitSequence(value, 9, 11, TYPE);
+ assertSeparator(value, 11, ':', TYPE);
+ assertDigitSequence(value, 12, 14, TYPE);
+ assertSeparator(value, 14, ':', TYPE);
+ assertDigitSequence(value, 15, LENGTH_INCL_SECONDS, TYPE);
+ if (value.length() == LENGTH_INCL_MILLIS) {
+ assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE);
+ assertDigitSequence(value, 18, LENGTH_INCL_MILLIS, TYPE);
+ } else if (value.length() == LENGTH_INCL_MICROS) {
+ assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE);
+ assertDigitSequence(value, 18, LENGTH_INCL_MICROS, TYPE);
+ } else if (value.length() == LENGTH_INCL_NANOS) {
+ assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE);
+ assertDigitSequence(value, 18, LENGTH_INCL_NANOS, TYPE);
+ } else if (value.length() == LENGTH_INCL_PICOS) {
+ assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE);
+ assertDigitSequence(value, 18, LENGTH_INCL_PICOS, TYPE);
+ } else if (value.length() != LENGTH_INCL_SECONDS) {
+ throwFieldConvertError(value, TYPE);
}
- assertDigitSequence(value, 0, 8, type);
- assertSeparator(value, 8, '-', type);
- assertDigitSequence(value, 9, 11, type);
- assertSeparator(value, 11, ':', type);
- assertDigitSequence(value, 12, 14, type);
- assertSeparator(value, 14, ':', type);
- assertDigitSequence(value, 15, 17, type);
- if (value.length() == 21) {
- assertSeparator(value, 17, '.', type);
- assertDigitSequence(value, 18, 21, type);
- } else if (value.length() != 17) {
- throwFieldConvertError(value, type);
+ }
+
+ /**
+ * @param localDateTime
+ * @return a java.util.Date filled from LocalDateTime (truncated to milliseconds).
+ */
+ public static Date getDate(LocalDateTime localDateTime) {
+ if (localDateTime != null) {
+ return Date.from(localDateTime.toInstant(ZoneOffset.UTC));
}
+ return null;
}
+
}
diff --git a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java
index cf3a026a0..5e60c0e1d 100644
--- a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java
+++ b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java
@@ -87,10 +87,14 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti
ioSession.closeNow();
}
} finally {
- ioSession.setAttribute("QFJ_RESET_IO_CONNECTOR", Boolean.TRUE);
+ ioSession.setAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR, Boolean.TRUE);
}
} else {
- log.error(reason, cause);
+ if (quickFixSession != null) {
+ LogUtil.logThrowable(quickFixSession.getLog(), reason, cause);
+ } else {
+ log.error(reason, cause);
+ }
}
}
@@ -121,20 +125,21 @@ public void messageReceived(IoSession ioSession, Object message) throws Exceptio
SessionID remoteSessionID = MessageUtils.getReverseSessionID(messageString);
Session quickFixSession = findQFSession(ioSession, remoteSessionID);
if (quickFixSession != null) {
- quickFixSession.getLog().onIncoming(messageString);
+ final Log sessionLog = quickFixSession.getLog();
+ sessionLog.onIncoming(messageString);
try {
Message fixMessage = parse(quickFixSession, messageString);
processMessage(ioSession, fixMessage);
} catch (InvalidMessage e) {
if (MsgType.LOGON.equals(MessageUtils.getMessageType(messageString))) {
- log.error("Invalid LOGON message, disconnecting: " + e.getMessage());
+ sessionLog.onErrorEvent("Invalid LOGON message, disconnecting: " + e.getMessage());
ioSession.closeNow();
} else {
- log.error("Invalid message: " + e.getMessage());
+ sessionLog.onErrorEvent("Invalid message: " + e.getMessage());
}
}
} else {
- log.error("Disconnecting; received message for unknown session: " + messageString);
+ log.error("Disconnecting; received message for unknown session: {}", messageString);
ioSession.closeNow();
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java
index c5149f843..573288f17 100644
--- a/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java
+++ b/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java
@@ -28,15 +28,14 @@
* it only handles message reception events.
*/
public interface EventHandlingStrategy {
-
/**
* Constant indicating how long we wait for an incoming message. After
* thread has been asked to stop, it can take up to this long to terminate.
*/
- static final long THREAD_WAIT_FOR_MESSAGE_MS = 250;
+ long THREAD_WAIT_FOR_MESSAGE_MS = 250;
// will be put to the eventQueue to signal a disconnection
- public static final Message END_OF_STREAM = new Message();
+ Message END_OF_STREAM = new Message();
void onMessage(Session quickfixSession, Message message);
diff --git a/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java b/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java
index d7351d5a3..1c32dcb36 100644
--- a/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java
+++ b/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java
@@ -64,11 +64,11 @@ public boolean send(String data) {
if (synchronousWrites) {
try {
if (!future.awaitUninterruptibly(synchronousWriteTimeout)) {
- log.error("Synchronous write timed out after " + synchronousWriteTimeout + "ms");
+ log.error("Synchronous write timed out after {}ms", synchronousWriteTimeout);
return false;
}
} catch (RuntimeException e) {
- log.error("Synchronous write failed: " + e.getMessage());
+ log.error("Synchronous write failed: {}", e.getMessage());
return false;
}
}
@@ -83,7 +83,7 @@ public void disconnect() {
// close event from being processed by this thread (if
// this thread is the MINA IO processor thread.
ioSession.closeOnFlush();
- ioSession.setAttribute("QFJ_RESET_IO_CONNECTOR", Boolean.TRUE);
+ ioSession.setAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR, Boolean.TRUE);
}
@Override
@@ -95,4 +95,7 @@ public String getRemoteAddress() {
return null;
}
+ IoSession getIoSession() {
+ return ioSession;
+ }
}
diff --git a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java
index 7d9b33aba..b653f15eb 100644
--- a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java
+++ b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java
@@ -19,21 +19,20 @@
package quickfix.mina;
-import java.net.SocketException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.session.IoSessionConfig;
import org.apache.mina.transport.socket.SocketSessionConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import quickfix.FieldConvertError;
import quickfix.field.converter.BooleanConverter;
import quickfix.field.converter.IntConverter;
+import java.net.SocketException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
/**
* This class holds the QFJ settings information related to networking options.
*/
@@ -65,7 +64,7 @@ public class NetworkingOptions {
public static final String IPTOC_RELIABILITY = "IPTOS_RELIABILITY";
public static final String IPTOC_THROUGHPUT = "IPTOS_THROUGHPUT";
public static final String IPTOC_LOWDELAY = "IPTOS_LOWDELAY";
- public static final Map trafficClasses = new HashMap();
+ public static final Map trafficClasses = new HashMap<>();
static {
trafficClasses.put(IPTOC_LOWCOST, 0x02);
@@ -101,8 +100,7 @@ public NetworkingOptions(Properties properties) throws FieldConvertError {
}
}
trafficClassSetting = trafficClassBits;
- log.info("Socket option: " + SETTING_SOCKET_TRAFFIC_CLASS + "= 0x"
- + Integer.toHexString(trafficClassBits) + " (" + trafficClassEnumString + ")");
+ log.info("Socket option: {}= 0x{} ({})", SETTING_SOCKET_TRAFFIC_CLASS, Integer.toHexString(trafficClassBits), trafficClassEnumString);
}
trafficClass = trafficClassSetting;
@@ -117,7 +115,7 @@ private Boolean getBoolean(Properties properties, String key, Boolean defaultVal
private void logOption(String key, Object value) {
if (value != null) {
- log.info("Socket option: " + key + "=" + value);
+ log.info("Socket option: {}={}", key, value);
}
}
diff --git a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java
index 640247b74..0e9bfaf6d 100644
--- a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java
+++ b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java
@@ -21,14 +21,28 @@
import java.net.InetSocketAddress;
import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoConnector;
+import org.apache.mina.transport.socket.SocketConnector;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.apache.mina.transport.vmpipe.VmPipeAcceptor;
import org.apache.mina.transport.vmpipe.VmPipeAddress;
import org.apache.mina.transport.vmpipe.VmPipeConnector;
+import org.apache.mina.proxy.ProxyConnector;
+import org.apache.mina.proxy.handlers.ProxyRequest;
+import org.apache.mina.proxy.handlers.http.HttpAuthenticationMethods;
+import org.apache.mina.proxy.handlers.http.HttpProxyConstants;
+import org.apache.mina.proxy.handlers.http.HttpProxyRequest;
+import org.apache.mina.proxy.handlers.socks.SocksProxyConstants;
+import org.apache.mina.proxy.handlers.socks.SocksProxyRequest;
+import org.apache.mina.proxy.session.ProxyIoSession;
+
import quickfix.ConfigError;
import quickfix.RuntimeError;
@@ -58,7 +72,7 @@ public static String getTypeString(int type) {
public static SocketAddress createSocketAddress(int transportType, String host,
int port) throws ConfigError {
- if (transportType == SOCKET) {
+ if (transportType == SOCKET || transportType == PROXY) {
return host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(port);
} else if (transportType == VM_PIPE) {
return new VmPipeAddress(port);
@@ -102,6 +116,108 @@ public static IoAcceptor createIoAcceptor(int transportType) {
}
}
+ public static ProxyConnector createIoProxyConnector(SocketConnector socketConnector,
+ InetSocketAddress address,
+ InetSocketAddress proxyAddress,
+ String proxyType,
+ String proxyVersion,
+ String proxyUser,
+ String proxyPassword,
+ String proxyDomain,
+ String proxyWorkstation ) throws ConfigError {
+
+ // Create proxy connector.
+ ProxyRequest req;
+
+ ProxyConnector connector = new ProxyConnector(socketConnector);
+ connector.setConnectTimeoutMillis(5000);
+
+ if (proxyType.equalsIgnoreCase("http")) {
+ req = createHttpProxyRequest(address, proxyVersion, proxyUser, proxyPassword, proxyDomain, proxyWorkstation);
+ } else if (proxyType.equalsIgnoreCase("socks")) {
+ req = createSocksProxyRequest(address, proxyVersion, proxyUser, proxyPassword);
+ } else {
+ throw new ConfigError("Proxy type must be http or socks");
+ }
+
+ ProxyIoSession proxyIoSession = new ProxyIoSession(proxyAddress, req);
+
+ List l = new ArrayList<>();
+ l.add(HttpAuthenticationMethods.NO_AUTH);
+ l.add(HttpAuthenticationMethods.DIGEST);
+ l.add(HttpAuthenticationMethods.BASIC);
+
+ proxyIoSession.setPreferedOrder(l);
+ connector.setProxyIoSession(proxyIoSession);
+
+ return connector;
+ }
+
+
+ private static ProxyRequest createHttpProxyRequest(InetSocketAddress address,
+ String proxyVersion,
+ String proxyUser,
+ String proxyPassword,
+ String proxyDomain,
+ String proxyWorkstation) {
+ HashMap props = new HashMap<>();
+ props.put(HttpProxyConstants.USER_PROPERTY, proxyUser);
+ props.put(HttpProxyConstants.PWD_PROPERTY, proxyPassword);
+ if (proxyDomain != null && proxyWorkstation != null) {
+ props.put(HttpProxyConstants.DOMAIN_PROPERTY, proxyDomain);
+ props.put(HttpProxyConstants.WORKSTATION_PROPERTY, proxyWorkstation);
+ }
+
+ HttpProxyRequest req = new HttpProxyRequest(address);
+ req.setProperties(props);
+ if (proxyVersion != null && proxyVersion.equalsIgnoreCase("1.1")) {
+ req.setHttpVersion(HttpProxyConstants.HTTP_1_1);
+ } else {
+ req.setHttpVersion(HttpProxyConstants.HTTP_1_0);
+ }
+
+ return req;
+ }
+
+
+ private static ProxyRequest createSocksProxyRequest(InetSocketAddress address,
+ String proxyVersion,
+ String proxyUser,
+ String proxyPassword) throws ConfigError {
+ SocksProxyRequest req;
+ if (proxyVersion.equalsIgnoreCase("4")) {
+ req = new SocksProxyRequest(
+ SocksProxyConstants.SOCKS_VERSION_4,
+ SocksProxyConstants.ESTABLISH_TCPIP_STREAM,
+ address,
+ proxyUser);
+
+ } else if (proxyVersion.equalsIgnoreCase("4a")) {
+ req = new SocksProxyRequest(
+ SocksProxyConstants.ESTABLISH_TCPIP_STREAM,
+ address.getAddress().getHostAddress(),
+ address.getPort(),
+ proxyUser);
+
+ } else if (proxyVersion.equalsIgnoreCase("5")) {
+ req = new SocksProxyRequest(
+ SocksProxyConstants.SOCKS_VERSION_5,
+ SocksProxyConstants.ESTABLISH_TCPIP_STREAM,
+ address,
+ proxyUser);
+
+ } else {
+ throw new ConfigError("SOCKS ProxyType must be 4,4a or 5");
+ }
+
+ if (proxyPassword != null) {
+ req.setPassword(proxyPassword);
+ }
+
+ return req;
+ }
+
+
public static IoConnector createIoConnector(SocketAddress address) throws ConfigError {
if (address instanceof InetSocketAddress) {
return new NioSocketConnector();
diff --git a/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java b/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java
new file mode 100644
index 000000000..aa2928e5e
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java
@@ -0,0 +1,10 @@
+package quickfix.mina;
+
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+interface QueueTracker {
+ void put(E e) throws InterruptedException;
+ E poll(long timeout, TimeUnit unit) throws InterruptedException;
+ int drainTo(Collection collection);
+}
diff --git a/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java b/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java
new file mode 100644
index 000000000..00f1ecb3b
--- /dev/null
+++ b/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java
@@ -0,0 +1,89 @@
+package quickfix.mina;
+
+import org.apache.mina.core.session.IoSession;
+import quickfix.Responder;
+import quickfix.Session;
+
+import java.util.Collection;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import static java.lang.String.format;
+
+/**
+ * Queue trackers factory methods
+ */
+final class QueueTrackers {
+ private static final String LOWER_WATERMARK_FMT = "inbound queue size < lower watermark (%d), socket reads resumed";
+ private static final String UPPER_WATERMARK_FMT = "inbound queue size > upper watermark (%d), socket reads suspended";
+
+ /**
+ * Watermarks-based queue tracker
+ */
+ static WatermarkTracker