Skip to content

Commit

Permalink
Fixed unsafe conccurent access to generic SimpleDateFormat instances
Browse files Browse the repository at this point in the history
SimpleDateFormat must not be used by concurrent threads without
synchronization for parsing or formating dates as it is not thread-safe
(internally holds a calendar instance that is not synchronized).

Prefer now DateTimeFormatter when possible as it is thread-safe without
concurrent access performance bottleneck (does not internally use
synchronization locks).
  • Loading branch information
luccioman committed Jun 28, 2018
1 parent 38a3a5e commit e97580d
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 104 deletions.
7 changes: 4 additions & 3 deletions htroot/ConfigAccountList_p.java
Expand Up @@ -18,8 +18,8 @@
// along with this program; if not, write to the Free Software // along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA


import java.util.Date;
import java.util.Iterator; import java.util.Iterator;

import net.yacy.cora.date.GenericFormatter; import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.protocol.RequestHeader; import net.yacy.cora.protocol.RequestHeader;
import net.yacy.data.UserDB; import net.yacy.data.UserDB;
Expand All @@ -30,7 +30,7 @@


public class ConfigAccountList_p { public class ConfigAccountList_p {


public static serverObjects respond(@SuppressWarnings("unused") final RequestHeader header, final serverObjects post, final serverSwitch env) { public static serverObjects respond(@SuppressWarnings("unused") final RequestHeader header, @SuppressWarnings("unused") final serverObjects post, final serverSwitch env) {


final serverObjects prop = new serverObjects(); final serverObjects prop = new serverObjects();
final Switchboard sb = (Switchboard) env; final Switchboard sb = (Switchboard) env;
Expand All @@ -54,7 +54,8 @@ public static serverObjects respond(@SuppressWarnings("unused") final RequestHea
prop.putHTML("userlist_" + numUsers + "_firstname", entry.getFirstName()); prop.putHTML("userlist_" + numUsers + "_firstname", entry.getFirstName());
prop.putHTML("userlist_" + numUsers + "_address", entry.getAddress()); prop.putHTML("userlist_" + numUsers + "_address", entry.getAddress());
if (entry.getLastAccess() != null) { if (entry.getLastAccess() != null) {
prop.put("userlist_" + numUsers + "_lastaccess", GenericFormatter.FORMAT_SIMPLE.format(new Date(entry.getLastAccess()))); prop.put("userlist_" + numUsers + "_lastaccess",
GenericFormatter.formatSafely(entry.getLastAccess(), GenericFormatter.FORMAT_SIMPLE));
} else { } else {
prop.put("userlist_" + numUsers + "_lastaccess", "never"); prop.put("userlist_" + numUsers + "_lastaccess", "never");
} }
Expand Down
9 changes: 5 additions & 4 deletions htroot/IndexImportMediawiki_p.java
Expand Up @@ -25,6 +25,7 @@
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.time.Instant;
import java.util.Date; import java.util.Date;


import org.apache.http.Header; import org.apache.http.Header;
Expand Down Expand Up @@ -131,11 +132,11 @@ public static serverObjects respond(final RequestHeader header, final serverObje


if (status == 0 && post.getBoolean("iffresh")) { if (status == 0 && post.getBoolean("iffresh")) {
long lastModified = getLastModified(sourceURL); long lastModified = getLastModified(sourceURL);
if (lastExecutionDate != null && lastModified != 0L if (lastExecutionDate != null && lastModified != 0L && Instant.ofEpochMilli(lastModified)
&& lastModified <= lastExecutionDate.getTime()) { .isBefore(lastExecutionDate.toInstant())) {
status = 5; status = 5;
prop.put("import_status_lastImportDate", prop.put("import_status_lastImportDate", GenericFormatter
GenericFormatter.FORMAT_SIMPLE.format(lastExecutionDate)); .formatSafely(lastExecutionDate.toInstant(), GenericFormatter.FORMAT_SIMPLE));


/* the import is not performed, but we increase here the api call count */ /* the import is not performed, but we increase here the api call count */
if(sb.tables != null) { if(sb.tables != null) {
Expand Down
11 changes: 7 additions & 4 deletions htroot/Table_API_p.java
Expand Up @@ -19,6 +19,7 @@


import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
Expand Down Expand Up @@ -58,6 +59,8 @@ public static serverObjects respond(final RequestHeader header, final serverObje
final Switchboard sb = (Switchboard) env; final Switchboard sb = (Switchboard) env;
final serverObjects prop = new serverObjects(); final serverObjects prop = new serverObjects();


final DateFormat dateFormat = GenericFormatter.newSimpleDateFormat();

prop.put("showexec", 0); prop.put("showexec", 0);
prop.put("showtable", 0); prop.put("showtable", 0);


Expand Down Expand Up @@ -182,7 +185,7 @@ public static serverObjects respond(final RequestHeader header, final serverObje
final int time = row.get(WorkTables.TABLE_API_COL_APICALL_SCHEDULE_TIME, 0); final int time = row.get(WorkTables.TABLE_API_COL_APICALL_SCHEDULE_TIME, 0);
final String dateNextExecStr = entry.getValue().trim(); final String dateNextExecStr = entry.getValue().trim();
try { try {
final Date dateNextExec = GenericFormatter.FORMAT_SIMPLE.parse(dateNextExecStr); final Date dateNextExec = dateFormat.parse(dateNextExecStr);


if(time != 0) { // Check there is effectively a schedule period on this row if(time != 0) { // Check there is effectively a schedule period on this row
if(dateNextExec.before(now)) { if(dateNextExec.before(now)) {
Expand Down Expand Up @@ -390,8 +393,8 @@ record = resultIterator.next();
prop.put("showtable_list_" + count + "_pk", UTF8.String(row.getPK())); prop.put("showtable_list_" + count + "_pk", UTF8.String(row.getPK()));
prop.put("showtable_list_" + count + "_count", count); prop.put("showtable_list_" + count + "_count", count);
prop.put("showtable_list_" + count + "_callcount", callcount); prop.put("showtable_list_" + count + "_callcount", callcount);
prop.put("showtable_list_" + count + "_dateRecording", date_recording == null ? "-" : GenericFormatter.FORMAT_SIMPLE.format(date_recording)); prop.put("showtable_list_" + count + "_dateRecording", date_recording == null ? "-" : dateFormat.format(date_recording));
prop.put("showtable_list_" + count + "_dateLastExec", date_last_exec == null ? "-" : GenericFormatter.FORMAT_SIMPLE.format(date_last_exec)); prop.put("showtable_list_" + count + "_dateLastExec", date_last_exec == null ? "-" : dateFormat.format(date_last_exec));


prop.put("showtable_list_" + count + "_editableDateNext", time != 0); prop.put("showtable_list_" + count + "_editableDateNext", time != 0);
final String enteredDateBeforeNow = nextExecDatesBeforeNow.get(rowPKStr); final String enteredDateBeforeNow = nextExecDatesBeforeNow.get(rowPKStr);
Expand All @@ -407,7 +410,7 @@ record = resultIterator.next();
} }


prop.put("showtable_list_" + count + "_editableDateNext_dateLastExecPattern", GenericFormatter.PATTERN_SIMPLE_REGEX); prop.put("showtable_list_" + count + "_editableDateNext_dateLastExecPattern", GenericFormatter.PATTERN_SIMPLE_REGEX);
prop.put("showtable_list_" + count + "_editableDateNext_dateNextExec", date_next_exec == null ? "-" : GenericFormatter.FORMAT_SIMPLE.format(date_next_exec)); prop.put("showtable_list_" + count + "_editableDateNext_dateNextExec", date_next_exec == null ? "-" : dateFormat.format(date_next_exec));
prop.put("showtable_list_" + count + "_editableDateNext_pk", rowPKStr); prop.put("showtable_list_" + count + "_editableDateNext_pk", rowPKStr);


prop.put("showtable_list_" + count + "_type", row.get(WorkTables.TABLE_API_COL_TYPE)); prop.put("showtable_list_" + count + "_type", row.get(WorkTables.TABLE_API_COL_TYPE));
Expand Down
232 changes: 207 additions & 25 deletions source/net/yacy/cora/date/GenericFormatter.java
Expand Up @@ -27,6 +27,13 @@
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
Expand All @@ -44,37 +51,159 @@ public class GenericFormatter extends AbstractFormatter implements DateFormatter
public static final String PATTERN_ANSIC = "EEE MMM d HH:mm:ss yyyy"; public static final String PATTERN_ANSIC = "EEE MMM d HH:mm:ss yyyy";
public static final String PATTERN_SIMPLE = "yyyy/MM/dd HH:mm:ss"; public static final String PATTERN_SIMPLE = "yyyy/MM/dd HH:mm:ss";


/** A regular expression matching the PATTERN_SIMPLE pattern (does not control last day of month (30/31 or 28/29 for february). /**
* Can be used as a HTML5 input field validation pattern */ * A regular expression matching the PATTERN_SIMPLE pattern (does not control
public static final String PATTERN_SIMPLE_REGEX ="[0-9]{4}/(0[1-9]|1[012])/(0[1-9]|1[0-9]|2[0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}"; * last day of month (30/31 or 28/29 for february). Can be used as a HTML5 input

* field validation pattern
public static final SimpleDateFormat FORMAT_SHORT_DAY = new SimpleDateFormat(PATTERN_SHORT_DAY, Locale.US); */
public static final SimpleDateFormat FORMAT_SHORT_MINUTE = new SimpleDateFormat(PATTERN_SHORT_MINUTE, Locale.US); public static final String PATTERN_SIMPLE_REGEX = "[0-9]{4}/(0[1-9]|1[012])/(0[1-9]|1[0-9]|2[0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}";
public static final SimpleDateFormat FORMAT_SHORT_SECOND = new SimpleDateFormat(PATTERN_SHORT_SECOND, Locale.US);
public static final SimpleDateFormat FORMAT_SHORT_MILSEC = new SimpleDateFormat(PATTERN_SHORT_MILSEC, Locale.US); /**
public static final SimpleDateFormat FORMAT_RFC1123_SHORT = new SimpleDateFormat(PATTERN_RFC1123_SHORT, Locale.US); * A thread-safe date formatter using the
public static final SimpleDateFormat FORMAT_ANSIC = new SimpleDateFormat(PATTERN_ANSIC, Locale.US); * {@link GenericFormatter#PATTERN_SHORT_DAY} pattern with the US locale on the
public static final SimpleDateFormat FORMAT_SIMPLE = new SimpleDateFormat(PATTERN_SIMPLE, Locale.US); * UTC time zone.

*/
static { public static final DateTimeFormatter FORMAT_SHORT_DAY = DateTimeFormatter
// we want GMT times on the formats as well as they don't support any timezone .ofPattern(PATTERN_SHORT_DAY.replace("yyyy", "uuuu"), Locale.US).withZone(ZoneOffset.UTC);
FORMAT_SHORT_DAY.setTimeZone(UTCtimeZone);
FORMAT_SHORT_SECOND.setTimeZone(UTCtimeZone); /**
FORMAT_SHORT_MILSEC.setTimeZone(UTCtimeZone); * A thread-safe date formatter using the
} * {@link GenericFormatter#PATTERN_SHORT_MINUTE} pattern with the US locale on
* the system time zone.
*/
public static final DateTimeFormatter FORMAT_SHORT_MINUTE = DateTimeFormatter
.ofPattern(PATTERN_SHORT_MINUTE.replace("yyyy", "uuuu"), Locale.US).withZone(ZoneId.systemDefault());


/**
* A thread-safe date formatter using the
* {@link GenericFormatter#PATTERN_SHORT_SECOND} pattern with the US locale on
* the UTC time zone.
*/
public static final DateTimeFormatter FORMAT_SHORT_SECOND = DateTimeFormatter
.ofPattern(PATTERN_SHORT_SECOND.replace("yyyy", "uuuu"), Locale.US).withZone(ZoneOffset.UTC);

/**
* A thread-safe date formatter using the
* {@link GenericFormatter#PATTERN_SHORT_MILSEC} pattern with the US locale on
* the UTC time zone.
*/
public static final DateTimeFormatter FORMAT_SHORT_MILSEC = new DateTimeFormatterBuilder()
.appendPattern(PATTERN_SHORT_MILSEC.replace("yyyy", "uuuu").replaceAll("SSS", ""))
.appendValue(ChronoField.MILLI_OF_SECOND, 3).toFormatter().withLocale(Locale.US)
.withZone(ZoneOffset.UTC);/* we can not use here the 'SSS' pattern for milliseconds on JDK 8 (see https://bugs.openjdk.java.net/browse/JDK-8031085) */

/**
* A thread-safe date formatter using the
* {@link GenericFormatter#PATTERN_RFC1123_SHORT} pattern with the US locale on
* the system time zone.
*/
public static final DateTimeFormatter FORMAT_RFC1123_SHORT = DateTimeFormatter
.ofPattern(PATTERN_RFC1123_SHORT.replace("yyyy", "uuuu"), Locale.US).withZone(ZoneId.systemDefault());

/**
* A thread-safe date formatter using the {@link GenericFormatter#PATTERN_ANSIC}
* pattern with the US locale on the system time zone.
*/
public static final DateTimeFormatter FORMAT_ANSIC = DateTimeFormatter.ofPattern(PATTERN_ANSIC.replace("yyyy", "uuuu"), Locale.US)
.withZone(ZoneId.systemDefault());

/**
* A thread-safe date formatter using the
* {@link GenericFormatter#PATTERN_SIMPLE} pattern (adapted for the DateTimeFormatter class) with the US locale on the
* system time zone.
*/
public static final DateTimeFormatter FORMAT_SIMPLE = DateTimeFormatter.ofPattern(PATTERN_SIMPLE.replace("yyyy", "uuuu"), Locale.US)
.withZone(ZoneId.systemDefault());

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_SHORT_DAY} pattern with the US
* locale.
*/
public static SimpleDateFormat newShortDayFormat() {
final SimpleDateFormat dateFormat = new SimpleDateFormat(GenericFormatter.PATTERN_SHORT_DAY, Locale.US);

// we want GMT times on the formats as well as they don't support any timezone
dateFormat.setTimeZone(UTCtimeZone);

return dateFormat;
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_SHORT_MINUTE} pattern with the US
* locale.
*/
public static SimpleDateFormat newShortMinuteFormat() {
return new SimpleDateFormat(GenericFormatter.PATTERN_SHORT_MINUTE, Locale.US);
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_SHORT_SECOND} pattern with the US
* locale.
*/
public static SimpleDateFormat newShortSecondFormat() {
final SimpleDateFormat dateFormat = new SimpleDateFormat(GenericFormatter.PATTERN_SHORT_SECOND, Locale.US);

// we want GMT times on the formats as well as they don't support any timezone
dateFormat.setTimeZone(UTCtimeZone);

return dateFormat;
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_SHORT_MILSEC} pattern with the US
* locale.
*/
public static SimpleDateFormat newShortMilsecFormat() {
final SimpleDateFormat dateFormat = new SimpleDateFormat(GenericFormatter.PATTERN_SHORT_MILSEC, Locale.US);

// we want GMT times on the formats as well as they don't support any timezone
dateFormat.setTimeZone(UTCtimeZone);

return dateFormat;
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_RFC1123_SHORT} pattern with the US
* locale.
*/
public static SimpleDateFormat newRfc1123ShortFormat() {
return new SimpleDateFormat(GenericFormatter.PATTERN_RFC1123_SHORT, Locale.US);
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_ANSIC} pattern with the US locale.
*/
public static SimpleDateFormat newAnsicFormat() {
return new SimpleDateFormat(GenericFormatter.PATTERN_ANSIC, Locale.US);
}

/**
* @return a new SimpleDateFormat instance using the
* {@link GenericFormatter#PATTERN_SIMPLE} pattern with the US locale.
*/
public static SimpleDateFormat newSimpleDateFormat() {
return new SimpleDateFormat(GenericFormatter.PATTERN_SIMPLE, Locale.US);
}


public static final long time_second = 1000L; public static final long time_second = 1000L;
public static final long time_minute = 60000L; public static final long time_minute = 60000L;
public static final long time_hour = 60 * time_minute; public static final long time_hour = 60 * time_minute;
public static final long time_day = 24 * time_hour; public static final long time_day = 24 * time_hour;


public static final GenericFormatter SHORT_DAY_FORMATTER = new GenericFormatter(FORMAT_SHORT_DAY, time_minute); public static final GenericFormatter SHORT_DAY_FORMATTER = new GenericFormatter(newShortDayFormat(), time_minute);
public static final GenericFormatter SHORT_MINUTE_FORMATTER = new GenericFormatter(FORMAT_SHORT_MINUTE, time_second); public static final GenericFormatter SHORT_MINUTE_FORMATTER = new GenericFormatter(newShortMinuteFormat(), time_second);
public static final GenericFormatter SHORT_SECOND_FORMATTER = new GenericFormatter(FORMAT_SHORT_SECOND, time_second); public static final GenericFormatter SHORT_SECOND_FORMATTER = new GenericFormatter(newShortSecondFormat(), time_second);
public static final GenericFormatter SHORT_MILSEC_FORMATTER = new GenericFormatter(FORMAT_SHORT_MILSEC, 1); public static final GenericFormatter SHORT_MILSEC_FORMATTER = new GenericFormatter(newShortMilsecFormat(), 1);
public static final GenericFormatter RFC1123_SHORT_FORMATTER = new GenericFormatter(FORMAT_RFC1123_SHORT, time_minute); public static final GenericFormatter RFC1123_SHORT_FORMATTER = new GenericFormatter(newRfc1123ShortFormat(), time_minute);
public static final GenericFormatter ANSIC_FORMATTER = new GenericFormatter(FORMAT_ANSIC, time_second); public static final GenericFormatter ANSIC_FORMATTER = new GenericFormatter(newAnsicFormat(), time_second);
public static final GenericFormatter SIMPLE_FORMATTER = new GenericFormatter(FORMAT_SIMPLE, time_second); public static final GenericFormatter SIMPLE_FORMATTER = new GenericFormatter(newSimpleDateFormat(), time_second);


private final SimpleDateFormat dateFormat; private final SimpleDateFormat dateFormat;
private final long maxCacheDiff; private final long maxCacheDiff;
Expand Down Expand Up @@ -201,6 +330,59 @@ public static String UTCDiffString() {
} }


private final static DecimalFormat D2 = new DecimalFormat("00"); private final static DecimalFormat D2 = new DecimalFormat("00");

/**
* Safely format the given time value using the given formatter. Fallback to
* ISO-8601 representation or to raw time value without exception when the
* format can not be applied.
*
* @param time
* a time value as millisecnods from Epoch (1970-01-01T00:00:00Z)
* @param formatter
* the formatter to use
* @return a String representation of the time value
*/
public static String formatSafely(final long time, final DateTimeFormatter formatter) {
String res;
try {
res = formatSafely(Instant.ofEpochMilli(time), formatter);
} catch (final DateTimeException e) {
/*
* Can occur on Instant.ofEpochMilli when the time value is greater than
* Instant.MAX.toEpochMilli() or lower than Instant.MIN.toEpochMilli()
*/
res = String.valueOf(time);
}
return res;
}

/**
* Safely format the given instant using the given formatter. Fallback to
* ISO-8601 representation without exception when the format can not be applied.
*
* @param instant
* the instant to format
* @param formatter
* the formatter to use
* @return a String representation of the time value
*/
public static String formatSafely(final Instant instant, final DateTimeFormatter formatter) {
String res;
if (instant == null) {
res = "";
} else {
try {
if (formatter != null) {
res = formatter.format(instant);
} else {
res = instant.toString();
}
} catch (final DateTimeException e) {
res = instant.toString();
}
}
return res;
}


public static void main(String[] args) { public static void main(String[] args) {
System.out.println(UTCDiffString()); System.out.println(UTCDiffString());
Expand Down
2 changes: 1 addition & 1 deletion source/net/yacy/cora/protocol/HeaderFramework.java
Expand Up @@ -245,7 +245,7 @@ public HeaderFramework(final Map<String, String> othermap) {
// RFC 1036/850 (old) "Monday, 12-Nov-07 10:11:12 GMT" // RFC 1036/850 (old) "Monday, 12-Nov-07 10:11:12 GMT"
FORMAT_RFC1036, FORMAT_RFC1036,
// ANSI C asctime() "Mon Nov 12 10:11:12 2007" // ANSI C asctime() "Mon Nov 12 10:11:12 2007"
GenericFormatter.FORMAT_ANSIC, GenericFormatter.newAnsicFormat(),
}; };




Expand Down
4 changes: 2 additions & 2 deletions source/net/yacy/document/DateDetection.java
Expand Up @@ -674,8 +674,8 @@ public static Date parseLine(final String text, final int timezoneOffset) {
// check standard date formats // check standard date formats
try {d = CONFORM.parse(text);} catch (ParseException e) {} try {d = CONFORM.parse(text);} catch (ParseException e) {}
//if (d == null) try {d = GenericFormatter.FORMAT_SHORT_DAY.parse(text);} catch (ParseException e) {} // did not work well and fired for wrong formats; do not use //if (d == null) try {d = GenericFormatter.FORMAT_SHORT_DAY.parse(text);} catch (ParseException e) {} // did not work well and fired for wrong formats; do not use
if (d == null) try {d = GenericFormatter.FORMAT_RFC1123_SHORT.parse(text);} catch (ParseException e) {} if (d == null) try {d = GenericFormatter.newRfc1123ShortFormat().parse(text);} catch (ParseException e) {}
if (d == null) try {d = GenericFormatter.FORMAT_ANSIC.parse(text);} catch (ParseException e) {} if (d == null) try {d = GenericFormatter.newAnsicFormat().parse(text);} catch (ParseException e) {}


if (d == null) { if (d == null) {
// check other date formats // check other date formats
Expand Down
2 changes: 1 addition & 1 deletion source/net/yacy/kelondro/blob/ArrayStack.java
Expand Up @@ -103,7 +103,7 @@ public class ArrayStack implements BLOB {
private final ExecutorService executor; private final ExecutorService executor;


// use our own formatter to prevent concurrency locks with other processes // use our own formatter to prevent concurrency locks with other processes
private final static GenericFormatter my_SHORT_MILSEC_FORMATTER = new GenericFormatter(GenericFormatter.FORMAT_SHORT_MILSEC, 1); private final static GenericFormatter my_SHORT_MILSEC_FORMATTER = new GenericFormatter(GenericFormatter.newShortMilsecFormat(), 1);




public ArrayStack( public ArrayStack(
Expand Down

0 comments on commit e97580d

Please sign in to comment.