From 7622516137b74ad49f3cff1aa2189032ee7b088b Mon Sep 17 00:00:00 2001 From: Kevin Kofler Date: Thu, 11 Jan 2018 19:54:01 +0100 Subject: [PATCH 1/2] Add support for a serverTimeZone property The serverTimeZone property can be passed in the connection string and allows globally setting a time zone, which is used for all date and time conversions where no Calendar is explicitly specified, instead of using the client JVM's time zone. Having this option set explicitly in all clients ensures that they all agree on the time zone to use, independently of what their default time zone is. Forcing serverTimeZone to UTC even also helps if the database contains times in more than one time zone, because UTC has no invalid time ranges, so no information is lost converting between a time with unspecified time zone and a time stamp in UTC. Some other JDBC drivers (e.g., MySQL Connector/J and MariaDB Connector/J) also have such an option. --- .../com/microsoft/sqlserver/jdbc/DDC.java | 10 ++++-- .../microsoft/sqlserver/jdbc/IOBuffer.java | 31 ++++++++-------- .../sqlserver/jdbc/SQLServerBulkCopy.java | 8 ++--- .../sqlserver/jdbc/SQLServerConnection.java | 13 +++++++ .../sqlserver/jdbc/SQLServerDataSource.java | 9 +++++ .../sqlserver/jdbc/SQLServerDriver.java | 2 ++ .../sqlserver/jdbc/SQLServerResource.java | 1 + .../com/microsoft/sqlserver/jdbc/dtv.java | 35 ++++++++++--------- 8 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 77891c37b..c69a34940 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java @@ -744,7 +744,10 @@ private static String fractionalSecondsString(long subSecondNanos, * * @param timeZoneCalendar * (optional) a Calendar representing the time zone to associate with the resulting converted value. For DATETIMEOFFSET, this parameter - * represents the time zone associated with the value. Null means to use the default VM time zone. + * represents the time zone associated with the value. Null means to use the default server time zone. + * + * @param serverTimeZone + * the default server time zone, used if timeZoneCalendar is null. * * @param daysSinceBaseDate * The date part of the value, expressed as a number of days since the base date for the specified SQL Server data type. For DATETIME @@ -766,12 +769,13 @@ private static String fractionalSecondsString(long subSecondNanos, static final Object convertTemporalToObject(JDBCType jdbcType, SSType ssType, Calendar timeZoneCalendar, + TimeZone serverTimeZone, int daysSinceBaseDate, long ticksSinceMidnight, int fractionalSecondsScale) { // Determine the local time zone to associate with the value. Use the default VM // time zone if no time zone is otherwise specified. - TimeZone localTimeZone = (null != timeZoneCalendar) ? timeZoneCalendar.getTimeZone() : TimeZone.getDefault(); + TimeZone localTimeZone = (null != timeZoneCalendar) ? timeZoneCalendar.getTimeZone() : serverTimeZone; // Assumed time zone associated with the date and time parts of the value. // @@ -909,7 +913,7 @@ static final Object convertTemporalToObject(JDBCType jdbcType, } int localMillisOffset; if (null == timeZoneCalendar) { - TimeZone tz = TimeZone.getDefault(); + TimeZone tz = serverTimeZone; GregorianCalendar _cal = new GregorianCalendar(componentTimeZone, Locale.US); _cal.setLenient(true); _cal.clear(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index d87a9cb14..5264363f8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -3437,7 +3437,7 @@ void writeSqlVariantInternalBigDecimal(BigDecimal bigDecimalVal, } void writeSmalldatetime(String value) throws SQLServerException { - GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); utcMillis = timestampValue.getTime(); @@ -3474,7 +3474,7 @@ void writeSmalldatetime(String value) throws SQLServerException { } void writeDatetime(String value) throws SQLServerException { - GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) int subSecondNanos; java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); @@ -3522,7 +3522,7 @@ void writeDatetime(String value) throws SQLServerException { } void writeDate(String value) throws SQLServerException { - GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); long utcMillis; java.sql.Date dateValue = java.sql.Date.valueOf(value); utcMillis = dateValue.getTime(); @@ -3537,7 +3537,7 @@ void writeDate(String value) throws SQLServerException { void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException { - GregorianCalendar calendar = initializeCalender(TimeZone.getDefault()); + GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) int subSecondNanos; utcMillis = value.getTime(); @@ -3624,10 +3624,10 @@ void writeOffsetDateTimeWithTimezone(OffsetDateTime offsetDateTimeValue, calendar.setTimeInMillis(utcMillis); // Local timezone value in minutes - int minuteAdjustment = ((TimeZone.getDefault().getRawOffset()) / (60 * 1000)); + int minuteAdjustment = ((con.getServerTimeZone().getRawOffset()) / (60 * 1000)); // check if date is in day light savings and add daylight saving minutes - if (TimeZone.getDefault().inDaylightTime(calendar.getTime())) - minuteAdjustment += (TimeZone.getDefault().getDSTSavings()) / (60 * 1000); + if (con.getServerTimeZone().inDaylightTime(calendar.getTime())) + minuteAdjustment += (con.getServerTimeZone().getDSTSavings()) / (60 * 1000); // If the local time is negative then positive minutesOffset must be subtracted from calender minuteAdjustment += (minuteAdjustment < 0) ? (minutesOffset * (-1)) : minutesOffset; calendar.add(Calendar.MINUTE, minuteAdjustment); @@ -3681,10 +3681,10 @@ void writeOffsetTimeWithTimezone(OffsetTime offsetTimeValue, calendar = initializeCalender(timeZone); calendar.setTimeInMillis(utcMillis); - int minuteAdjustment = (TimeZone.getDefault().getRawOffset()) / (60 * 1000); + int minuteAdjustment = (con.getServerTimeZone().getRawOffset()) / (60 * 1000); // check if date is in day light savings and add daylight saving minutes to Local timezone(in minutes) - if (TimeZone.getDefault().inDaylightTime(calendar.getTime())) - minuteAdjustment += ((TimeZone.getDefault().getDSTSavings()) / (60 * 1000)); + if (con.getServerTimeZone().inDaylightTime(calendar.getTime())) + minuteAdjustment += ((con.getServerTimeZone().getDSTSavings()) / (60 * 1000)); // If the local time is negative then positive minutesOffset must be subtracted from calender minuteAdjustment += (minuteAdjustment < 0) ? (minutesOffset * (-1)) : minutesOffset; calendar.add(Calendar.MINUTE, minuteAdjustment); @@ -6868,7 +6868,7 @@ final Object readDateTime(int valueLength, } // Convert the DATETIME/SMALLDATETIME value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, appTimeZoneCalendar, daysSinceSQLBaseDate, msecSinceMidnight, 0); // scale + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, appTimeZoneCalendar, con.getServerTimeZone(), daysSinceSQLBaseDate, msecSinceMidnight, 0); // scale // (ignored // for // fixed-scale @@ -6886,7 +6886,7 @@ final Object readDate(int valueLength, int localDaysIntoCE = readDaysIntoCE(); // Convert the DATE value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATE, appTimeZoneCalendar, localDaysIntoCE, 0, // midnight local to app time zone + return DDC.convertTemporalToObject(jdbcType, SSType.DATE, appTimeZoneCalendar, con.getServerTimeZone(), localDaysIntoCE, 0, // midnight local to app time zone 0); // scale (ignored for DATE) } @@ -6901,7 +6901,7 @@ final Object readTime(int valueLength, long localNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale()); // Convert the TIME value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.TIME, appTimeZoneCalendar, 0, localNanosSinceMidnight, typeInfo.getScale()); + return DDC.convertTemporalToObject(jdbcType, SSType.TIME, appTimeZoneCalendar, con.getServerTimeZone(), 0, localNanosSinceMidnight, typeInfo.getScale()); } final Object readDateTime2(int valueLength, @@ -6916,7 +6916,7 @@ final Object readDateTime2(int valueLength, int localDaysIntoCE = readDaysIntoCE(); // Convert the DATETIME2 value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, appTimeZoneCalendar, localDaysIntoCE, localNanosSinceMidnight, + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, appTimeZoneCalendar, con.getServerTimeZone(), localDaysIntoCE, localNanosSinceMidnight, typeInfo.getScale()); } @@ -6934,7 +6934,8 @@ final Object readDateTimeOffset(int valueLength, // Convert the DATETIMEOFFSET value to the desired Java type. return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET, - new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), utcDaysIntoCE, utcNanosSinceMidnight, + new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), + con.getServerTimeZone(), utcDaysIntoCE, utcNanosSinceMidnight, typeInfo.getScale()); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index a383e1946..ec2c9a186 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -3242,7 +3242,7 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, switch (srcTemporalJdbcType) { case DATE: - calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US); + calendar = new GregorianCalendar(connection.getServerTimeZone(), java.util.Locale.US); calendar.setLenient(true); calendar.clear(); calendar.setTimeInMillis(((Date) colValue).getTime()); @@ -3251,7 +3251,7 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, SSType.DATE, (short) 0); case TIME: - calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US); + calendar = new GregorianCalendar(connection.getServerTimeZone(), java.util.Locale.US); calendar.setLenient(true); calendar.clear(); utcMillis = ((java.sql.Timestamp) colValue).getTime(); @@ -3268,7 +3268,7 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, return tdsWriter.writeEncryptedScaledTemporal(calendar, subSecondNanos, scale, SSType.TIME, (short) 0); case TIMESTAMP: - calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US); + calendar = new GregorianCalendar(connection.getServerTimeZone(), java.util.Locale.US); calendar.setLenient(true); calendar.clear(); utcMillis = ((java.sql.Timestamp) colValue).getTime(); @@ -3278,7 +3278,7 @@ private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, case DATETIME: case SMALLDATETIME: - calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US); + calendar = new GregorianCalendar(connection.getServerTimeZone(), java.util.Locale.US); calendar.setLenient(true); calendar.clear(); utcMillis = ((java.sql.Timestamp) colValue).getTime(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index e883f9a75..9f4b3ccf0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -43,6 +43,7 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; @@ -543,6 +544,12 @@ boolean getServerSupportsColumnEncryption() { return serverSupportsColumnEncryption; } + private TimeZone serverTimeZone = TimeZone.getDefault(); + + TimeZone getServerTimeZone() { + return serverTimeZone; + } + static boolean isWindows; static Map globalSystemColumnEncryptionKeyStoreProviders = new HashMap<>(); static { @@ -1729,6 +1736,12 @@ else if (0 == requestedPacketSize) activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString()); } + sPropKey = SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + serverTimeZone = sPropValue.isEmpty() ? TimeZone.getDefault() : TimeZone.getTimeZone(sPropValue); + } + FailoverInfo fo = null; String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString(); String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 49a6b25ba..788cf985d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -612,6 +612,15 @@ public String getTrustManagerConstructorArg() { return getStringProperty(connectionProps, SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(), SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.getDefaultValue()); } + + public void setServerTimeZone(String serverTimeZone) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(), serverTimeZone); + } + + public String getServerTimeZone() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(), + SQLServerDriverStringProperty.SERVER_TIME_ZONE.getDefaultValue()); + } // The URL property is exposed for backwards compatibility reasons. Also, several // Java Application servers expect a setURL function on the DataSource and set it diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 8181219ba..63a420347 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -274,6 +274,7 @@ enum SQLServerDriverStringProperty SELECT_METHOD ("selectMethod", "direct"), SERVER_NAME ("serverName", ""), SERVER_SPN ("serverSpn", ""), + SERVER_TIME_ZONE ("serverTimeZone", ""), TRUST_STORE_TYPE ("trustStoreType", "JKS"), TRUST_STORE ("trustStore", ""), TRUST_STORE_PASSWORD ("trustStorePassword", ""), @@ -408,6 +409,7 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(), SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(), SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(), SQLServerDriverStringProperty.SERVER_TIME_ZONE.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(), Boolean.toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.TRUST_STORE_TYPE.toString(), SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue(), false, null), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 63e701505..ee1fe1979 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -392,5 +392,6 @@ protected Object[][] getContents() { {"R_invalidDataTypeSupportForSQLVariant", "Unexpected TDS type ' '{0}' ' in SQL_VARIANT."}, {"R_sslProtocolPropertyDescription", "SSL protocol label from TLS, TLSv1, TLSv1.1 & TLSv1.2. The default is TLS."}, {"R_invalidSSLProtocol", "SSL Protocol {0} label is not valid. Only TLS, TLSv1, TLSv1.1 & TLSv1.2 are supported."}, + {"R_serverTimeZonePropertyDescription", "The default time zone to use to convert date and time values. The default is the VM default time zone."}, }; } \ No newline at end of file diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index e0ea30378..7452555f9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -519,14 +519,14 @@ private void sendTemporal(DTV dtv, * the DTV's calendar directly, as it may not be Gregorian... */ if (null != value) { - TimeZone timeZone = TimeZone.getDefault(); // Time zone to associate with the value in the Gregorian calendar + TimeZone timeZone = conn.getServerTimeZone(); // Time zone to associate with the value in the Gregorian calendar long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) // Figure out the value components according to the type of the Java object passed in... switch (javaType) { case TIME: { - // Set the time zone from the calendar supplied by the app or use the JVM default - timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : TimeZone.getDefault(); + // Set the time zone from the calendar supplied by the app or use the server default + timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : conn.getServerTimeZone(); utcMillis = ((java.sql.Time) value).getTime(); subSecondNanos = Nanos.PER_MILLISECOND * (int) (utcMillis % 1000); @@ -545,16 +545,16 @@ private void sendTemporal(DTV dtv, } case DATE: { - // Set the time zone from the calendar supplied by the app or use the JVM default - timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : TimeZone.getDefault(); + // Set the time zone from the calendar supplied by the app or use the server default + timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : conn.getServerTimeZone(); utcMillis = ((java.sql.Date) value).getTime(); break; } case TIMESTAMP: { - // Set the time zone from the calendar supplied by the app or use the JVM default - timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : TimeZone.getDefault(); + // Set the time zone from the calendar supplied by the app or use the server default + timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : conn.getServerTimeZone(); java.sql.Timestamp timestampValue = (java.sql.Timestamp) value; utcMillis = timestampValue.getTime(); @@ -565,8 +565,8 @@ private void sendTemporal(DTV dtv, case UTILDATE: { // java.util.Date is mapped to JDBC type TIMESTAMP // java.util.Date and java.sql.Date are both millisecond precision - // Set the time zone from the calendar supplied by the app or use the JVM default - timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : TimeZone.getDefault(); + // Set the time zone from the calendar supplied by the app or use the server default + timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : conn.getServerTimeZone(); utcMillis = ((java.util.Date) value).getTime(); @@ -589,8 +589,8 @@ private void sendTemporal(DTV dtv, case CALENDAR: { // java.util.Calendar is mapped to JDBC type TIMESTAMP // java.util.Calendar is millisecond precision - // Set the time zone from the calendar supplied by the app or use the JVM default - timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : TimeZone.getDefault(); + // Set the time zone from the calendar supplied by the app or use the server default + timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone() : conn.getServerTimeZone(); utcMillis = ((java.util.Calendar) value).getTimeInMillis(); @@ -3705,14 +3705,14 @@ else if (4 == decryptedValue.length) { // cannot reuse method int daysIntoCE = getDaysIntoCE(decryptedValue, baseSSType); - return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, daysIntoCE, 0, 0); + return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, con.getServerTimeZone(), daysIntoCE, 0, 0); } case TIME: { long localNanosSinceMidnight = readNanosSinceMidnightAE(decryptedValue, baseTypeInfo.getScale(), baseSSType); - return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, 0, localNanosSinceMidnight, baseTypeInfo.getScale()); + return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, con.getServerTimeZone(), 0, localNanosSinceMidnight, baseTypeInfo.getScale()); } case DATETIME2: { @@ -3732,7 +3732,7 @@ else if (4 == decryptedValue.length) { int daysIntoCE = getDaysIntoCE(datePortion, baseSSType); // Convert the DATETIME2 value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale()); + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, con.getServerTimeZone(), daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale()); } @@ -3745,7 +3745,7 @@ else if (4 == decryptedValue.length) { // SQL smalldatetime has less precision. It stores 2 bytes // for the days since SQL Base Date and 2 bytes for minutes // after midnight. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, Util.readUnsignedShort(decryptedValue, 0), + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), Util.readUnsignedShort(decryptedValue, 0), Util.readUnsignedShort(decryptedValue, 2) * 60L * 1000L, 0); } @@ -3758,7 +3758,7 @@ else if (4 == decryptedValue.length) { // SQL datetime is 4 bytes for days since SQL Base Date // (January 1, 1900 00:00:00 GMT) and 4 bytes for // the number of three hundredths (1/300) of a second since midnight. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, Util.readInt(decryptedValue, 0), + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), Util.readInt(decryptedValue, 0), (Util.readInt(decryptedValue, 4) * 10 + 1) / 3, 0); } @@ -3778,7 +3778,8 @@ else if (4 == decryptedValue.length) { int localMinutesOffset = ByteBuffer.wrap(offsetPortion).order(ByteOrder.LITTLE_ENDIAN).getShort(); return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET, - new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), daysIntoCE, localNanosSinceMidnight, + new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), + con.getServerTimeZone(), daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale()); } From 0031e3d0a816d0070525fc3768308d1943d4c674 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 27 Aug 2018 17:46:11 -0700 Subject: [PATCH 2/2] Source reformatted - fixed issues due to merge. --- .../com/microsoft/sqlserver/jdbc/DDC.java | 11 +- .../microsoft/sqlserver/jdbc/IOBuffer.java | 35 +- ...rColumnEncryptionJavaKeyStoreProvider.java | 3 +- .../sqlserver/jdbc/SQLServerConnection.java | 3 +- .../sqlserver/jdbc/SQLServerDataSource.java | 2 +- .../sqlserver/jdbc/SQLServerDriver.java | 3 +- .../sqlserver/jdbc/SQLServerResource.java | 3 +- .../sqlserver/jdbc/SQLServerResultSet.java | 7 +- .../jdbc/SQLServerSpatialDatatype.java | 4 +- .../com/microsoft/sqlserver/jdbc/Util.java | 12 +- .../com/microsoft/sqlserver/jdbc/dtv.java | 18 +- .../mssql/googlecode/cityhash/CityHash.java | 127 +- .../googlecode/cityhash/package-info.java | 10 +- .../ConcurrentLinkedHashMap.java | 2728 ++++++++--------- .../concurrentlinkedhashmap/EntryWeigher.java | 43 +- .../EvictionListener.java | 52 +- .../concurrentlinkedhashmap/LinkedDeque.java | 809 +++-- .../concurrentlinkedhashmap/Weigher.java | 40 +- .../concurrentlinkedhashmap/Weighers.java | 473 ++- .../concurrentlinkedhashmap/package-info.java | 33 +- .../jdbc/resultset/ResultSetTest.java | 16 +- 21 files changed, 2152 insertions(+), 2280 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 9a5489283..39143d648 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java @@ -734,7 +734,7 @@ private static String fractionalSecondsString(long subSecondNanos, int scale) { * default VM time zone. * * @param serverTimeZone - * the default server time zone, used if timeZoneCalendar is null. + * the default server time zone, used if timeZoneCalendar is null. * * @param daysSinceBaseDate * The date part of the value, expressed as a number of days since the base date for the specified SQL Server @@ -754,13 +754,8 @@ private static String fractionalSecondsString(long subSecondNanos, int scale) { * * @return a Java object of the desired type. */ - static final Object convertTemporalToObject(JDBCType jdbcType, - SSType ssType, - Calendar timeZoneCalendar, - TimeZone serverTimeZone, - int daysSinceBaseDate, - long ticksSinceMidnight, - int fractionalSecondsScale) { + static final Object convertTemporalToObject(JDBCType jdbcType, SSType ssType, Calendar timeZoneCalendar, + TimeZone serverTimeZone, int daysSinceBaseDate, long ticksSinceMidnight, int fractionalSecondsScale) { // Determine the local time zone to associate with the value. Use the default VM // time zone if no time zone is otherwise specified. TimeZone localTimeZone = (null != timeZoneCalendar) ? timeZoneCalendar.getTimeZone() : serverTimeZone; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 4c13d8083..4230b65a0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -3359,7 +3359,7 @@ void writeSqlVariantInternalBigDecimal(BigDecimal bigDecimalVal, int srcJdbcType void writeSmalldatetime(String value) throws SQLServerException { GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); - long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); utcMillis = timestampValue.getTime(); @@ -3398,7 +3398,7 @@ void writeSmalldatetime(String value) throws SQLServerException { void writeDatetime(String value) throws SQLServerException { GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); - long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) int subSecondNanos; java.sql.Timestamp timestampValue = java.sql.Timestamp.valueOf(value); @@ -3463,10 +3463,9 @@ void writeDate(String value) throws SQLServerException { SSType.DATE); } - void writeTime(java.sql.Timestamp value, - int scale) throws SQLServerException { + void writeTime(java.sql.Timestamp value, int scale) throws SQLServerException { GregorianCalendar calendar = initializeCalender(con.getServerTimeZone()); - long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) + long utcMillis; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) int subSecondNanos; utcMillis = value.getTime(); @@ -6734,12 +6733,13 @@ final Object readDateTime(int valueLength, Calendar appTimeZoneCalendar, JDBCTyp } // Convert the DATETIME/SMALLDATETIME value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, appTimeZoneCalendar, con.getServerTimeZone(), daysSinceSQLBaseDate, msecSinceMidnight, 0); // scale - // (ignored - // for - // fixed-scale - // DATETIME/SMALLDATETIME - // types) + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, appTimeZoneCalendar, con.getServerTimeZone(), + daysSinceSQLBaseDate, msecSinceMidnight, 0); // scale + // (ignored + // for + // fixed-scale + // DATETIME/SMALLDATETIME + // types) } final Object readDate(int valueLength, Calendar appTimeZoneCalendar, JDBCType jdbcType) throws SQLServerException { @@ -6750,7 +6750,8 @@ final Object readDate(int valueLength, Calendar appTimeZoneCalendar, JDBCType jd int localDaysIntoCE = readDaysIntoCE(); // Convert the DATE value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATE, appTimeZoneCalendar, con.getServerTimeZone(), localDaysIntoCE, 0, // midnight local to app time zone + return DDC.convertTemporalToObject(jdbcType, SSType.DATE, appTimeZoneCalendar, con.getServerTimeZone(), + localDaysIntoCE, 0, // midnight local to app time zone 0); // scale (ignored for DATE) } @@ -6763,7 +6764,8 @@ final Object readTime(int valueLength, TypeInfo typeInfo, Calendar appTimeZoneCa long localNanosSinceMidnight = readNanosSinceMidnight(typeInfo.getScale()); // Convert the TIME value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.TIME, appTimeZoneCalendar, con.getServerTimeZone(), 0, localNanosSinceMidnight, typeInfo.getScale()); + return DDC.convertTemporalToObject(jdbcType, SSType.TIME, appTimeZoneCalendar, con.getServerTimeZone(), 0, + localNanosSinceMidnight, typeInfo.getScale()); } final Object readDateTime2(int valueLength, TypeInfo typeInfo, Calendar appTimeZoneCalendar, @@ -6776,8 +6778,8 @@ final Object readDateTime2(int valueLength, TypeInfo typeInfo, Calendar appTimeZ int localDaysIntoCE = readDaysIntoCE(); // Convert the DATETIME2 value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, appTimeZoneCalendar, con.getServerTimeZone(), localDaysIntoCE, localNanosSinceMidnight, - typeInfo.getScale()); + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, appTimeZoneCalendar, con.getServerTimeZone(), + localDaysIntoCE, localNanosSinceMidnight, typeInfo.getScale()); } final Object readDateTimeOffset(int valueLength, TypeInfo typeInfo, JDBCType jdbcType) throws SQLServerException { @@ -6793,8 +6795,7 @@ final Object readDateTimeOffset(int valueLength, TypeInfo typeInfo, JDBCType jdb // Convert the DATETIMEOFFSET value to the desired Java type. return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET, new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), - con.getServerTimeZone(), utcDaysIntoCE, utcNanosSinceMidnight, - typeInfo.getScale()); + con.getServerTimeZone(), utcDaysIntoCE, utcNanosSinceMidnight, typeInfo.getScale()); } private int readDaysIntoCE() throws SQLServerException { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java index 0b72c58f9..41d012b65 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java @@ -257,8 +257,7 @@ public byte[] encryptColumnEncryptionKey(String masterKeyPath, String encryption System.arraycopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.length); javaKeyStoreLogger.exiting(SQLServerColumnEncryptionJavaKeyStoreProvider.class.getName(), - "encryptColumnEncryptionKey", - "Finished encrypting Column Encryption Key."); + "encryptColumnEncryptionKey", "Finished encrypting Column Encryption Key."); return encryptedColumnEncryptionKey; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 77b4f1c18..de95fca0d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -597,6 +597,7 @@ boolean getServerSupportsColumnEncryption() { TimeZone getServerTimeZone() { return serverTimeZone; + } private boolean serverSupportsDataClassification = false; @@ -1845,7 +1846,7 @@ else if (0 == requestedPacketSize) } else { activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString()); } - + sPropKey = SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null != sPropValue) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 90a30fdfc..7806c432c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -680,7 +680,7 @@ public String getTrustManagerConstructorArg() { SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.toString(), SQLServerDriverStringProperty.TRUST_MANAGER_CONSTRUCTOR_ARG.getDefaultValue()); } - + public void setServerTimeZone(String serverTimeZone) { setStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_TIME_ZONE.toString(), serverTimeZone); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 07cf277e9..fc68aabf5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -252,6 +252,7 @@ public String toString() { } } + enum SQLServerDriverStringProperty { APPLICATION_INTENT("applicationIntent", ApplicationIntent.READ_WRITE.toString()), APPLICATION_NAME("applicationName", SQLServerDriver.DEFAULT_APP_NAME), @@ -265,7 +266,7 @@ enum SQLServerDriverStringProperty { SELECT_METHOD("selectMethod", "direct"), SERVER_NAME("serverName", ""), SERVER_SPN("serverSpn", ""), - SERVER_TIME_ZONE("serverTimeZone", ""), + SERVER_TIME_ZONE("serverTimeZone", ""), TRUST_STORE_TYPE("trustStoreType", "JKS"), TRUST_STORE("trustStore", ""), TRUST_STORE_PASSWORD("trustStorePassword", ""), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index ca5530310..0e0e0dd04 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -534,5 +534,6 @@ protected Object[][] getContents() { {"R_illegalWKT", "Illegal Well-Known text. Please make sure Well-Known text is valid."}, {"R_illegalTypeForGeometry", "{0} is not supported for Geometry."}, {"R_illegalWKTposition", "Illegal character in Well-Known text at position {0}."}, - {"R_serverTimeZonePropertyDescription", "The default time zone to use to convert date and time values. The default is the VM default time zone."},}; + {"R_serverTimeZonePropertyDescription", + "The default time zone to use to convert date and time values. The default is the VM default time zone."},}; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index 2899b0186..612c44ab3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -2381,16 +2381,15 @@ public T getObject(int columnIndex, Class type) throws SQLException { returnValue = getTime(columnIndex); } else if (type == java.sql.Timestamp.class) { returnValue = getTimestamp(columnIndex); - } else if (type == java.time.LocalDateTime.class - || type == java.time.LocalDate.class + } else if (type == java.time.LocalDateTime.class || type == java.time.LocalDate.class || type == java.time.LocalTime.class) { java.sql.Timestamp ts = getTimestamp(columnIndex, Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))); if (ts == null) { returnValue = null; } else { - java.time.LocalDateTime ldt = java.time.LocalDateTime - .ofInstant(ts.toInstant(), java.time.ZoneId.of("UTC")); + java.time.LocalDateTime ldt = java.time.LocalDateTime.ofInstant(ts.toInstant(), + java.time.ZoneId.of("UTC")); if (type == java.time.LocalDateTime.class) { returnValue = ldt; } else if (type == java.time.LocalDate.class) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java index 6959dec93..672982ce9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java @@ -175,9 +175,9 @@ protected void serializeToWkb(boolean noZM, SQLServerSpatialDatatype type) { * multiple corresponding data structures. * * @param type - * Type of Spatial Datatype (Geography/Geometry) + * Type of Spatial Datatype (Geography/Geometry) * @throws SQLServerException - * if an Exception occurs + * if an Exception occurs */ protected void parseWkb(SQLServerSpatialDatatype type) throws SQLServerException { srid = readInt(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index f3501be4b..4b7378f58 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -197,14 +197,10 @@ static BigDecimal readBigDecimal(byte valueBytes[], int valueLength, int scale) * @return long value as read from bytes. */ /* L0 */static long readLong(byte data[], int nOffset) { - return ((long) (data[nOffset + 7] & 0xff) << 56) - | ((long) (data[nOffset + 6] & 0xff) << 48) - | ((long) (data[nOffset + 5] & 0xff) << 40) - | ((long) (data[nOffset + 4] & 0xff) << 32) - | ((long) (data[nOffset + 3] & 0xff) << 24) - | ((long) (data[nOffset + 2] & 0xff) << 16) - | ((long) (data[nOffset + 1] & 0xff) << 8) - | ((long) (data[nOffset] & 0xff)); + return ((long) (data[nOffset + 7] & 0xff) << 56) | ((long) (data[nOffset + 6] & 0xff) << 48) + | ((long) (data[nOffset + 5] & 0xff) << 40) | ((long) (data[nOffset + 4] & 0xff) << 32) + | ((long) (data[nOffset + 3] & 0xff) << 24) | ((long) (data[nOffset + 2] & 0xff) << 16) + | ((long) (data[nOffset + 1] & 0xff) << 8) | ((long) (data[nOffset] & 0xff)); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index dc8fc8c7b..fd4b24aba 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -446,7 +446,7 @@ private void sendTemporal(DTV dtv, JavaType javaType, Object value) throws SQLSe */ if (null != value) { TimeZone timeZone = conn.getServerTimeZone(); // Time zone to associate with the value in the Gregorian - // calendar + // calendar long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT) // Figure out the value components according to the type of the Java object passed in... @@ -3546,7 +3546,8 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // cannot reuse method int daysIntoCE = getDaysIntoCE(decryptedValue, baseSSType); - return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, con.getServerTimeZone(), daysIntoCE, 0, 0); + return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, con.getServerTimeZone(), daysIntoCE, 0, + 0); } @@ -3554,7 +3555,7 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base long localNanosSinceMidnight = readNanosSinceMidnightAE(decryptedValue, baseTypeInfo.getScale(), baseSSType); - return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, con.getServerTimeZone(), 0, + return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, con.getServerTimeZone(), 0, localNanosSinceMidnight, baseTypeInfo.getScale()); } @@ -3577,7 +3578,7 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base int daysIntoCE = getDaysIntoCE(datePortion, baseSSType); // Convert the DATETIME2 value to the desired Java type. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, con.getServerTimeZone(), daysIntoCE, + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, con.getServerTimeZone(), daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale()); } @@ -3591,7 +3592,7 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // SQL smalldatetime has less precision. It stores 2 bytes // for the days since SQL Base Date and 2 bytes for minutes // after midnight. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), Util.readUnsignedShort(decryptedValue, 0), Util.readUnsignedShort(decryptedValue, 2) * 60L * 1000L, 0); } @@ -3605,8 +3606,8 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base // SQL datetime is 4 bytes for days since SQL Base Date // (January 1, 1900 00:00:00 GMT) and 4 bytes for // the number of three hundredths (1/300) of a second since midnight. - return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), Util.readInt(decryptedValue, 0), - (Util.readInt(decryptedValue, 4) * 10 + 1) / 3, 0); + return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, con.getServerTimeZone(), + Util.readInt(decryptedValue, 0), (Util.readInt(decryptedValue, 4) * 10 + 1) / 3, 0); } case DATETIMEOFFSET: { @@ -3627,8 +3628,7 @@ Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo base return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET, new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US), - con.getServerTimeZone(), daysIntoCE, localNanosSinceMidnight, - baseTypeInfo.getScale()); + con.getServerTimeZone(), daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale()); } diff --git a/src/main/java/mssql/googlecode/cityhash/CityHash.java b/src/main/java/mssql/googlecode/cityhash/CityHash.java index 2585916e4..2f11b9b29 100644 --- a/src/main/java/mssql/googlecode/cityhash/CityHash.java +++ b/src/main/java/mssql/googlecode/cityhash/CityHash.java @@ -3,14 +3,14 @@ /** * Copyright (C) 2012 tamtam180 * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a - * copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. * * @author tamtam180 - kirscheless at gmail.com * @see >> shift) | (val << (64 - shift)); } - private static long rotateByAtLeast1(long val, - int shift) { + private static long rotateByAtLeast1(long val, int shift) { return (val >>> shift) | (val << (64 - shift)); } @@ -61,8 +57,7 @@ private static long shiftMix(long val) { private static final long kMul = 0x9ddfea08eb382d69L; - private static long hash128to64(long u, - long v) { + private static long hash128to64(long u, long v) { long a = (u ^ v) * kMul; a ^= (a >>> 47); long b = (v ^ a) * kMul; @@ -71,14 +66,11 @@ private static long hash128to64(long u, return b; } - private static long hashLen16(long u, - long v) { + private static long hashLen16(long u, long v) { return hash128to64(u, v); } - private static long hashLen0to16(byte[] s, - int pos, - int len) { + private static long hashLen0to16(byte[] s, int pos, int len) { if (len > 8) { long a = fetch64(s, pos + 0); long b = fetch64(s, pos + len - 8); @@ -99,9 +91,7 @@ private static long hashLen0to16(byte[] s, return k2; } - private static long hashLen17to32(byte[] s, - int pos, - int len) { + private static long hashLen17to32(byte[] s, int pos, int len) { long a = fetch64(s, pos + 0) * k1; long b = fetch64(s, pos + 8); long c = fetch64(s, pos + len - 8) * k2; @@ -109,12 +99,7 @@ private static long hashLen17to32(byte[] s, return hashLen16(rotate(a - b, 43) + rotate(c, 30) + d, a + rotate(b ^ k3, 20) - c + len); } - private static long[] weakHashLen32WithSeeds(long w, - long x, - long y, - long z, - long a, - long b) { + private static long[] weakHashLen32WithSeeds(long w, long x, long y, long z, long a, long b) { a += w; b = rotate(b + a + z, 21); @@ -125,16 +110,12 @@ private static long[] weakHashLen32WithSeeds(long w, return new long[] {a + z, b + c}; } - private static long[] weakHashLen32WithSeeds(byte[] s, - int pos, - long a, - long b) { - return weakHashLen32WithSeeds(fetch64(s, pos + 0), fetch64(s, pos + 8), fetch64(s, pos + 16), fetch64(s, pos + 24), a, b); + private static long[] weakHashLen32WithSeeds(byte[] s, int pos, long a, long b) { + return weakHashLen32WithSeeds(fetch64(s, pos + 0), fetch64(s, pos + 8), fetch64(s, pos + 16), + fetch64(s, pos + 24), a, b); } - private static long hashLen33to64(byte[] s, - int pos, - int len) { + private static long hashLen33to64(byte[] s, int pos, int len) { long z = fetch64(s, pos + 24); long a = fetch64(s, pos + 0) + (fetch64(s, pos + len - 16) + len) * k0; @@ -164,19 +145,15 @@ private static long hashLen33to64(byte[] s, } - static long cityHash64(byte[] s, - int pos, - int len) { + static long cityHash64(byte[] s, int pos, int len) { if (len <= 32) { if (len <= 16) { return hashLen0to16(s, pos, len); - } - else { + } else { return hashLen17to32(s, pos, len); } - } - else if (len <= 64) { + } else if (len <= 64) { return hashLen33to64(s, pos, len); } @@ -204,33 +181,21 @@ else if (len <= 64) { } pos += 64; len -= 64; - } - while (len != 0); + } while (len != 0); return hashLen16(hashLen16(v[0], w[0]) + shiftMix(y) * k1 + z, hashLen16(v[1], w[1]) + x); } - static long cityHash64WithSeed(byte[] s, - int pos, - int len, - long seed) { + static long cityHash64WithSeed(byte[] s, int pos, int len, long seed) { return cityHash64WithSeeds(s, pos, len, k2, seed); } - static long cityHash64WithSeeds(byte[] s, - int pos, - int len, - long seed0, - long seed1) { + static long cityHash64WithSeeds(byte[] s, int pos, int len, long seed0, long seed1) { return hashLen16(cityHash64(s, pos, len) - seed0, seed1); } - static long[] cityMurmur(byte[] s, - int pos, - int len, - long seed0, - long seed1) { + static long[] cityMurmur(byte[] s, int pos, int len, long seed0, long seed1) { long a = seed0; long b = seed1; @@ -242,8 +207,7 @@ static long[] cityMurmur(byte[] s, a = shiftMix(a * k1) * k1; c = b * k1 + hashLen0to16(s, pos, len); d = shiftMix(a + (len >= 8 ? fetch64(s, pos + 0) : c)); - } - else { + } else { c = hashLen16(fetch64(s, pos + len - 8) + k1, a); d = hashLen16(b + len, c + fetch64(s, pos + len - 16)); @@ -258,8 +222,7 @@ static long[] cityMurmur(byte[] s, d ^= c; pos += 16; l -= 16; - } - while (l > 0); + } while (l > 0); } a = hashLen16(a, c); @@ -269,11 +232,7 @@ static long[] cityMurmur(byte[] s, } - static long[] cityHash128WithSeed(byte[] s, - int pos, - int len, - long seed0, - long seed1) { + static long[] cityHash128WithSeed(byte[] s, int pos, int len, long seed0, long seed1) { if (len < 128) { return cityMurmur(s, pos, len, seed0, seed1); @@ -318,8 +277,7 @@ static long[] cityHash128WithSeed(byte[] s, } pos += 64; len -= 128; - } - while (len >= 128); + } while (len >= 128); x += rotate(v[0] + z, 49) * k0; z += rotate(w[0], 37) * k0; @@ -341,17 +299,14 @@ static long[] cityHash128WithSeed(byte[] s, } - public static long[] cityHash128(byte[] s, - int pos, - int len) { + public static long[] cityHash128(byte[] s, int pos, int len) { if (len >= 16) { return cityHash128WithSeed(s, pos + 16, len - 16, fetch64(s, pos + 0) ^ k3, fetch64(s, pos + 8)); - } - else if (len >= 8) { - return cityHash128WithSeed(new byte[0], 0, 0, fetch64(s, pos + 0) ^ (len * k0), fetch64(s, pos + len - 8) ^ k1); - } - else { + } else if (len >= 8) { + return cityHash128WithSeed(new byte[0], 0, 0, fetch64(s, pos + 0) ^ (len * k0), + fetch64(s, pos + len - 8) ^ k1); + } else { return cityHash128WithSeed(s, pos, len, k0, k1); } } diff --git a/src/main/java/mssql/googlecode/cityhash/package-info.java b/src/main/java/mssql/googlecode/cityhash/package-info.java index 5a4d5748e..2473d37f5 100644 --- a/src/main/java/mssql/googlecode/cityhash/package-info.java +++ b/src/main/java/mssql/googlecode/cityhash/package-info.java @@ -1,14 +1,14 @@ /** * Copyright (C) 2012 tamtam180 * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a - * copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. * * @author tamtam180 - kirscheless at gmail.com * @see Java Collections Framework, this - * map does not have a publicly visible constructor and instances are created - * through a {@link Builder}. + * A hash table supporting full concurrency of retrievals, adjustable expected concurrency for updates, and a maximum + * capacity to bound the map by. This implementation differs from {@link ConcurrentHashMap} in that it maintains a page + * replacement algorithm that is used to evict an entry when the map has exceeded its capacity. Unlike the + * Java Collections Framework, this map does not have a publicly visible constructor and instances are + * created through a {@link Builder}. *

- * An entry is evicted from the map when the weighted capacity exceeds - * its maximum weighted capacity threshold. A {@link EntryWeigher} - * determines how many units of capacity that an entry consumes. The default - * weigher assigns each value a weight of 1 to bound the map by the - * total number of key-value pairs. A map that holds collections may choose to - * weigh values by the number of elements in the collection and bound the map - * by the total number of elements that it contains. A change to a value that - * modifies its weight requires that an update operation is performed on the - * map. + * An entry is evicted from the map when the weighted capacity exceeds its + * maximum weighted capacity threshold. A {@link EntryWeigher} determines how many units of capacity that + * an entry consumes. The default weigher assigns each value a weight of 1 to bound the map by the total + * number of key-value pairs. A map that holds collections may choose to weigh values by the number of elements in the + * collection and bound the map by the total number of elements that it contains. A change to a value that modifies its + * weight requires that an update operation is performed on the map. *

- * An {@link EvictionListener} may be supplied for notification when an entry - * is evicted from the map. This listener is invoked on a caller's thread and - * will not block other threads from operating on the map. An implementation - * should be aware that the caller's thread will not expect long execution - * times or failures as a side effect of the listener being notified. Execution - * safety and a fast turn around time can be achieved by performing the - * operation asynchronously, such as by submitting a task to an - * {@link java.util.concurrent.ExecutorService}. + * An {@link EvictionListener} may be supplied for notification when an entry is evicted from the map. This listener is + * invoked on a caller's thread and will not block other threads from operating on the map. An implementation should be + * aware that the caller's thread will not expect long execution times or failures as a side effect of the listener + * being notified. Execution safety and a fast turn around time can be achieved by performing the operation + * asynchronously, such as by submitting a task to an {@link java.util.concurrent.ExecutorService}. *

- * The concurrency level determines the number of threads that can - * concurrently modify the table. Using a significantly higher or lower value - * than needed can waste space or lead to thread contention, but an estimate - * within an order of magnitude of the ideal value does not usually have a - * noticeable impact. Because placement in hash tables is essentially random, - * the actual concurrency will vary. + * The concurrency level determines the number of threads that can concurrently modify the table. Using a + * significantly higher or lower value than needed can waste space or lead to thread contention, but an estimate within + * an order of magnitude of the ideal value does not usually have a noticeable impact. Because placement in hash tables + * is essentially random, the actual concurrency will vary. *

- * This class and its views and iterators implement all of the - * optional methods of the {@link Map} and {@link Iterator} - * interfaces. + * This class and its views and iterators implement all of the optional methods of the {@link Map} and + * {@link Iterator} interfaces. *

- * Like {@link java.util.Hashtable} but unlike {@link HashMap}, this class - * does not allow null to be used as a key or value. Unlike - * {@link java.util.LinkedHashMap}, this class does not provide - * predictable iteration order. A snapshot of the keys and entries may be - * obtained in ascending and descending order of retention. + * Like {@link java.util.Hashtable} but unlike {@link HashMap}, this class does not allow null to + * be used as a key or value. Unlike {@link java.util.LinkedHashMap}, this class does not provide predictable + * iteration order. A snapshot of the keys and entries may be obtained in ascending and descending order of retention. * * @author ben.manes@gmail.com (Ben Manes) - * @param the type of keys maintained by this map - * @param the type of mapped values + * @param + * the type of keys maintained by this map + * @param + * the type of mapped values * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ public final class ConcurrentLinkedHashMap extends AbstractMap - implements ConcurrentMap, Serializable { - - /* - * This class performs a best-effort bounding of a ConcurrentHashMap using a - * page-replacement algorithm to determine which entries to evict when the - * capacity is exceeded. - * - * The page replacement algorithm's data structures are kept eventually - * consistent with the map. An update to the map and recording of reads may - * not be immediately reflected on the algorithm's data structures. These - * structures are guarded by a lock and operations are applied in batches to - * avoid lock contention. The penalty of applying the batches is spread across - * threads so that the amortized cost is slightly higher than performing just - * the ConcurrentHashMap operation. - * - * A memento of the reads and writes that were performed on the map are - * recorded in buffers. These buffers are drained at the first opportunity - * after a write or when the read buffer exceeds a threshold size. The reads - * are recorded in a lossy buffer, allowing the reordering operations to be - * discarded if the draining process cannot keep up. Due to the concurrent - * nature of the read and write operations a strict policy ordering is not - * possible, but is observably strict when single threaded. - * - * Due to a lack of a strict ordering guarantee, a task can be executed - * out-of-order, such as a removal followed by its addition. The state of the - * entry is encoded within the value's weight. - * - * Alive: The entry is in both the hash-table and the page replacement policy. - * This is represented by a positive weight. - * - * Retired: The entry is not in the hash-table and is pending removal from the - * page replacement policy. This is represented by a negative weight. - * - * Dead: The entry is not in the hash-table and is not in the page replacement - * policy. This is represented by a weight of zero. - * - * The Least Recently Used page replacement algorithm was chosen due to its - * simplicity, high hit rate, and ability to be implemented with O(1) time - * complexity. - */ - - /** The number of CPUs */ - static final int NCPU = Runtime.getRuntime().availableProcessors(); - - /** The maximum weighted capacity of the map. */ - static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; - - /** The number of read buffers to use. */ - static final int NUMBER_OF_READ_BUFFERS = ceilingNextPowerOfTwo(NCPU); - - /** Mask value for indexing into the read buffers. */ - static final int READ_BUFFERS_MASK = NUMBER_OF_READ_BUFFERS - 1; - - /** The number of pending read operations before attempting to drain. */ - static final int READ_BUFFER_THRESHOLD = 32; - - /** The maximum number of read operations to perform per amortized drain. */ - static final int READ_BUFFER_DRAIN_THRESHOLD = 2 * READ_BUFFER_THRESHOLD; - - /** The maximum number of pending reads per buffer. */ - static final int READ_BUFFER_SIZE = 2 * READ_BUFFER_DRAIN_THRESHOLD; - - /** Mask value for indexing into the read buffer. */ - static final int READ_BUFFER_INDEX_MASK = READ_BUFFER_SIZE - 1; - - /** The maximum number of write operations to perform per amortized drain. */ - static final int WRITE_BUFFER_DRAIN_THRESHOLD = 16; - - /** A queue that discards all entries. */ - static final Queue DISCARDING_QUEUE = new DiscardingQueue(); - - static int ceilingNextPowerOfTwo(int x) { - // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. - return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); - } - - // The backing data store holding the key-value associations - final ConcurrentMap> data; - final int concurrencyLevel; - - // These fields provide support to bound the map by a maximum capacity - final long[] readBufferReadCount; - final LinkedDeque> evictionDeque; - - final AtomicLong weightedSize; - final AtomicLong capacity; - - final Lock evictionLock; - final Queue writeBuffer; - final AtomicLong[] readBufferWriteCount; - final AtomicLong[] readBufferDrainAtWriteCount; - final AtomicReference>[][] readBuffers; - - final AtomicReference drainStatus; - final EntryWeigher weigher; - - // These fields provide support for notifying a listener. - final Queue> pendingNotifications; - final EvictionListener listener; - - transient Set keySet; - transient Collection values; - transient Set> entrySet; - - /** - * Creates an instance based on the builder's configuration. - */ - @SuppressWarnings({"unchecked", "cast"}) - private ConcurrentLinkedHashMap(Builder builder) { - // The data store and its maximum capacity - concurrencyLevel = builder.concurrencyLevel; - capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); - data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); - - // The eviction support - weigher = builder.weigher; - evictionLock = new ReentrantLock(); - weightedSize = new AtomicLong(); - evictionDeque = new LinkedDeque>(); - writeBuffer = new ConcurrentLinkedQueue(); - drainStatus = new AtomicReference(IDLE); - - readBufferReadCount = new long[NUMBER_OF_READ_BUFFERS]; - readBufferWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; - readBufferDrainAtWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; - readBuffers = new AtomicReference[NUMBER_OF_READ_BUFFERS][READ_BUFFER_SIZE]; - for (int i = 0; i < NUMBER_OF_READ_BUFFERS; i++) { - readBufferWriteCount[i] = new AtomicLong(); - readBufferDrainAtWriteCount[i] = new AtomicLong(); - readBuffers[i] = new AtomicReference[READ_BUFFER_SIZE]; - for (int j = 0; j < READ_BUFFER_SIZE; j++) { - readBuffers[i][j] = new AtomicReference>(); - } - } - - // The notification queue and listener - listener = builder.listener; - pendingNotifications = (listener == DiscardingListener.INSTANCE) - ? (Queue>) DISCARDING_QUEUE - : new ConcurrentLinkedQueue>(); - } - - /** Ensures that the object is not null. */ - static void checkNotNull(Object o) { - if (o == null) { - throw new NullPointerException(); - } - } - - /** Ensures that the argument expression is true. */ - static void checkArgument(boolean expression) { - if (!expression) { - throw new IllegalArgumentException(); - } - } - - /** Ensures that the state expression is true. */ - static void checkState(boolean expression) { - if (!expression) { - throw new IllegalStateException(); - } - } - - /* ---------------- Eviction Support -------------- */ - - /** - * Retrieves the maximum weighted capacity of the map. - * - * @return the maximum weighted capacity - */ - public long capacity() { - return capacity.get(); - } - - /** - * Sets the maximum weighted capacity of the map and eagerly evicts entries - * until it shrinks to the appropriate size. - * - * @param capacity the maximum weighted capacity of the map - * @throws IllegalArgumentException if the capacity is negative - */ - public void setCapacity(long capacity) { - checkArgument(capacity >= 0); - evictionLock.lock(); - try { - this.capacity.lazySet(Math.min(capacity, MAXIMUM_CAPACITY)); - drainBuffers(); - evict(); - } finally { - evictionLock.unlock(); - } - notifyListener(); - } - - /** Determines whether the map has exceeded its capacity. */ - boolean hasOverflowed() { - return weightedSize.get() > capacity.get(); - } - - /** - * Evicts entries from the map while it exceeds the capacity and appends - * evicted entries to the notification queue for processing. - */ - void evict() { - // Attempts to evict entries from the map if it exceeds the maximum - // capacity. If the eviction fails due to a concurrent removal of the - // victim, that removal may cancel out the addition that triggered this - // eviction. The victim is eagerly unlinked before the removal task so - // that if an eviction is still required then a new victim will be chosen - // for removal. - while (hasOverflowed()) { - final Node node = evictionDeque.poll(); - - // If weighted values are used, then the pending operations will adjust - // the size to reflect the correct weight - if (node == null) { - return; - } - - // Notify the listener only if the entry was evicted - if (data.remove(node.key, node)) { - pendingNotifications.add(node); - } - - makeDead(node); - } - } - - /** - * Performs the post-processing work required after a read. - * - * @param node the entry in the page replacement policy - */ - void afterRead(Node node) { - final int bufferIndex = readBufferIndex(); - final long writeCount = recordRead(bufferIndex, node); - drainOnReadIfNeeded(bufferIndex, writeCount); - notifyListener(); - } - - /** Returns the index to the read buffer to record into. */ - static int readBufferIndex() { - // A buffer is chosen by the thread's id so that tasks are distributed in a - // pseudo evenly manner. This helps avoid hot entries causing contention - // due to other threads trying to append to the same buffer. - return ((int) Thread.currentThread().getId()) & READ_BUFFERS_MASK; - } - - /** - * Records a read in the buffer and return its write count. - * - * @param bufferIndex the index to the chosen read buffer - * @param node the entry in the page replacement policy - * @return the number of writes on the chosen read buffer - */ - long recordRead(int bufferIndex, Node node) { - // The location in the buffer is chosen in a racy fashion as the increment - // is not atomic with the insertion. This means that concurrent reads can - // overlap and overwrite one another, resulting in a lossy buffer. - final AtomicLong counter = readBufferWriteCount[bufferIndex]; - final long writeCount = counter.get(); - counter.lazySet(writeCount + 1); - - final int index = (int) (writeCount & READ_BUFFER_INDEX_MASK); - readBuffers[bufferIndex][index].lazySet(node); - - return writeCount; - } - - /** - * Attempts to drain the buffers if it is determined to be needed when - * post-processing a read. - * - * @param bufferIndex the index to the chosen read buffer - * @param writeCount the number of writes on the chosen read buffer - */ - void drainOnReadIfNeeded(int bufferIndex, long writeCount) { - final long pending = (writeCount - readBufferDrainAtWriteCount[bufferIndex].get()); - final boolean delayable = (pending < READ_BUFFER_THRESHOLD); - final DrainStatus status = drainStatus.get(); - if (status.shouldDrainBuffers(delayable)) { - tryToDrainBuffers(); - } - } - - /** - * Performs the post-processing work required after a write. - * - * @param task the pending operation to be applied - */ - void afterWrite(Runnable task) { - writeBuffer.add(task); - drainStatus.lazySet(REQUIRED); - tryToDrainBuffers(); - notifyListener(); - } - - /** - * Attempts to acquire the eviction lock and apply the pending operations, up - * to the amortized threshold, to the page replacement policy. - */ - void tryToDrainBuffers() { - if (evictionLock.tryLock()) { - try { - drainStatus.lazySet(PROCESSING); - drainBuffers(); - } finally { - drainStatus.compareAndSet(PROCESSING, IDLE); - evictionLock.unlock(); - } - } - } - - /** Drains the read and write buffers up to an amortized threshold. */ - void drainBuffers() { - drainReadBuffers(); - drainWriteBuffer(); - } - - /** Drains the read buffers, each up to an amortized threshold. */ - void drainReadBuffers() { - final int start = (int) Thread.currentThread().getId(); - final int end = start + NUMBER_OF_READ_BUFFERS; - for (int i = start; i < end; i++) { - drainReadBuffer(i & READ_BUFFERS_MASK); - } - } - - /** Drains the read buffer up to an amortized threshold. */ - void drainReadBuffer(int bufferIndex) { - final long writeCount = readBufferWriteCount[bufferIndex].get(); - for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { - final int index = (int) (readBufferReadCount[bufferIndex] & READ_BUFFER_INDEX_MASK); - final AtomicReference> slot = readBuffers[bufferIndex][index]; - final Node node = slot.get(); - if (node == null) { - break; - } - - slot.lazySet(null); - applyRead(node); - readBufferReadCount[bufferIndex]++; - } - readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); - } - - /** Updates the node's location in the page replacement policy. */ - void applyRead(Node node) { - // An entry may be scheduled for reordering despite having been removed. - // This can occur when the entry was concurrently read while a writer was - // removing it. If the entry is no longer linked then it does not need to - // be processed. - if (evictionDeque.contains(node)) { - evictionDeque.moveToBack(node); - } - } - - /** Drains the read buffer up to an amortized threshold. */ - void drainWriteBuffer() { - for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { - final Runnable task = writeBuffer.poll(); - if (task == null) { - break; - } - task.run(); - } - } - - /** - * Attempts to transition the node from the alive state to the - * retired state. - * - * @param node the entry in the page replacement policy - * @param expect the expected weighted value - * @return if successful - */ - boolean tryToRetire(Node node, WeightedValue expect) { - if (expect.isAlive()) { - final WeightedValue retired = new WeightedValue(expect.value, -expect.weight); - return node.compareAndSet(expect, retired); - } - return false; - } - - /** - * Atomically transitions the node from the alive state to the - * retired state, if a valid transition. - * - * @param node the entry in the page replacement policy - */ - void makeRetired(Node node) { - for (;;) { - final WeightedValue current = node.get(); - if (!current.isAlive()) { - return; - } - final WeightedValue retired = new WeightedValue(current.value, -current.weight); - if (node.compareAndSet(current, retired)) { - return; - } - } - } - - /** - * Atomically transitions the node to the dead state and decrements - * the weightedSize. - * - * @param node the entry in the page replacement policy - */ - void makeDead(Node node) { - for (;;) { - WeightedValue current = node.get(); - WeightedValue dead = new WeightedValue(current.value, 0); - if (node.compareAndSet(current, dead)) { - weightedSize.lazySet(weightedSize.get() - Math.abs(current.weight)); - return; - } - } - } - - /** Notifies the listener of entries that were evicted. */ - void notifyListener() { - Node node; - while ((node = pendingNotifications.poll()) != null) { - listener.onEviction(node.key, node.getValue()); - } - } - - /** Adds the node to the page replacement policy. */ - final class AddTask implements Runnable { - final Node node; - final int weight; - - AddTask(Node node, int weight) { - this.weight = weight; - this.node = node; - } + implements ConcurrentMap, Serializable { + + /* + * This class performs a best-effort bounding of a ConcurrentHashMap using a page-replacement algorithm to determine + * which entries to evict when the capacity is exceeded. The page replacement algorithm's data structures are kept + * eventually consistent with the map. An update to the map and recording of reads may not be immediately reflected + * on the algorithm's data structures. These structures are guarded by a lock and operations are applied in batches + * to avoid lock contention. The penalty of applying the batches is spread across threads so that the amortized cost + * is slightly higher than performing just the ConcurrentHashMap operation. A memento of the reads and writes that + * were performed on the map are recorded in buffers. These buffers are drained at the first opportunity after a + * write or when the read buffer exceeds a threshold size. The reads are recorded in a lossy buffer, allowing the + * reordering operations to be discarded if the draining process cannot keep up. Due to the concurrent nature of the + * read and write operations a strict policy ordering is not possible, but is observably strict when single + * threaded. Due to a lack of a strict ordering guarantee, a task can be executed out-of-order, such as a removal + * followed by its addition. The state of the entry is encoded within the value's weight. Alive: The entry is in + * both the hash-table and the page replacement policy. This is represented by a positive weight. Retired: The entry + * is not in the hash-table and is pending removal from the page replacement policy. This is represented by a + * negative weight. Dead: The entry is not in the hash-table and is not in the page replacement policy. This is + * represented by a weight of zero. The Least Recently Used page replacement algorithm was chosen due to its + * simplicity, high hit rate, and ability to be implemented with O(1) time complexity. + */ - @Override - public void run() { - weightedSize.lazySet(weightedSize.get() + weight); + /** The number of CPUs */ + static final int NCPU = Runtime.getRuntime().availableProcessors(); + + /** The maximum weighted capacity of the map. */ + static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; + + /** The number of read buffers to use. */ + static final int NUMBER_OF_READ_BUFFERS = ceilingNextPowerOfTwo(NCPU); + + /** Mask value for indexing into the read buffers. */ + static final int READ_BUFFERS_MASK = NUMBER_OF_READ_BUFFERS - 1; + + /** The number of pending read operations before attempting to drain. */ + static final int READ_BUFFER_THRESHOLD = 32; - // ignore out-of-order write operations - if (node.get().isAlive()) { - evictionDeque.add(node); - evict(); - } + /** The maximum number of read operations to perform per amortized drain. */ + static final int READ_BUFFER_DRAIN_THRESHOLD = 2 * READ_BUFFER_THRESHOLD; + + /** The maximum number of pending reads per buffer. */ + static final int READ_BUFFER_SIZE = 2 * READ_BUFFER_DRAIN_THRESHOLD; + + /** Mask value for indexing into the read buffer. */ + static final int READ_BUFFER_INDEX_MASK = READ_BUFFER_SIZE - 1; + + /** The maximum number of write operations to perform per amortized drain. */ + static final int WRITE_BUFFER_DRAIN_THRESHOLD = 16; + + /** A queue that discards all entries. */ + static final Queue DISCARDING_QUEUE = new DiscardingQueue(); + + static int ceilingNextPowerOfTwo(int x) { + // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. + return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); } - } - /** Removes a node from the page replacement policy. */ - final class RemovalTask implements Runnable { - final Node node; + // The backing data store holding the key-value associations + final ConcurrentMap> data; + final int concurrencyLevel; + + // These fields provide support to bound the map by a maximum capacity + final long[] readBufferReadCount; + final LinkedDeque> evictionDeque; + + final AtomicLong weightedSize; + final AtomicLong capacity; - RemovalTask(Node node) { - this.node = node; + final Lock evictionLock; + final Queue writeBuffer; + final AtomicLong[] readBufferWriteCount; + final AtomicLong[] readBufferDrainAtWriteCount; + final AtomicReference>[][] readBuffers; + + final AtomicReference drainStatus; + final EntryWeigher weigher; + + // These fields provide support for notifying a listener. + final Queue> pendingNotifications; + final EvictionListener listener; + + transient Set keySet; + transient Collection values; + transient Set> entrySet; + + /** + * Creates an instance based on the builder's configuration. + */ + @SuppressWarnings({"unchecked", "cast"}) + private ConcurrentLinkedHashMap(Builder builder) { + // The data store and its maximum capacity + concurrencyLevel = builder.concurrencyLevel; + capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); + data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel); + + // The eviction support + weigher = builder.weigher; + evictionLock = new ReentrantLock(); + weightedSize = new AtomicLong(); + evictionDeque = new LinkedDeque>(); + writeBuffer = new ConcurrentLinkedQueue(); + drainStatus = new AtomicReference(IDLE); + + readBufferReadCount = new long[NUMBER_OF_READ_BUFFERS]; + readBufferWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBufferDrainAtWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; + readBuffers = new AtomicReference[NUMBER_OF_READ_BUFFERS][READ_BUFFER_SIZE]; + for (int i = 0; i < NUMBER_OF_READ_BUFFERS; i++) { + readBufferWriteCount[i] = new AtomicLong(); + readBufferDrainAtWriteCount[i] = new AtomicLong(); + readBuffers[i] = new AtomicReference[READ_BUFFER_SIZE]; + for (int j = 0; j < READ_BUFFER_SIZE; j++) { + readBuffers[i][j] = new AtomicReference>(); + } + } + + // The notification queue and listener + listener = builder.listener; + pendingNotifications = (listener == DiscardingListener.INSTANCE) ? (Queue>) DISCARDING_QUEUE + : new ConcurrentLinkedQueue>(); } - @Override - public void run() { - // add may not have been processed yet - evictionDeque.remove(node); - makeDead(node); + /** Ensures that the object is not null. */ + static void checkNotNull(Object o) { + if (o == null) { + throw new NullPointerException(); + } } - } - /** Updates the weighted size and evicts an entry on overflow. */ - final class UpdateTask implements Runnable { - final int weightDifference; - final Node node; + /** Ensures that the argument expression is true. */ + static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } - public UpdateTask(Node node, int weightDifference) { - this.weightDifference = weightDifference; - this.node = node; + /** Ensures that the state expression is true. */ + static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } } - @Override - public void run() { - weightedSize.lazySet(weightedSize.get() + weightDifference); - applyRead(node); - evict(); - } - } - - /* ---------------- Concurrent Map Support -------------- */ - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public int size() { - return data.size(); - } - - /** - * Returns the weighted size of this map. - * - * @return the combined weight of the values in this map - */ - public long weightedSize() { - return Math.max(0, weightedSize.get()); - } - - @Override - public void clear() { - evictionLock.lock(); - try { - // Discard all entries - Node node; - while ((node = evictionDeque.poll()) != null) { - data.remove(node.key, node); - makeDead(node); - } - - // Discard all pending reads - for (AtomicReference>[] buffer : readBuffers) { - for (AtomicReference> slot : buffer) { - slot.lazySet(null); - } - } - - // Apply all pending writes - Runnable task; - while ((task = writeBuffer.poll()) != null) { - task.run(); - } - } finally { - evictionLock.unlock(); - } - } - - @Override - public boolean containsKey(Object key) { - return data.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - checkNotNull(value); - - for (Node node : data.values()) { - if (node.getValue().equals(value)) { - return true; - } - } - return false; - } - - @Override - public V get(Object key) { - final Node node = data.get(key); - if (node == null) { - return null; - } - afterRead(node); - return node.getValue(); - } - - /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. This method differs from - * {@link #get(Object)} in that it does not record the operation with the - * page replacement policy. - * - * @param key the key whose associated value is to be returned - * @return the value to which the specified key is mapped, or - * {@code null} if this map contains no mapping for the key - * @throws NullPointerException if the specified key is null - */ - public V getQuietly(Object key) { - final Node node = data.get(key); - return (node == null) ? null : node.getValue(); - } - - @Override - public V put(K key, V value) { - return put(key, value, false); - } - - @Override - public V putIfAbsent(K key, V value) { - return put(key, value, true); - } - - /** - * Adds a node to the list and the data store. If an existing node is found, - * then its value is updated if allowed. - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - * @param onlyIfAbsent a write is performed only if the key is not already - * associated with a value - * @return the prior value in the data store or null if no mapping was found - */ - V put(K key, V value, boolean onlyIfAbsent) { - checkNotNull(key); - checkNotNull(value); - - final int weight = weigher.weightOf(key, value); - final WeightedValue weightedValue = new WeightedValue(value, weight); - final Node node = new Node(key, weightedValue); - - for (;;) { - final Node prior = data.putIfAbsent(node.key, node); - if (prior == null) { - afterWrite(new AddTask(node, weight)); - return null; - } else if (onlyIfAbsent) { - afterRead(prior); - return prior.getValue(); - } - for (;;) { - final WeightedValue oldWeightedValue = prior.get(); - if (!oldWeightedValue.isAlive()) { - break; - } - - if (prior.compareAndSet(oldWeightedValue, weightedValue)) { - final int weightedDifference = weight - oldWeightedValue.weight; - if (weightedDifference == 0) { - afterRead(prior); - } else { - afterWrite(new UpdateTask(prior, weightedDifference)); - } - return oldWeightedValue.value; - } - } - } - } - - @Override - public V remove(Object key) { - final Node node = data.remove(key); - if (node == null) { - return null; - } - - makeRetired(node); - afterWrite(new RemovalTask(node)); - return node.getValue(); - } - - @Override - public boolean remove(Object key, Object value) { - final Node node = data.get(key); - if ((node == null) || (value == null)) { - return false; - } - - WeightedValue weightedValue = node.get(); - for (;;) { - if (weightedValue.contains(value)) { - if (tryToRetire(node, weightedValue)) { - if (data.remove(key, node)) { - afterWrite(new RemovalTask(node)); - return true; - } - } else { - weightedValue = node.get(); - if (weightedValue.isAlive()) { - // retry as an intermediate update may have replaced the value with - // an equal instance that has a different reference identity - continue; - } - } - } - return false; - } - } - - @Override - public V replace(K key, V value) { - checkNotNull(key); - checkNotNull(value); - - final int weight = weigher.weightOf(key, value); - final WeightedValue weightedValue = new WeightedValue(value, weight); - - final Node node = data.get(key); - if (node == null) { - return null; - } - for (;;) { - final WeightedValue oldWeightedValue = node.get(); - if (!oldWeightedValue.isAlive()) { - return null; - } - if (node.compareAndSet(oldWeightedValue, weightedValue)) { - final int weightedDifference = weight - oldWeightedValue.weight; - if (weightedDifference == 0) { - afterRead(node); - } else { - afterWrite(new UpdateTask(node, weightedDifference)); - } - return oldWeightedValue.value; - } - } - } - - @Override - public boolean replace(K key, V oldValue, V newValue) { - checkNotNull(key); - checkNotNull(oldValue); - checkNotNull(newValue); - - final int weight = weigher.weightOf(key, newValue); - final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); - - final Node node = data.get(key); - if (node == null) { - return false; - } - for (;;) { - final WeightedValue weightedValue = node.get(); - if (!weightedValue.isAlive() || !weightedValue.contains(oldValue)) { - return false; - } - if (node.compareAndSet(weightedValue, newWeightedValue)) { - final int weightedDifference = weight - weightedValue.weight; - if (weightedDifference == 0) { - afterRead(node); - } else { - afterWrite(new UpdateTask(node, weightedDifference)); - } - return true; - } - } - } - - @Override - public Set keySet() { - final Set ks = keySet; - return (ks == null) ? (keySet = new KeySet()) : ks; - } - - /** - * Returns a unmodifiable snapshot {@link Set} view of the keys contained in - * this map. The set's iterator returns the keys whose order of iteration is - * the ascending order in which its entries are considered eligible for - * retention, from the least-likely to be retained to the most-likely. - *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT - * a constant-time operation. Because of the asynchronous nature of the page - * replacement policy, determining the retention ordering requires a traversal - * of the keys. - * - * @return an ascending snapshot view of the keys in this map - */ - public Set ascendingKeySet() { - return ascendingKeySetWithLimit(Integer.MAX_VALUE); - } - - /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in - * this map. The set's iterator returns the keys whose order of iteration is - * the ascending order in which its entries are considered eligible for - * retention, from the least-likely to be retained to the most-likely. - *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT - * a constant-time operation. Because of the asynchronous nature of the page - * replacement policy, determining the retention ordering requires a traversal - * of the keys. - * - * @param limit the maximum size of the returned set - * @return a ascending snapshot view of the keys in this map - * @throws IllegalArgumentException if the limit is negative - */ - public Set ascendingKeySetWithLimit(int limit) { - return orderedKeySet(true, limit); - } - - /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in - * this map. The set's iterator returns the keys whose order of iteration is - * the descending order in which its entries are considered eligible for - * retention, from the most-likely to be retained to the least-likely. - *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT - * a constant-time operation. Because of the asynchronous nature of the page - * replacement policy, determining the retention ordering requires a traversal - * of the keys. - * - * @return a descending snapshot view of the keys in this map - */ - public Set descendingKeySet() { - return descendingKeySetWithLimit(Integer.MAX_VALUE); - } - - /** - * Returns an unmodifiable snapshot {@link Set} view of the keys contained in - * this map. The set's iterator returns the keys whose order of iteration is - * the descending order in which its entries are considered eligible for - * retention, from the most-likely to be retained to the least-likely. - *

- * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT - * a constant-time operation. Because of the asynchronous nature of the page - * replacement policy, determining the retention ordering requires a traversal - * of the keys. - * - * @param limit the maximum size of the returned set - * @return a descending snapshot view of the keys in this map - * @throws IllegalArgumentException if the limit is negative - */ - public Set descendingKeySetWithLimit(int limit) { - return orderedKeySet(false, limit); - } - - Set orderedKeySet(boolean ascending, int limit) { - checkArgument(limit >= 0); - evictionLock.lock(); - try { - drainBuffers(); - - final int initialCapacity = (weigher == Weighers.entrySingleton()) - ? Math.min(limit, (int) weightedSize()) - : 16; - final Set keys = new LinkedHashSet(initialCapacity); - final Iterator> iterator = ascending - ? evictionDeque.iterator() - : evictionDeque.descendingIterator(); - while (iterator.hasNext() && (limit > keys.size())) { - keys.add(iterator.next().key); - } - return unmodifiableSet(keys); - } finally { - evictionLock.unlock(); - } - } - - @Override - public Collection values() { - final Collection vs = values; - return (vs == null) ? (values = new Values()) : vs; - } - - @Override - public Set> entrySet() { - final Set> es = entrySet; - return (es == null) ? (entrySet = new EntrySet()) : es; - } - - /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained - * in this map. The map's collections return the mappings whose order of - * iteration is the ascending order in which its entries are considered - * eligible for retention, from the least-likely to be retained to the - * most-likely. - *

- * Beware that obtaining the mappings is NOT a constant-time - * operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the - * entries. - * - * @return a ascending snapshot view of this map - */ - public Map ascendingMap() { - return ascendingMapWithLimit(Integer.MAX_VALUE); - } - - /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained - * in this map. The map's collections return the mappings whose order of - * iteration is the ascending order in which its entries are considered - * eligible for retention, from the least-likely to be retained to the - * most-likely. - *

- * Beware that obtaining the mappings is NOT a constant-time - * operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the - * entries. - * - * @param limit the maximum size of the returned map - * @return a ascending snapshot view of this map - * @throws IllegalArgumentException if the limit is negative - */ - public Map ascendingMapWithLimit(int limit) { - return orderedMap(true, limit); - } - - /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained - * in this map. The map's collections return the mappings whose order of - * iteration is the descending order in which its entries are considered - * eligible for retention, from the most-likely to be retained to the - * least-likely. - *

- * Beware that obtaining the mappings is NOT a constant-time - * operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the - * entries. - * - * @return a descending snapshot view of this map - */ - public Map descendingMap() { - return descendingMapWithLimit(Integer.MAX_VALUE); - } - - /** - * Returns an unmodifiable snapshot {@link Map} view of the mappings contained - * in this map. The map's collections return the mappings whose order of - * iteration is the descending order in which its entries are considered - * eligible for retention, from the most-likely to be retained to the - * least-likely. - *

- * Beware that obtaining the mappings is NOT a constant-time - * operation. Because of the asynchronous nature of the page replacement - * policy, determining the retention ordering requires a traversal of the - * entries. - * - * @param limit the maximum size of the returned map - * @return a descending snapshot view of this map - * @throws IllegalArgumentException if the limit is negative - */ - public Map descendingMapWithLimit(int limit) { - return orderedMap(false, limit); - } - - Map orderedMap(boolean ascending, int limit) { - checkArgument(limit >= 0); - evictionLock.lock(); - try { - drainBuffers(); - - final int initialCapacity = (weigher == Weighers.entrySingleton()) - ? Math.min(limit, (int) weightedSize()) - : 16; - final Map map = new LinkedHashMap(initialCapacity); - final Iterator> iterator = ascending - ? evictionDeque.iterator() - : evictionDeque.descendingIterator(); - while (iterator.hasNext() && (limit > map.size())) { - Node node = iterator.next(); - map.put(node.key, node.getValue()); - } - return unmodifiableMap(map); - } finally { - evictionLock.unlock(); - } - } - - /** The draining status of the buffers. */ - enum DrainStatus { - - /** A drain is not taking place. */ - IDLE { - @Override boolean shouldDrainBuffers(boolean delayable) { - return !delayable; - } - }, - - /** A drain is required due to a pending write modification. */ - REQUIRED { - @Override boolean shouldDrainBuffers(boolean delayable) { - return true; - } - }, - - /** A drain is in progress. */ - PROCESSING { - @Override boolean shouldDrainBuffers(boolean delayable) { - return false; - } - }; + /* ---------------- Eviction Support -------------- */ /** - * Determines whether the buffers should be drained. + * Retrieves the maximum weighted capacity of the map. * - * @param delayable if a drain should be delayed until required - * @return if a drain should be attempted + * @return the maximum weighted capacity */ - abstract boolean shouldDrainBuffers(boolean delayable); - } - - /** A value, its weight, and the entry's status. */ - static final class WeightedValue { - final int weight; - final V value; + public long capacity() { + return capacity.get(); + } - WeightedValue(V value, int weight) { - this.weight = weight; - this.value = value; + /** + * Sets the maximum weighted capacity of the map and eagerly evicts entries until it shrinks to the appropriate + * size. + * + * @param capacity + * the maximum weighted capacity of the map + * @throws IllegalArgumentException + * if the capacity is negative + */ + public void setCapacity(long capacity) { + checkArgument(capacity >= 0); + evictionLock.lock(); + try { + this.capacity.lazySet(Math.min(capacity, MAXIMUM_CAPACITY)); + drainBuffers(); + evict(); + } finally { + evictionLock.unlock(); + } + notifyListener(); } - boolean contains(Object o) { - return (o == value) || value.equals(o); + /** Determines whether the map has exceeded its capacity. */ + boolean hasOverflowed() { + return weightedSize.get() > capacity.get(); } /** - * If the entry is available in the hash-table and page replacement policy. + * Evicts entries from the map while it exceeds the capacity and appends evicted entries to the notification queue + * for processing. */ - boolean isAlive() { - return weight > 0; + void evict() { + // Attempts to evict entries from the map if it exceeds the maximum + // capacity. If the eviction fails due to a concurrent removal of the + // victim, that removal may cancel out the addition that triggered this + // eviction. The victim is eagerly unlinked before the removal task so + // that if an eviction is still required then a new victim will be chosen + // for removal. + while (hasOverflowed()) { + final Node node = evictionDeque.poll(); + + // If weighted values are used, then the pending operations will adjust + // the size to reflect the correct weight + if (node == null) { + return; + } + + // Notify the listener only if the entry was evicted + if (data.remove(node.key, node)) { + pendingNotifications.add(node); + } + + makeDead(node); + } } /** - * If the entry was removed from the hash-table and is awaiting removal from - * the page replacement policy. + * Performs the post-processing work required after a read. + * + * @param node + * the entry in the page replacement policy */ - boolean isRetired() { - return weight < 0; + void afterRead(Node node) { + final int bufferIndex = readBufferIndex(); + final long writeCount = recordRead(bufferIndex, node); + drainOnReadIfNeeded(bufferIndex, writeCount); + notifyListener(); + } + + /** Returns the index to the read buffer to record into. */ + static int readBufferIndex() { + // A buffer is chosen by the thread's id so that tasks are distributed in a + // pseudo evenly manner. This helps avoid hot entries causing contention + // due to other threads trying to append to the same buffer. + return ((int) Thread.currentThread().getId()) & READ_BUFFERS_MASK; } /** - * If the entry was removed from the hash-table and the page replacement - * policy. + * Records a read in the buffer and return its write count. + * + * @param bufferIndex + * the index to the chosen read buffer + * @param node + * the entry in the page replacement policy + * @return the number of writes on the chosen read buffer */ - boolean isDead() { - return weight == 0; - } - } + long recordRead(int bufferIndex, Node node) { + // The location in the buffer is chosen in a racy fashion as the increment + // is not atomic with the insertion. This means that concurrent reads can + // overlap and overwrite one another, resulting in a lossy buffer. + final AtomicLong counter = readBufferWriteCount[bufferIndex]; + final long writeCount = counter.get(); + counter.lazySet(writeCount + 1); - /** - * A node contains the key, the weighted value, and the linkage pointers on - * the page-replacement algorithm's data structures. - */ - @SuppressWarnings("serial") - static final class Node extends AtomicReference> - implements Linked> { - final K key; - Node prev; - Node next; + final int index = (int) (writeCount & READ_BUFFER_INDEX_MASK); + readBuffers[bufferIndex][index].lazySet(node); - /** Creates a new, unlinked node. */ - Node(K key, WeightedValue weightedValue) { - super(weightedValue); - this.key = key; + return writeCount; } - @Override - public Node getPrevious() { - return prev; + /** + * Attempts to drain the buffers if it is determined to be needed when post-processing a read. + * + * @param bufferIndex + * the index to the chosen read buffer + * @param writeCount + * the number of writes on the chosen read buffer + */ + void drainOnReadIfNeeded(int bufferIndex, long writeCount) { + final long pending = (writeCount - readBufferDrainAtWriteCount[bufferIndex].get()); + final boolean delayable = (pending < READ_BUFFER_THRESHOLD); + final DrainStatus status = drainStatus.get(); + if (status.shouldDrainBuffers(delayable)) { + tryToDrainBuffers(); + } } - @Override - public void setPrevious(Node prev) { - this.prev = prev; + /** + * Performs the post-processing work required after a write. + * + * @param task + * the pending operation to be applied + */ + void afterWrite(Runnable task) { + writeBuffer.add(task); + drainStatus.lazySet(REQUIRED); + tryToDrainBuffers(); + notifyListener(); } - @Override - public Node getNext() { - return next; + /** + * Attempts to acquire the eviction lock and apply the pending operations, up to the amortized threshold, to the + * page replacement policy. + */ + void tryToDrainBuffers() { + if (evictionLock.tryLock()) { + try { + drainStatus.lazySet(PROCESSING); + drainBuffers(); + } finally { + drainStatus.compareAndSet(PROCESSING, IDLE); + evictionLock.unlock(); + } + } } - @Override - public void setNext(Node next) { - this.next = next; + /** Drains the read and write buffers up to an amortized threshold. */ + void drainBuffers() { + drainReadBuffers(); + drainWriteBuffer(); } - /** Retrieves the value held by the current WeightedValue. */ - V getValue() { - return get().value; + /** Drains the read buffers, each up to an amortized threshold. */ + void drainReadBuffers() { + final int start = (int) Thread.currentThread().getId(); + final int end = start + NUMBER_OF_READ_BUFFERS; + for (int i = start; i < end; i++) { + drainReadBuffer(i & READ_BUFFERS_MASK); + } } - } - /** An adapter to safely externalize the keys. */ - final class KeySet extends AbstractSet { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; - - @Override - public int size() { - return map.size(); + /** Drains the read buffer up to an amortized threshold. */ + void drainReadBuffer(int bufferIndex) { + final long writeCount = readBufferWriteCount[bufferIndex].get(); + for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { + final int index = (int) (readBufferReadCount[bufferIndex] & READ_BUFFER_INDEX_MASK); + final AtomicReference> slot = readBuffers[bufferIndex][index]; + final Node node = slot.get(); + if (node == null) { + break; + } + + slot.lazySet(null); + applyRead(node); + readBufferReadCount[bufferIndex]++; + } + readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); } - @Override - public void clear() { - map.clear(); + /** Updates the node's location in the page replacement policy. */ + void applyRead(Node node) { + // An entry may be scheduled for reordering despite having been removed. + // This can occur when the entry was concurrently read while a writer was + // removing it. If the entry is no longer linked then it does not need to + // be processed. + if (evictionDeque.contains(node)) { + evictionDeque.moveToBack(node); + } } - @Override - public Iterator iterator() { - return new KeyIterator(); + /** Drains the read buffer up to an amortized threshold. */ + void drainWriteBuffer() { + for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { + final Runnable task = writeBuffer.poll(); + if (task == null) { + break; + } + task.run(); + } } - @Override - public boolean contains(Object obj) { - return containsKey(obj); + /** + * Attempts to transition the node from the alive state to the retired state. + * + * @param node + * the entry in the page replacement policy + * @param expect + * the expected weighted value + * @return if successful + */ + boolean tryToRetire(Node node, WeightedValue expect) { + if (expect.isAlive()) { + final WeightedValue retired = new WeightedValue(expect.value, -expect.weight); + return node.compareAndSet(expect, retired); + } + return false; } - @Override - public boolean remove(Object obj) { - return (map.remove(obj) != null); + /** + * Atomically transitions the node from the alive state to the retired state, if a valid + * transition. + * + * @param node + * the entry in the page replacement policy + */ + void makeRetired(Node node) { + for (;;) { + final WeightedValue current = node.get(); + if (!current.isAlive()) { + return; + } + final WeightedValue retired = new WeightedValue(current.value, -current.weight); + if (node.compareAndSet(current, retired)) { + return; + } + } } - @Override - public Object[] toArray() { - return map.data.keySet().toArray(); + /** + * Atomically transitions the node to the dead state and decrements the weightedSize. + * + * @param node + * the entry in the page replacement policy + */ + void makeDead(Node node) { + for (;;) { + WeightedValue current = node.get(); + WeightedValue dead = new WeightedValue(current.value, 0); + if (node.compareAndSet(current, dead)) { + weightedSize.lazySet(weightedSize.get() - Math.abs(current.weight)); + return; + } + } } - @Override - public T[] toArray(T[] array) { - return map.data.keySet().toArray(array); + /** Notifies the listener of entries that were evicted. */ + void notifyListener() { + Node node; + while ((node = pendingNotifications.poll()) != null) { + listener.onEviction(node.key, node.getValue()); + } } - } - /** An adapter to safely externalize the key iterator. */ - final class KeyIterator implements Iterator { - final Iterator iterator = data.keySet().iterator(); - K current; + /** Adds the node to the page replacement policy. */ + final class AddTask implements Runnable { + final Node node; + final int weight; - @Override - public boolean hasNext() { - return iterator.hasNext(); - } + AddTask(Node node, int weight) { + this.weight = weight; + this.node = node; + } - @Override - public K next() { - current = iterator.next(); - return current; - } + @Override + public void run() { + weightedSize.lazySet(weightedSize.get() + weight); - @Override - public void remove() { - checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current); - current = null; + // ignore out-of-order write operations + if (node.get().isAlive()) { + evictionDeque.add(node); + evict(); + } + } } - } - /** An adapter to safely externalize the values. */ - final class Values extends AbstractCollection { + /** Removes a node from the page replacement policy. */ + final class RemovalTask implements Runnable { + final Node node; - @Override - public int size() { - return ConcurrentLinkedHashMap.this.size(); + RemovalTask(Node node) { + this.node = node; + } + + @Override + public void run() { + // add may not have been processed yet + evictionDeque.remove(node); + makeDead(node); + } } - @Override - public void clear() { - ConcurrentLinkedHashMap.this.clear(); + /** Updates the weighted size and evicts an entry on overflow. */ + final class UpdateTask implements Runnable { + final int weightDifference; + final Node node; + + public UpdateTask(Node node, int weightDifference) { + this.weightDifference = weightDifference; + this.node = node; + } + + @Override + public void run() { + weightedSize.lazySet(weightedSize.get() + weightDifference); + applyRead(node); + evict(); + } } + /* ---------------- Concurrent Map Support -------------- */ + @Override - public Iterator iterator() { - return new ValueIterator(); + public boolean isEmpty() { + return data.isEmpty(); } @Override - public boolean contains(Object o) { - return containsValue(o); + public int size() { + return data.size(); } - } - /** An adapter to safely externalize the value iterator. */ - final class ValueIterator implements Iterator { - final Iterator> iterator = data.values().iterator(); - Node current; + /** + * Returns the weighted size of this map. + * + * @return the combined weight of the values in this map + */ + public long weightedSize() { + return Math.max(0, weightedSize.get()); + } @Override - public boolean hasNext() { - return iterator.hasNext(); + public void clear() { + evictionLock.lock(); + try { + // Discard all entries + Node node; + while ((node = evictionDeque.poll()) != null) { + data.remove(node.key, node); + makeDead(node); + } + + // Discard all pending reads + for (AtomicReference>[] buffer : readBuffers) { + for (AtomicReference> slot : buffer) { + slot.lazySet(null); + } + } + + // Apply all pending writes + Runnable task; + while ((task = writeBuffer.poll()) != null) { + task.run(); + } + } finally { + evictionLock.unlock(); + } } @Override - public V next() { - current = iterator.next(); - return current.getValue(); + public boolean containsKey(Object key) { + return data.containsKey(key); } @Override - public void remove() { - checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current.key); - current = null; - } - } + public boolean containsValue(Object value) { + checkNotNull(value); - /** An adapter to safely externalize the entries. */ - final class EntrySet extends AbstractSet> { - final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + for (Node node : data.values()) { + if (node.getValue().equals(value)) { + return true; + } + } + return false; + } @Override - public int size() { - return map.size(); + public V get(Object key) { + final Node node = data.get(key); + if (node == null) { + return null; + } + afterRead(node); + return node.getValue(); } - @Override - public void clear() { - map.clear(); + /** + * Returns the value to which the specified key is mapped, or {@code null} if this map contains no mapping for the + * key. This method differs from {@link #get(Object)} in that it does not record the operation with the page + * replacement policy. + * + * @param key + * the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or {@code null} if this map contains no mapping for the + * key + * @throws NullPointerException + * if the specified key is null + */ + public V getQuietly(Object key) { + final Node node = data.get(key); + return (node == null) ? null : node.getValue(); } @Override - public Iterator> iterator() { - return new EntryIterator(); + public V put(K key, V value) { + return put(key, value, false); } @Override - public boolean contains(Object obj) { - if (!(obj instanceof Entry)) { - return false; - } - Entry entry = (Entry) obj; - Node node = map.data.get(entry.getKey()); - return (node != null) && (node.getValue().equals(entry.getValue())); + public V putIfAbsent(K key, V value) { + return put(key, value, true); } - @Override - public boolean add(Entry entry) { - return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); + /** + * Adds a node to the list and the data store. If an existing node is found, then its value is updated if allowed. + * + * @param key + * key with which the specified value is to be associated + * @param value + * value to be associated with the specified key + * @param onlyIfAbsent + * a write is performed only if the key is not already associated with a value + * @return the prior value in the data store or null if no mapping was found + */ + V put(K key, V value, boolean onlyIfAbsent) { + checkNotNull(key); + checkNotNull(value); + + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); + final Node node = new Node(key, weightedValue); + + for (;;) { + final Node prior = data.putIfAbsent(node.key, node); + if (prior == null) { + afterWrite(new AddTask(node, weight)); + return null; + } else if (onlyIfAbsent) { + afterRead(prior); + return prior.getValue(); + } + for (;;) { + final WeightedValue oldWeightedValue = prior.get(); + if (!oldWeightedValue.isAlive()) { + break; + } + + if (prior.compareAndSet(oldWeightedValue, weightedValue)) { + final int weightedDifference = weight - oldWeightedValue.weight; + if (weightedDifference == 0) { + afterRead(prior); + } else { + afterWrite(new UpdateTask(prior, weightedDifference)); + } + return oldWeightedValue.value; + } + } + } } @Override - public boolean remove(Object obj) { - if (!(obj instanceof Entry)) { - return false; - } - Entry entry = (Entry) obj; - return map.remove(entry.getKey(), entry.getValue()); - } - } + public V remove(Object key) { + final Node node = data.remove(key); + if (node == null) { + return null; + } - /** An adapter to safely externalize the entry iterator. */ - final class EntryIterator implements Iterator> { - final Iterator> iterator = data.values().iterator(); - Node current; + makeRetired(node); + afterWrite(new RemovalTask(node)); + return node.getValue(); + } @Override - public boolean hasNext() { - return iterator.hasNext(); + public boolean remove(Object key, Object value) { + final Node node = data.get(key); + if ((node == null) || (value == null)) { + return false; + } + + WeightedValue weightedValue = node.get(); + for (;;) { + if (weightedValue.contains(value)) { + if (tryToRetire(node, weightedValue)) { + if (data.remove(key, node)) { + afterWrite(new RemovalTask(node)); + return true; + } + } else { + weightedValue = node.get(); + if (weightedValue.isAlive()) { + // retry as an intermediate update may have replaced the value with + // an equal instance that has a different reference identity + continue; + } + } + } + return false; + } } @Override - public Entry next() { - current = iterator.next(); - return new WriteThroughEntry(current); + public V replace(K key, V value) { + checkNotNull(key); + checkNotNull(value); + + final int weight = weigher.weightOf(key, value); + final WeightedValue weightedValue = new WeightedValue(value, weight); + + final Node node = data.get(key); + if (node == null) { + return null; + } + for (;;) { + final WeightedValue oldWeightedValue = node.get(); + if (!oldWeightedValue.isAlive()) { + return null; + } + if (node.compareAndSet(oldWeightedValue, weightedValue)) { + final int weightedDifference = weight - oldWeightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } + return oldWeightedValue.value; + } + } } @Override - public void remove() { - checkState(current != null); - ConcurrentLinkedHashMap.this.remove(current.key); - current = null; - } - } + public boolean replace(K key, V oldValue, V newValue) { + checkNotNull(key); + checkNotNull(oldValue); + checkNotNull(newValue); - /** An entry that allows updates to write through to the map. */ - final class WriteThroughEntry extends SimpleEntry { - static final long serialVersionUID = 1; + final int weight = weigher.weightOf(key, newValue); + final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); - WriteThroughEntry(Node node) { - super(node.key, node.getValue()); + final Node node = data.get(key); + if (node == null) { + return false; + } + for (;;) { + final WeightedValue weightedValue = node.get(); + if (!weightedValue.isAlive() || !weightedValue.contains(oldValue)) { + return false; + } + if (node.compareAndSet(weightedValue, newWeightedValue)) { + final int weightedDifference = weight - weightedValue.weight; + if (weightedDifference == 0) { + afterRead(node); + } else { + afterWrite(new UpdateTask(node, weightedDifference)); + } + return true; + } + } } @Override - public V setValue(V value) { - put(getKey(), value); - return super.setValue(value); + public Set keySet() { + final Set ks = keySet; + return (ks == null) ? (keySet = new KeySet()) : ks; } - Object writeReplace() { - return new SimpleEntry(this); + /** + * Returns a unmodifiable snapshot {@link Set} view of the keys contained in this map. The set's iterator returns + * the keys whose order of iteration is the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. + *

+ * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a constant-time operation. Because of + * the asynchronous nature of the page replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @return an ascending snapshot view of the keys in this map + */ + public Set ascendingKeySet() { + return ascendingKeySetWithLimit(Integer.MAX_VALUE); } - } - /** A weigher that enforces that the weight falls within a valid range. */ - static final class BoundedEntryWeigher implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; - final EntryWeigher weigher; - - BoundedEntryWeigher(EntryWeigher weigher) { - checkNotNull(weigher); - this.weigher = weigher; + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this map. The set's iterator returns + * the keys whose order of iteration is the ascending order in which its entries are considered eligible for + * retention, from the least-likely to be retained to the most-likely. + *

+ * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a constant-time operation. Because of + * the asynchronous nature of the page replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @param limit + * the maximum size of the returned set + * @return a ascending snapshot view of the keys in this map + * @throws IllegalArgumentException + * if the limit is negative + */ + public Set ascendingKeySetWithLimit(int limit) { + return orderedKeySet(true, limit); } - @Override - public int weightOf(K key, V value) { - int weight = weigher.weightOf(key, value); - checkArgument(weight >= 1); - return weight; + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this map. The set's iterator returns + * the keys whose order of iteration is the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. + *

+ * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a constant-time operation. Because of + * the asynchronous nature of the page replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @return a descending snapshot view of the keys in this map + */ + public Set descendingKeySet() { + return descendingKeySetWithLimit(Integer.MAX_VALUE); } - Object writeReplace() { - return weigher; - } - } - - /** A queue that discards all additions and is always empty. */ - static final class DiscardingQueue extends AbstractQueue { - @Override public boolean add(Object e) { return true; } - @Override public boolean offer(Object e) { return true; } - @Override public Object poll() { return null; } - @Override public Object peek() { return null; } - @Override public int size() { return 0; } - @Override public Iterator iterator() { return emptyList().iterator(); } - } - - /** A listener that ignores all notifications. */ - enum DiscardingListener implements EvictionListener { - INSTANCE; - - @Override public void onEviction(Object key, Object value) {} - } - - /* ---------------- Serialization Support -------------- */ - - static final long serialVersionUID = 1; - - Object writeReplace() { - return new SerializationProxy(this); - } - - private void readObject(ObjectInputStream stream) throws InvalidObjectException { - throw new InvalidObjectException("Proxy required"); - } - - /** - * A proxy that is serialized instead of the map. The page-replacement - * algorithm's data structures are not serialized so the deserialized - * instance contains only the entries. This is acceptable as caches hold - * transient data that is recomputable and serialization would tend to be - * used as a fast warm-up process. - */ - static final class SerializationProxy implements Serializable { - final EntryWeigher weigher; - final EvictionListener listener; - final int concurrencyLevel; - final Map data; - final long capacity; - - SerializationProxy(ConcurrentLinkedHashMap map) { - concurrencyLevel = map.concurrencyLevel; - data = new HashMap(map); - capacity = map.capacity.get(); - listener = map.listener; - weigher = map.weigher; + /** + * Returns an unmodifiable snapshot {@link Set} view of the keys contained in this map. The set's iterator returns + * the keys whose order of iteration is the descending order in which its entries are considered eligible for + * retention, from the most-likely to be retained to the least-likely. + *

+ * Beware that, unlike in {@link #keySet()}, obtaining the set is NOT a constant-time operation. Because of + * the asynchronous nature of the page replacement policy, determining the retention ordering requires a traversal + * of the keys. + * + * @param limit + * the maximum size of the returned set + * @return a descending snapshot view of the keys in this map + * @throws IllegalArgumentException + * if the limit is negative + */ + public Set descendingKeySetWithLimit(int limit) { + return orderedKeySet(false, limit); + } + + Set orderedKeySet(boolean ascending, int limit) { + checkArgument(limit >= 0); + evictionLock.lock(); + try { + drainBuffers(); + + final int initialCapacity = (weigher == Weighers.entrySingleton()) ? Math.min(limit, (int) weightedSize()) + : 16; + final Set keys = new LinkedHashSet(initialCapacity); + final Iterator> iterator = ascending ? evictionDeque.iterator() + : evictionDeque.descendingIterator(); + while (iterator.hasNext() && (limit > keys.size())) { + keys.add(iterator.next().key); + } + return unmodifiableSet(keys); + } finally { + evictionLock.unlock(); + } } - Object readResolve() { - ConcurrentLinkedHashMap map = new Builder() - .concurrencyLevel(concurrencyLevel) - .maximumWeightedCapacity(capacity) - .listener(listener) - .weigher(weigher) - .build(); - map.putAll(data); - return map; + @Override + public Collection values() { + final Collection vs = values; + return (vs == null) ? (values = new Values()) : vs; } - static final long serialVersionUID = 1; - } - - /* ---------------- Builder -------------- */ - - /** - * A builder that creates {@link ConcurrentLinkedHashMap} instances. It - * provides a flexible approach for constructing customized instances with - * a named parameter syntax. It can be used in the following manner: - *

{@code
-   * ConcurrentMap> graph = new Builder>()
-   *     .maximumWeightedCapacity(5000)
-   *     .weigher(Weighers.set())
-   *     .build();
-   * }
- */ - public static final class Builder { - static final int DEFAULT_CONCURRENCY_LEVEL = 16; - static final int DEFAULT_INITIAL_CAPACITY = 16; - - EvictionListener listener; - EntryWeigher weigher; - - int concurrencyLevel; - int initialCapacity; - long capacity; - - @SuppressWarnings("unchecked") - public Builder() { - capacity = -1; - weigher = Weighers.entrySingleton(); - initialCapacity = DEFAULT_INITIAL_CAPACITY; - concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; - listener = (EvictionListener) DiscardingListener.INSTANCE; + @Override + public Set> entrySet() { + final Set> es = entrySet; + return (es == null) ? (entrySet = new EntrySet()) : es; } /** - * Specifies the initial capacity of the hash table (default 16). - * This is the number of key-value pairs that the hash table can hold - * before a resize operation is required. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this map. The map's collections + * return the mappings whose order of iteration is the ascending order in which its entries are considered eligible + * for retention, from the least-likely to be retained to the most-likely. + *

+ * Beware that obtaining the mappings is NOT a constant-time operation. Because of the asynchronous nature + * of the page replacement policy, determining the retention ordering requires a traversal of the entries. * - * @param initialCapacity the initial capacity used to size the hash table - * to accommodate this many entries. - * - * @return Builder - * @throws IllegalArgumentException if the initialCapacity is negative + * @return a ascending snapshot view of this map */ - public Builder initialCapacity(int initialCapacity) { - checkArgument(initialCapacity >= 0); - this.initialCapacity = initialCapacity; - return this; + public Map ascendingMap() { + return ascendingMapWithLimit(Integer.MAX_VALUE); } /** - * Specifies the maximum weighted capacity to coerce the map to and may - * exceed it temporarily. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this map. The map's collections + * return the mappings whose order of iteration is the ascending order in which its entries are considered eligible + * for retention, from the least-likely to be retained to the most-likely. + *

+ * Beware that obtaining the mappings is NOT a constant-time operation. Because of the asynchronous nature + * of the page replacement policy, determining the retention ordering requires a traversal of the entries. * - * @param capacity the weighted threshold to bound the map by - * @return Builder - * @throws IllegalArgumentException if the maximumWeightedCapacity is - * negative + * @param limit + * the maximum size of the returned map + * @return a ascending snapshot view of this map + * @throws IllegalArgumentException + * if the limit is negative */ - public Builder maximumWeightedCapacity(long capacity) { - checkArgument(capacity >= 0); - this.capacity = capacity; - return this; + public Map ascendingMapWithLimit(int limit) { + return orderedMap(true, limit); } /** - * Specifies the estimated number of concurrently updating threads. The - * implementation performs internal sizing to try to accommodate this many - * threads (default 16). + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this map. The map's collections + * return the mappings whose order of iteration is the descending order in which its entries are considered eligible + * for retention, from the most-likely to be retained to the least-likely. + *

+ * Beware that obtaining the mappings is NOT a constant-time operation. Because of the asynchronous nature + * of the page replacement policy, determining the retention ordering requires a traversal of the entries. * - * @param concurrencyLevel the estimated number of concurrently updating - * threads - * @return Builder - * @throws IllegalArgumentException if the concurrencyLevel is less than or - * equal to zero + * @return a descending snapshot view of this map */ - public Builder concurrencyLevel(int concurrencyLevel) { - checkArgument(concurrencyLevel > 0); - this.concurrencyLevel = concurrencyLevel; - return this; + public Map descendingMap() { + return descendingMapWithLimit(Integer.MAX_VALUE); } /** - * Specifies an optional listener that is registered for notification when - * an entry is evicted. + * Returns an unmodifiable snapshot {@link Map} view of the mappings contained in this map. The map's collections + * return the mappings whose order of iteration is the descending order in which its entries are considered eligible + * for retention, from the most-likely to be retained to the least-likely. + *

+ * Beware that obtaining the mappings is NOT a constant-time operation. Because of the asynchronous nature + * of the page replacement policy, determining the retention ordering requires a traversal of the entries. * - * @param listener the object to forward evicted entries to - * @return Builder - * @throws NullPointerException if the listener is null + * @param limit + * the maximum size of the returned map + * @return a descending snapshot view of this map + * @throws IllegalArgumentException + * if the limit is negative */ - public Builder listener(EvictionListener listener) { - checkNotNull(listener); - this.listener = listener; - return this; + public Map descendingMapWithLimit(int limit) { + return orderedMap(false, limit); + } + + Map orderedMap(boolean ascending, int limit) { + checkArgument(limit >= 0); + evictionLock.lock(); + try { + drainBuffers(); + + final int initialCapacity = (weigher == Weighers.entrySingleton()) ? Math.min(limit, (int) weightedSize()) + : 16; + final Map map = new LinkedHashMap(initialCapacity); + final Iterator> iterator = ascending ? evictionDeque.iterator() + : evictionDeque.descendingIterator(); + while (iterator.hasNext() && (limit > map.size())) { + Node node = iterator.next(); + map.put(node.key, node.getValue()); + } + return unmodifiableMap(map); + } finally { + evictionLock.unlock(); + } + } + + /** The draining status of the buffers. */ + enum DrainStatus { + + /** A drain is not taking place. */ + IDLE { + @Override + boolean shouldDrainBuffers(boolean delayable) { + return !delayable; + } + }, + + /** A drain is required due to a pending write modification. */ + REQUIRED { + @Override + boolean shouldDrainBuffers(boolean delayable) { + return true; + } + }, + + /** A drain is in progress. */ + PROCESSING { + @Override + boolean shouldDrainBuffers(boolean delayable) { + return false; + } + }; + + /** + * Determines whether the buffers should be drained. + * + * @param delayable + * if a drain should be delayed until required + * @return if a drain should be attempted + */ + abstract boolean shouldDrainBuffers(boolean delayable); + } + + /** A value, its weight, and the entry's status. */ + static final class WeightedValue { + final int weight; + final V value; + + WeightedValue(V value, int weight) { + this.weight = weight; + this.value = value; + } + + boolean contains(Object o) { + return (o == value) || value.equals(o); + } + + /** + * If the entry is available in the hash-table and page replacement policy. + */ + boolean isAlive() { + return weight > 0; + } + + /** + * If the entry was removed from the hash-table and is awaiting removal from the page replacement policy. + */ + boolean isRetired() { + return weight < 0; + } + + /** + * If the entry was removed from the hash-table and the page replacement policy. + */ + boolean isDead() { + return weight == 0; + } } /** - * Specifies an algorithm to determine how many the units of capacity a - * value consumes. The default algorithm bounds the map by the number of - * key-value pairs by giving each entry a weight of 1. - * - * @param weigher the algorithm to determine a value's weight - * @return Builder - * @throws NullPointerException if the weigher is null + * A node contains the key, the weighted value, and the linkage pointers on the page-replacement algorithm's data + * structures. */ - public Builder weigher(Weigher weigher) { - this.weigher = (weigher == Weighers.singleton()) - ? Weighers.entrySingleton() - : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); - return this; + @SuppressWarnings("serial") + static final class Node extends AtomicReference> implements Linked> { + final K key; + Node prev; + Node next; + + /** Creates a new, unlinked node. */ + Node(K key, WeightedValue weightedValue) { + super(weightedValue); + this.key = key; + } + + @Override + public Node getPrevious() { + return prev; + } + + @Override + public void setPrevious(Node prev) { + this.prev = prev; + } + + @Override + public Node getNext() { + return next; + } + + @Override + public void setNext(Node next) { + this.next = next; + } + + /** Retrieves the value held by the current WeightedValue. */ + V getValue() { + return get().value; + } + } + + /** An adapter to safely externalize the keys. */ + final class KeySet extends AbstractSet { + final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + + @Override + public int size() { + return map.size(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Iterator iterator() { + return new KeyIterator(); + } + + @Override + public boolean contains(Object obj) { + return containsKey(obj); + } + + @Override + public boolean remove(Object obj) { + return (map.remove(obj) != null); + } + + @Override + public Object[] toArray() { + return map.data.keySet().toArray(); + } + + @Override + public T[] toArray(T[] array) { + return map.data.keySet().toArray(array); + } + } + + /** An adapter to safely externalize the key iterator. */ + final class KeyIterator implements Iterator { + final Iterator iterator = data.keySet().iterator(); + K current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public K next() { + current = iterator.next(); + return current; + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current); + current = null; + } + } + + /** An adapter to safely externalize the values. */ + final class Values extends AbstractCollection { + + @Override + public int size() { + return ConcurrentLinkedHashMap.this.size(); + } + + @Override + public void clear() { + ConcurrentLinkedHashMap.this.clear(); + } + + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Override + public boolean contains(Object o) { + return containsValue(o); + } + } + + /** An adapter to safely externalize the value iterator. */ + final class ValueIterator implements Iterator { + final Iterator> iterator = data.values().iterator(); + Node current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public V next() { + current = iterator.next(); + return current.getValue(); + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current.key); + current = null; + } + } + + /** An adapter to safely externalize the entries. */ + final class EntrySet extends AbstractSet> { + final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; + + @Override + public int size() { + return map.size(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public boolean contains(Object obj) { + if (!(obj instanceof Entry)) { + return false; + } + Entry entry = (Entry) obj; + Node node = map.data.get(entry.getKey()); + return (node != null) && (node.getValue().equals(entry.getValue())); + } + + @Override + public boolean add(Entry entry) { + return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); + } + + @Override + public boolean remove(Object obj) { + if (!(obj instanceof Entry)) { + return false; + } + Entry entry = (Entry) obj; + return map.remove(entry.getKey(), entry.getValue()); + } + } + + /** An adapter to safely externalize the entry iterator. */ + final class EntryIterator implements Iterator> { + final Iterator> iterator = data.values().iterator(); + Node current; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Entry next() { + current = iterator.next(); + return new WriteThroughEntry(current); + } + + @Override + public void remove() { + checkState(current != null); + ConcurrentLinkedHashMap.this.remove(current.key); + current = null; + } + } + + /** An entry that allows updates to write through to the map. */ + final class WriteThroughEntry extends SimpleEntry { + static final long serialVersionUID = 1; + + WriteThroughEntry(Node node) { + super(node.key, node.getValue()); + } + + @Override + public V setValue(V value) { + put(getKey(), value); + return super.setValue(value); + } + + Object writeReplace() { + return new SimpleEntry(this); + } + } + + /** A weigher that enforces that the weight falls within a valid range. */ + static final class BoundedEntryWeigher implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; + final EntryWeigher weigher; + + BoundedEntryWeigher(EntryWeigher weigher) { + checkNotNull(weigher); + this.weigher = weigher; + } + + @Override + public int weightOf(K key, V value) { + int weight = weigher.weightOf(key, value); + checkArgument(weight >= 1); + return weight; + } + + Object writeReplace() { + return weigher; + } + } + + /** A queue that discards all additions and is always empty. */ + static final class DiscardingQueue extends AbstractQueue { + @Override + public boolean add(Object e) { + return true; + } + + @Override + public boolean offer(Object e) { + return true; + } + + @Override + public Object poll() { + return null; + } + + @Override + public Object peek() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public Iterator iterator() { + return emptyList().iterator(); + } + } + + /** A listener that ignores all notifications. */ + enum DiscardingListener implements EvictionListener { + INSTANCE; + + @Override + public void onEviction(Object key, Object value) {} + } + + /* ---------------- Serialization Support -------------- */ + + static final long serialVersionUID = 1; + + Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); } /** - * Specifies an algorithm to determine how many the units of capacity an - * entry consumes. The default algorithm bounds the map by the number of - * key-value pairs by giving each entry a weight of 1. - * - * @param weigher the algorithm to determine a entry's weight - * @return Builder - * @throws NullPointerException if the weigher is null + * A proxy that is serialized instead of the map. The page-replacement algorithm's data structures are not + * serialized so the deserialized instance contains only the entries. This is acceptable as caches hold transient + * data that is recomputable and serialization would tend to be used as a fast warm-up process. */ - public Builder weigher(EntryWeigher weigher) { - this.weigher = (weigher == Weighers.entrySingleton()) - ? Weighers.entrySingleton() - : new BoundedEntryWeigher(weigher); - return this; + static final class SerializationProxy implements Serializable { + final EntryWeigher weigher; + final EvictionListener listener; + final int concurrencyLevel; + final Map data; + final long capacity; + + SerializationProxy(ConcurrentLinkedHashMap map) { + concurrencyLevel = map.concurrencyLevel; + data = new HashMap(map); + capacity = map.capacity.get(); + listener = map.listener; + weigher = map.weigher; + } + + Object readResolve() { + ConcurrentLinkedHashMap map = new Builder().concurrencyLevel(concurrencyLevel) + .maximumWeightedCapacity(capacity).listener(listener).weigher(weigher).build(); + map.putAll(data); + return map; + } + + static final long serialVersionUID = 1; } + /* ---------------- Builder -------------- */ + /** - * Creates a new {@link ConcurrentLinkedHashMap} instance. - * - * @return ConcurrentLinkedHashMap - * @throws IllegalStateException if the maximum weighted capacity was - * not set + * A builder that creates {@link ConcurrentLinkedHashMap} instances. It provides a flexible approach for + * constructing customized instances with a named parameter syntax. It can be used in the following manner: + * + *
+     * {
+     *     @code
+     *     ConcurrentMap> graph = new Builder>().maximumWeightedCapacity(5000)
+     *             .weigher(Weighers.set()).build();
+     * }
+     * 
*/ - public ConcurrentLinkedHashMap build() { - checkState(capacity >= 0); - return new ConcurrentLinkedHashMap(this); + public static final class Builder { + static final int DEFAULT_CONCURRENCY_LEVEL = 16; + static final int DEFAULT_INITIAL_CAPACITY = 16; + + EvictionListener listener; + EntryWeigher weigher; + + int concurrencyLevel; + int initialCapacity; + long capacity; + + @SuppressWarnings("unchecked") + public Builder() { + capacity = -1; + weigher = Weighers.entrySingleton(); + initialCapacity = DEFAULT_INITIAL_CAPACITY; + concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; + listener = (EvictionListener) DiscardingListener.INSTANCE; + } + + /** + * Specifies the initial capacity of the hash table (default 16). This is the number of key-value + * pairs that the hash table can hold before a resize operation is required. + * + * @param initialCapacity + * the initial capacity used to size the hash table to accommodate this many entries. + * + * @return Builder + * @throws IllegalArgumentException + * if the initialCapacity is negative + */ + public Builder initialCapacity(int initialCapacity) { + checkArgument(initialCapacity >= 0); + this.initialCapacity = initialCapacity; + return this; + } + + /** + * Specifies the maximum weighted capacity to coerce the map to and may exceed it temporarily. + * + * @param capacity + * the weighted threshold to bound the map by + * @return Builder + * @throws IllegalArgumentException + * if the maximumWeightedCapacity is negative + */ + public Builder maximumWeightedCapacity(long capacity) { + checkArgument(capacity >= 0); + this.capacity = capacity; + return this; + } + + /** + * Specifies the estimated number of concurrently updating threads. The implementation performs internal sizing + * to try to accommodate this many threads (default 16). + * + * @param concurrencyLevel + * the estimated number of concurrently updating threads + * @return Builder + * @throws IllegalArgumentException + * if the concurrencyLevel is less than or equal to zero + */ + public Builder concurrencyLevel(int concurrencyLevel) { + checkArgument(concurrencyLevel > 0); + this.concurrencyLevel = concurrencyLevel; + return this; + } + + /** + * Specifies an optional listener that is registered for notification when an entry is evicted. + * + * @param listener + * the object to forward evicted entries to + * @return Builder + * @throws NullPointerException + * if the listener is null + */ + public Builder listener(EvictionListener listener) { + checkNotNull(listener); + this.listener = listener; + return this; + } + + /** + * Specifies an algorithm to determine how many the units of capacity a value consumes. The default algorithm + * bounds the map by the number of key-value pairs by giving each entry a weight of 1. + * + * @param weigher + * the algorithm to determine a value's weight + * @return Builder + * @throws NullPointerException + * if the weigher is null + */ + public Builder weigher(Weigher weigher) { + this.weigher = (weigher == Weighers.singleton()) ? Weighers + .entrySingleton() : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); + return this; + } + + /** + * Specifies an algorithm to determine how many the units of capacity an entry consumes. The default algorithm + * bounds the map by the number of key-value pairs by giving each entry a weight of 1. + * + * @param weigher + * the algorithm to determine a entry's weight + * @return Builder + * @throws NullPointerException + * if the weigher is null + */ + public Builder weigher(EntryWeigher weigher) { + this.weigher = (weigher == Weighers.entrySingleton()) ? Weighers.entrySingleton() + : new BoundedEntryWeigher(weigher); + return this; + } + + /** + * Creates a new {@link ConcurrentLinkedHashMap} instance. + * + * @return ConcurrentLinkedHashMap + * @throws IllegalStateException + * if the maximum weighted capacity was not set + */ + public ConcurrentLinkedHashMap build() { + checkState(capacity >= 0); + return new ConcurrentLinkedHashMap(this); + } } - } } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java index 9bf2a22b0..a2268ae8b 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EntryWeigher.java @@ -1,23 +1,16 @@ /* - * Copyright 2012 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2012 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ package mssql.googlecode.concurrentlinkedhashmap; /** - * A class that can determine the weight of an entry. The total weight threshold - * is used to determine when an eviction is required. + * A class that can determine the weight of an entry. The total weight threshold is used to determine when an eviction + * is required. * * @author ben.manes@gmail.com (Ben Manes) * @see @@ -25,13 +18,15 @@ */ public interface EntryWeigher { - /** - * Measures an entry's weight to determine how many units of capacity that - * the key and value consumes. An entry must consume a minimum of one unit. - * - * @param key the key to weigh - * @param value the value to weigh - * @return the entry's weight - */ - int weightOf(K key, V value); + /** + * Measures an entry's weight to determine how many units of capacity that the key and value consumes. An entry must + * consume a minimum of one unit. + * + * @param key + * the key to weigh + * @param value + * the value to weigh + * @return the entry's weight + */ + int weightOf(K key, V value); } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java index 65488587c..3c878a815 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/EvictionListener.java @@ -1,32 +1,22 @@ /* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2010 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ package mssql.googlecode.concurrentlinkedhashmap; /** - * A listener registered for notification when an entry is evicted. An instance - * may be called concurrently by multiple threads to process entries. An - * implementation should avoid performing blocking calls or synchronizing on - * shared resources. + * A listener registered for notification when an entry is evicted. An instance may be called concurrently by multiple + * threads to process entries. An implementation should avoid performing blocking calls or synchronizing on shared + * resources. *

- * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's - * thread and will not block other threads from operating on the map. An - * implementation should be aware that the caller's thread will not expect - * long execution times or failures as a side effect of the listener being - * notified. Execution safety and a fast turn around time can be achieved by - * performing the operation asynchronously, such as by submitting a task to an + * The listener is invoked by {@link ConcurrentLinkedHashMap} on a caller's thread and will not block other threads from + * operating on the map. An implementation should be aware that the caller's thread will not expect long execution times + * or failures as a side effect of the listener being notified. Execution safety and a fast turn around time can be + * achieved by performing the operation asynchronously, such as by submitting a task to an * {@link java.util.concurrent.ExecutorService}. * * @author ben.manes@gmail.com (Ben Manes) @@ -35,11 +25,13 @@ */ public interface EvictionListener { - /** - * A call-back notification that the entry was evicted. - * - * @param key the entry's key - * @param value the entry's value - */ - void onEviction(K key, V value); + /** + * A call-back notification that the entry was evicted. + * + * @param key + * the entry's key + * @param value + * the entry's value + */ + void onEviction(K key, V value); } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java index 2bb23ea78..11b37579b 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/LinkedDeque.java @@ -1,17 +1,10 @@ /* - * Copyright 2011 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2011 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ package mssql.googlecode.concurrentlinkedhashmap; @@ -21,440 +14,432 @@ import java.util.Iterator; import java.util.NoSuchElementException; + /** - * Linked list implementation of the {@link Deque} interface where the link - * pointers are tightly integrated with the element. Linked deques have no - * capacity restrictions; they grow as necessary to support usage. They are not - * thread-safe; in the absence of external synchronization, they do not support - * concurrent access by multiple threads. Null elements are prohibited. + * Linked list implementation of the {@link Deque} interface where the link pointers are tightly integrated with the + * element. Linked deques have no capacity restrictions; they grow as necessary to support usage. They are not + * thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. + * Null elements are prohibited. *

- * Most LinkedDeque operations run in constant time by assuming that - * the {@link Linked} parameter is associated with the deque instance. Any usage - * that violates this assumption will result in non-deterministic behavior. + * Most LinkedDeque operations run in constant time by assuming that the {@link Linked} parameter is associated + * with the deque instance. Any usage that violates this assumption will result in non-deterministic behavior. *

- * The iterators returned by this class are not fail-fast: If - * the deque is modified at any time after the iterator is created, the iterator - * will be in an unknown state. Thus, in the face of concurrent modification, - * the iterator risks arbitrary, non-deterministic behavior at an undetermined - * time in the future. + * The iterators returned by this class are not fail-fast: If the deque is modified at any time after + * the iterator is created, the iterator will be in an unknown state. Thus, in the face of concurrent modification, the + * iterator risks arbitrary, non-deterministic behavior at an undetermined time in the future. * * @author ben.manes@gmail.com (Ben Manes) - * @param the type of elements held in this collection + * @param + * the type of elements held in this collection * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ final class LinkedDeque> extends AbstractCollection implements Deque { - // This class provides a doubly-linked list that is optimized for the virtual - // machine. The first and last elements are manipulated instead of a slightly - // more convenient sentinel element to avoid the insertion of null checks with - // NullPointerException throws in the byte code. The links to a removed - // element are cleared to help a generational garbage collector if the - // discarded elements inhabit more than one generation. - - /** - * Pointer to first node. - * Invariant: (first == null && last == null) || - * (first.prev == null) - */ - E first; - - /** - * Pointer to last node. - * Invariant: (first == null && last == null) || - * (last.next == null) - */ - E last; - - /** - * Links the element to the front of the deque so that it becomes the first - * element. - * - * @param e the unlinked element - */ - void linkFirst(final E e) { - final E f = first; - first = e; - - if (f == null) { - last = e; - } else { - f.setPrevious(e); - e.setNext(f); - } - } - - /** - * Links the element to the back of the deque so that it becomes the last - * element. - * - * @param e the unlinked element - */ - void linkLast(final E e) { - final E l = last; - last = e; - - if (l == null) { - first = e; - } else { - l.setNext(e); - e.setPrevious(l); - } - } - - /** Unlinks the non-null first element. */ - E unlinkFirst() { - final E f = first; - final E next = f.getNext(); - f.setNext(null); - - first = next; - if (next == null) { - last = null; - } else { - next.setPrevious(null); - } - return f; - } - - /** Unlinks the non-null last element. */ - E unlinkLast() { - final E l = last; - final E prev = l.getPrevious(); - l.setPrevious(null); - last = prev; - if (prev == null) { - first = null; - } else { - prev.setNext(null); - } - return l; - } - - /** Unlinks the non-null element. */ - void unlink(E e) { - final E prev = e.getPrevious(); - final E next = e.getNext(); - - if (prev == null) { - first = next; - } else { - prev.setNext(next); - e.setPrevious(null); - } - - if (next == null) { - last = prev; - } else { - next.setPrevious(prev); - e.setNext(null); - } - } - - @Override - public boolean isEmpty() { - return (first == null); - } - - void checkNotEmpty() { - if (isEmpty()) { - throw new NoSuchElementException(); - } - } - - /** - * {@inheritDoc} - *

- * Beware that, unlike in most collections, this method is NOT a - * constant-time operation. - */ - @Override - public int size() { - int size = 0; - for (E e = first; e != null; e = e.getNext()) { - size++; - } - return size; - } - - @Override - public void clear() { - for (E e = first; e != null;) { - E next = e.getNext(); - e.setPrevious(null); - e.setNext(null); - e = next; - } - first = last = null; - } - - @Override - public boolean contains(Object o) { - return (o instanceof Linked) && contains((Linked) o); - } - - // A fast-path containment check - boolean contains(Linked e) { - return (e.getPrevious() != null) - || (e.getNext() != null) - || (e == first); - } - - /** - * Moves the element to the front of the deque so that it becomes the first - * element. - * - * @param e the linked element - */ - public void moveToFront(E e) { - if (e != first) { - unlink(e); - linkFirst(e); - } - } - - /** - * Moves the element to the back of the deque so that it becomes the last - * element. - * - * @param e the linked element - */ - public void moveToBack(E e) { - if (e != last) { - unlink(e); - linkLast(e); - } - } - - @Override - public E peek() { - return peekFirst(); - } - - @Override - public E peekFirst() { - return first; - } - - @Override - public E peekLast() { - return last; - } - - @Override - public E getFirst() { - checkNotEmpty(); - return peekFirst(); - } - - @Override - public E getLast() { - checkNotEmpty(); - return peekLast(); - } - - @Override - public E element() { - return getFirst(); - } - - @Override - public boolean offer(E e) { - return offerLast(e); - } - - @Override - public boolean offerFirst(E e) { - if (contains(e)) { - return false; - } - linkFirst(e); - return true; - } - - @Override - public boolean offerLast(E e) { - if (contains(e)) { - return false; - } - linkLast(e); - return true; - } - - @Override - public boolean add(E e) { - return offerLast(e); - } - - - @Override - public void addFirst(E e) { - if (!offerFirst(e)) { - throw new IllegalArgumentException(); - } - } - - @Override - public void addLast(E e) { - if (!offerLast(e)) { - throw new IllegalArgumentException(); - } - } - - @Override - public E poll() { - return pollFirst(); - } - - @Override - public E pollFirst() { - return isEmpty() ? null : unlinkFirst(); - } - - @Override - public E pollLast() { - return isEmpty() ? null : unlinkLast(); - } - - @Override - public E remove() { - return removeFirst(); - } - - @Override - @SuppressWarnings("unchecked") - public boolean remove(Object o) { - return (o instanceof Linked) && remove((E) o); - } - - // A fast-path removal - boolean remove(E e) { - if (contains(e)) { - unlink(e); - return true; - } - return false; - } - - @Override - public E removeFirst() { - checkNotEmpty(); - return pollFirst(); - } - - @Override - public boolean removeFirstOccurrence(Object o) { - return remove(o); - } - - @Override - public E removeLast() { - checkNotEmpty(); - return pollLast(); - } - - @Override - public boolean removeLastOccurrence(Object o) { - return remove(o); - } - - @Override - public boolean removeAll(Collection c) { - boolean modified = false; - for (Object o : c) { - modified |= remove(o); - } - return modified; - } - - @Override - public void push(E e) { - addFirst(e); - } - - @Override - public E pop() { - return removeFirst(); - } - - @Override - public Iterator iterator() { - return new AbstractLinkedIterator(first) { - @Override E computeNext() { - return cursor.getNext(); - } - }; - } - - @Override - public Iterator descendingIterator() { - return new AbstractLinkedIterator(last) { - @Override E computeNext() { - return cursor.getPrevious(); - } - }; - } - - abstract class AbstractLinkedIterator implements Iterator { - E cursor; + // This class provides a doubly-linked list that is optimized for the virtual + // machine. The first and last elements are manipulated instead of a slightly + // more convenient sentinel element to avoid the insertion of null checks with + // NullPointerException throws in the byte code. The links to a removed + // element are cleared to help a generational garbage collector if the + // discarded elements inhabit more than one generation. + + /** + * Pointer to first node. Invariant: (first == null && last == null) || (first.prev == null) + */ + E first; + + /** + * Pointer to last node. Invariant: (first == null && last == null) || (last.next == null) + */ + E last; + + /** + * Links the element to the front of the deque so that it becomes the first element. + * + * @param e + * the unlinked element + */ + void linkFirst(final E e) { + final E f = first; + first = e; + + if (f == null) { + last = e; + } else { + f.setPrevious(e); + e.setNext(f); + } + } /** - * Creates an iterator that can can traverse the deque. + * Links the element to the back of the deque so that it becomes the last element. * - * @param start the initial element to begin traversal from + * @param e + * the unlinked element */ - AbstractLinkedIterator(E start) { - cursor = start; + void linkLast(final E e) { + final E l = last; + last = e; + + if (l == null) { + first = e; + } else { + l.setNext(e); + e.setPrevious(l); + } + } + + /** Unlinks the non-null first element. */ + E unlinkFirst() { + final E f = first; + final E next = f.getNext(); + f.setNext(null); + + first = next; + if (next == null) { + last = null; + } else { + next.setPrevious(null); + } + return f; + } + + /** Unlinks the non-null last element. */ + E unlinkLast() { + final E l = last; + final E prev = l.getPrevious(); + l.setPrevious(null); + last = prev; + if (prev == null) { + first = null; + } else { + prev.setNext(null); + } + return l; + } + + /** Unlinks the non-null element. */ + void unlink(E e) { + final E prev = e.getPrevious(); + final E next = e.getNext(); + + if (prev == null) { + first = next; + } else { + prev.setNext(next); + e.setPrevious(null); + } + + if (next == null) { + last = prev; + } else { + next.setPrevious(prev); + e.setNext(null); + } } @Override - public boolean hasNext() { - return (cursor != null); + public boolean isEmpty() { + return (first == null); + } + + void checkNotEmpty() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + } + + /** + * {@inheritDoc} + *

+ * Beware that, unlike in most collections, this method is NOT a constant-time operation. + */ + @Override + public int size() { + int size = 0; + for (E e = first; e != null; e = e.getNext()) { + size++; + } + return size; } @Override - public E next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - E e = cursor; - cursor = computeNext(); - return e; + public void clear() { + for (E e = first; e != null;) { + E next = e.getNext(); + e.setPrevious(null); + e.setNext(null); + e = next; + } + first = last = null; } @Override - public void remove() { - throw new UnsupportedOperationException(); + public boolean contains(Object o) { + return (o instanceof Linked) && contains((Linked) o); + } + + // A fast-path containment check + boolean contains(Linked e) { + return (e.getPrevious() != null) || (e.getNext() != null) || (e == first); + } + + /** + * Moves the element to the front of the deque so that it becomes the first element. + * + * @param e + * the linked element + */ + public void moveToFront(E e) { + if (e != first) { + unlink(e); + linkFirst(e); + } } /** - * Retrieves the next element to traverse to or null if there are - * no more elements. + * Moves the element to the back of the deque so that it becomes the last element. + * + * @param e + * the linked element */ - abstract E computeNext(); - } + public void moveToBack(E e) { + if (e != last) { + unlink(e); + linkLast(e); + } + } + + @Override + public E peek() { + return peekFirst(); + } + + @Override + public E peekFirst() { + return first; + } + + @Override + public E peekLast() { + return last; + } + + @Override + public E getFirst() { + checkNotEmpty(); + return peekFirst(); + } + + @Override + public E getLast() { + checkNotEmpty(); + return peekLast(); + } + + @Override + public E element() { + return getFirst(); + } + + @Override + public boolean offer(E e) { + return offerLast(e); + } + + @Override + public boolean offerFirst(E e) { + if (contains(e)) { + return false; + } + linkFirst(e); + return true; + } + + @Override + public boolean offerLast(E e) { + if (contains(e)) { + return false; + } + linkLast(e); + return true; + } + + @Override + public boolean add(E e) { + return offerLast(e); + } + + @Override + public void addFirst(E e) { + if (!offerFirst(e)) { + throw new IllegalArgumentException(); + } + } + + @Override + public void addLast(E e) { + if (!offerLast(e)) { + throw new IllegalArgumentException(); + } + } + + @Override + public E poll() { + return pollFirst(); + } + + @Override + public E pollFirst() { + return isEmpty() ? null : unlinkFirst(); + } + + @Override + public E pollLast() { + return isEmpty() ? null : unlinkLast(); + } + + @Override + public E remove() { + return removeFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + return (o instanceof Linked) && remove((E) o); + } + + // A fast-path removal + boolean remove(E e) { + if (contains(e)) { + unlink(e); + return true; + } + return false; + } + + @Override + public E removeFirst() { + checkNotEmpty(); + return pollFirst(); + } + + @Override + public boolean removeFirstOccurrence(Object o) { + return remove(o); + } + + @Override + public E removeLast() { + checkNotEmpty(); + return pollLast(); + } + + @Override + public boolean removeLastOccurrence(Object o) { + return remove(o); + } + + @Override + public boolean removeAll(Collection c) { + boolean modified = false; + for (Object o : c) { + modified |= remove(o); + } + return modified; + } + + @Override + public void push(E e) { + addFirst(e); + } + + @Override + public E pop() { + return removeFirst(); + } + + @Override + public Iterator iterator() { + return new AbstractLinkedIterator(first) { + @Override + E computeNext() { + return cursor.getNext(); + } + }; + } + + @Override + public Iterator descendingIterator() { + return new AbstractLinkedIterator(last) { + @Override + E computeNext() { + return cursor.getPrevious(); + } + }; + } + + abstract class AbstractLinkedIterator implements Iterator { + E cursor; + + /** + * Creates an iterator that can can traverse the deque. + * + * @param start + * the initial element to begin traversal from + */ + AbstractLinkedIterator(E start) { + cursor = start; + } + + @Override + public boolean hasNext() { + return (cursor != null); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + E e = cursor; + cursor = computeNext(); + return e; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Retrieves the next element to traverse to or null if there are no more elements. + */ + abstract E computeNext(); + } } + /** * An element that is linked on the {@link Deque}. */ interface Linked> { - /** - * Retrieves the previous element or null if either the element is - * unlinked or the first element on the deque. - */ - T getPrevious(); + /** + * Retrieves the previous element or null if either the element is unlinked or the first element on the + * deque. + */ + T getPrevious(); - /** Sets the previous element or null if there is no link. */ - void setPrevious(T prev); + /** Sets the previous element or null if there is no link. */ + void setPrevious(T prev); - /** - * Retrieves the next element or null if either the element is - * unlinked or the last element on the deque. - */ - T getNext(); + /** + * Retrieves the next element or null if either the element is unlinked or the last element on the deque. + */ + T getNext(); - /** Sets the next element or null if there is no link. */ - void setNext(T next); + /** Sets the next element or null if there is no link. */ + void setNext(T next); } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java index 529622c8e..947ae0d1d 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weigher.java @@ -1,23 +1,16 @@ /* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2010 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ package mssql.googlecode.concurrentlinkedhashmap; /** - * A class that can determine the weight of a value. The total weight threshold - * is used to determine when an eviction is required. + * A class that can determine the weight of a value. The total weight threshold is used to determine when an eviction is + * required. * * @author ben.manes@gmail.com (Ben Manes) * @see @@ -25,12 +18,13 @@ */ public interface Weigher { - /** - * Measures an object's weight to determine how many units of capacity that - * the value consumes. A value must consume a minimum of one unit. - * - * @param value the object to weigh - * @return the object's weight - */ - int weightOf(V value); + /** + * Measures an object's weight to determine how many units of capacity that the value consumes. A value must consume + * a minimum of one unit. + * + * @param value + * the object to weigh + * @return the object's weight + */ + int weightOf(V value); } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java index 469977a11..a979cebcc 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/Weighers.java @@ -1,17 +1,10 @@ /* - * Copyright 2010 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2010 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ package mssql.googlecode.concurrentlinkedhashmap; @@ -24,6 +17,7 @@ import java.util.Map; import java.util.Set; + /** * A common set of {@link Weigher} and {@link EntryWeigher} implementations. * @@ -33,260 +27,253 @@ */ public final class Weighers { - private Weighers() { - throw new AssertionError(); - } - - /** - * A entry weigher backed by the specified weigher. The weight of the value - * determines the weight of the entry. - * - * @param K - * @param V - * @param weigher the weigher to be "wrapped" in a entry weigher. - * @return A entry weigher view of the specified weigher. - */ - public static EntryWeigher asEntryWeigher( - final Weigher weigher) { - return (weigher == singleton()) - ? Weighers.entrySingleton() - : new EntryWeigherView(weigher); - } - - /** - * A weigher where an entry has a weight of 1. A map bounded with - * this weigher will evict when the number of key-value pairs exceeds the - * capacity. - * - * @param K - * @param V - * @return A weigher where a value takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static EntryWeigher entrySingleton() { - return (EntryWeigher) SingletonEntryWeigher.INSTANCE; - } - - /** - * A weigher where a value has a weight of 1. A map bounded with - * this weigher will evict when the number of key-value pairs exceeds the - * capacity. - * - * @param V - * @return A weigher where a value takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher singleton() { - return (Weigher) SingletonWeigher.INSTANCE; - } - - /** - * A weigher where the value is a byte array and its weight is the number of - * bytes. A map bounded with this weigher will evict when the number of bytes - * exceeds the capacity rather than the number of key-value pairs in the map. - * This allows for restricting the capacity based on the memory-consumption - * and is primarily for usage by dedicated caching servers that hold the - * serialized data. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @return A weigher where each byte takes one unit of capacity. - */ - public static Weigher byteArray() { - return ByteArrayWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Iterable} and its weight is the - * number of elements. This weigher only should be used when the alternative - * {@link #collection()} weigher cannot be, as evaluation takes O(n) time. A - * map bounded with this weigher will evict when the total number of elements - * exceeds the capacity rather than the number of key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @param E - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> iterable() { - return (Weigher>) (Weigher) IterableWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Collection} and its weight is the - * number of elements. A map bounded with this weigher will evict when the - * total number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @param E - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> collection() { - return (Weigher>) (Weigher) CollectionWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link List} and its weight is the number - * of elements. A map bounded with this weigher will evict when the total - * number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @param E - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> list() { - return (Weigher>) (Weigher) ListWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Set} and its weight is the number - * of elements. A map bounded with this weigher will evict when the total - * number of elements exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @param E - * @return A weigher where each element takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> set() { - return (Weigher>) (Weigher) SetWeigher.INSTANCE; - } - - /** - * A weigher where the value is a {@link Map} and its weight is the number of - * entries. A map bounded with this weigher will evict when the total number of - * entries across all values exceeds the capacity rather than the number of - * key-value pairs in the map. - *

- * A value with a weight of 0 will be rejected by the map. If a value - * with this weight can occur then the caller should eagerly evaluate the - * value and treat it as a removal operation. Alternatively, a custom weigher - * may be specified on the map to assign an empty value a positive weight. - * - * @param A - * @param B - * @return A weigher where each entry takes one unit of capacity. - */ - @SuppressWarnings({"cast", "unchecked"}) - public static Weigher> map() { - return (Weigher>) (Weigher) MapWeigher.INSTANCE; - } - - static final class EntryWeigherView implements EntryWeigher, Serializable { - static final long serialVersionUID = 1; - final Weigher weigher; - - EntryWeigherView(Weigher weigher) { - checkNotNull(weigher); - this.weigher = weigher; + private Weighers() { + throw new AssertionError(); + } + + /** + * A entry weigher backed by the specified weigher. The weight of the value determines the weight of the entry. + * + * @param + * K + * @param + * V + * @param weigher + * the weigher to be "wrapped" in a entry weigher. + * @return A entry weigher view of the specified weigher. + */ + public static EntryWeigher asEntryWeigher(final Weigher weigher) { + return (weigher == singleton()) ? Weighers.entrySingleton() : new EntryWeigherView(weigher); + } + + /** + * A weigher where an entry has a weight of 1. A map bounded with this weigher will evict when the + * number of key-value pairs exceeds the capacity. + * + * @param + * K + * @param + * V + * @return A weigher where a value takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static EntryWeigher entrySingleton() { + return (EntryWeigher) SingletonEntryWeigher.INSTANCE; + } + + /** + * A weigher where a value has a weight of 1. A map bounded with this weigher will evict when the + * number of key-value pairs exceeds the capacity. + * + * @param + * V + * @return A weigher where a value takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher singleton() { + return (Weigher) SingletonWeigher.INSTANCE; + } + + /** + * A weigher where the value is a byte array and its weight is the number of bytes. A map bounded with this weigher + * will evict when the number of bytes exceeds the capacity rather than the number of key-value pairs in the map. + * This allows for restricting the capacity based on the memory-consumption and is primarily for usage by dedicated + * caching servers that hold the serialized data. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @return A weigher where each byte takes one unit of capacity. + */ + public static Weigher byteArray() { + return ByteArrayWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Iterable} and its weight is the number of elements. This weigher only + * should be used when the alternative {@link #collection()} weigher cannot be, as evaluation takes O(n) time. A map + * bounded with this weigher will evict when the total number of elements exceeds the capacity rather than the + * number of key-value pairs in the map. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param + * E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> iterable() { + return (Weigher>) (Weigher) IterableWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Collection} and its weight is the number of elements. A map bounded with + * this weigher will evict when the total number of elements exceeds the capacity rather than the number of + * key-value pairs in the map. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param + * E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> collection() { + return (Weigher>) (Weigher) CollectionWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link List} and its weight is the number of elements. A map bounded with this + * weigher will evict when the total number of elements exceeds the capacity rather than the number of key-value + * pairs in the map. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param + * E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> list() { + return (Weigher>) (Weigher) ListWeigher.INSTANCE; + } + + /** + * A weigher where the value is a {@link Set} and its weight is the number of elements. A map bounded with this + * weigher will evict when the total number of elements exceeds the capacity rather than the number of key-value + * pairs in the map. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param + * E + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> set() { + return (Weigher>) (Weigher) SetWeigher.INSTANCE; } - @Override - public int weightOf(K key, V value) { - return weigher.weightOf(value); + /** + * A weigher where the value is a {@link Map} and its weight is the number of entries. A map bounded with this + * weigher will evict when the total number of entries across all values exceeds the capacity rather than the number + * of key-value pairs in the map. + *

+ * A value with a weight of 0 will be rejected by the map. If a value with this weight can occur then + * the caller should eagerly evaluate the value and treat it as a removal operation. Alternatively, a custom weigher + * may be specified on the map to assign an empty value a positive weight. + * + * @param + * A + * @param + * B + * @return A weigher where each entry takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> map() { + return (Weigher>) (Weigher) MapWeigher.INSTANCE; } - } - enum SingletonEntryWeigher implements EntryWeigher { - INSTANCE; + static final class EntryWeigherView implements EntryWeigher, Serializable { + static final long serialVersionUID = 1; + final Weigher weigher; + + EntryWeigherView(Weigher weigher) { + checkNotNull(weigher); + this.weigher = weigher; + } - @Override - public int weightOf(Object key, Object value) { - return 1; + @Override + public int weightOf(K key, V value) { + return weigher.weightOf(value); + } } - } - enum SingletonWeigher implements Weigher { - INSTANCE; + enum SingletonEntryWeigher implements EntryWeigher { + INSTANCE; - @Override - public int weightOf(Object value) { - return 1; + @Override + public int weightOf(Object key, Object value) { + return 1; + } } - } - enum ByteArrayWeigher implements Weigher { - INSTANCE; + enum SingletonWeigher implements Weigher { + INSTANCE; - @Override - public int weightOf(byte[] value) { - return value.length; + @Override + public int weightOf(Object value) { + return 1; + } } - } - - enum IterableWeigher implements Weigher> { - INSTANCE; - - @Override - public int weightOf(Iterable values) { - if (values instanceof Collection) { - return ((Collection) values).size(); - } - int size = 0; - for (Object value : values) { - size++; + + enum ByteArrayWeigher implements Weigher { + INSTANCE; + + @Override + public int weightOf(byte[] value) { + return value.length; + } + } + + enum IterableWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(Iterable values) { + if (values instanceof Collection) { + return ((Collection) values).size(); + } + int size = 0; + for (Object value : values) { + size++; + } + return size; } - return size; } - } - enum CollectionWeigher implements Weigher> { - INSTANCE; + enum CollectionWeigher implements Weigher> { + INSTANCE; - @Override - public int weightOf(Collection values) { - return values.size(); + @Override + public int weightOf(Collection values) { + return values.size(); + } } - } - enum ListWeigher implements Weigher> { - INSTANCE; + enum ListWeigher implements Weigher> { + INSTANCE; - @Override - public int weightOf(List values) { - return values.size(); + @Override + public int weightOf(List values) { + return values.size(); + } } - } - enum SetWeigher implements Weigher> { - INSTANCE; + enum SetWeigher implements Weigher> { + INSTANCE; - @Override - public int weightOf(Set values) { - return values.size(); + @Override + public int weightOf(Set values) { + return values.size(); + } } - } - enum MapWeigher implements Weigher> { - INSTANCE; + enum MapWeigher implements Weigher> { + INSTANCE; - @Override - public int weightOf(Map values) { - return values.size(); + @Override + public int weightOf(Map values) { + return values.size(); + } } - } } diff --git a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java index 5e0e55095..130edd41c 100644 --- a/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java +++ b/src/main/java/mssql/googlecode/concurrentlinkedhashmap/package-info.java @@ -1,29 +1,28 @@ /* - * Copyright 2011 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a - * copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations - * under the License. + * Copyright 2011 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and limitations under the + * License. */ /** * This package contains an implementation of a bounded {@link java.util.concurrent.ConcurrentMap} data structure. *

- * {@link Weigher} is a simple interface for determining how many units of capacity an entry consumes. Depending on which concrete Weigher class is - * used, an entry may consume a different amount of space within the cache. The {@link Weighers} class provides utility methods for obtaining the most - * common kinds of implementations. + * {@link Weigher} is a simple interface for determining how many units of capacity an entry consumes. Depending on + * which concrete Weigher class is used, an entry may consume a different amount of space within the cache. The + * {@link Weighers} class provides utility methods for obtaining the most common kinds of implementations. *

- * {@link EvictionListener} provides the ability to be notified when an entry is evicted from the map. An eviction occurs when the entry was - * automatically removed due to the map exceeding a capacity threshold. It is not called when an entry was explicitly removed. + * {@link EvictionListener} provides the ability to be notified when an entry is evicted from the map. An eviction + * occurs when the entry was automatically removed due to the map exceeding a capacity threshold. It is not called when + * an entry was explicitly removed. *

* The {@link ConcurrentLinkedHashMap} class supplies an efficient, scalable, thread-safe, bounded map. As with the - * Java Collections Framework the "Concurrent" prefix is used to indicate that the map is not governed by a single exclusion lock. + * Java Collections Framework the "Concurrent" prefix is used to indicate that the map is not governed by a + * single exclusion lock. * - * @see http://code.google.com/p/concurrentlinkedhashmap/ + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ */ package mssql.googlecode.concurrentlinkedhashmap; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java index eb73fd3ff..8fbdeff2c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java @@ -254,28 +254,26 @@ public void testGetObjectAsLocalDateTime() throws SQLException { try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { TimeZone prevTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); - + // a local date/time that does not actually exist because of Daylight Saving Time final String testValueDate = "2018-03-11"; final String testValueTime = "02:00:00.1234567"; final String testValueDateTime = testValueDate + "T" + testValueTime; - - stmt.executeUpdate( - "CREATE TABLE " + tableName + " (id INT PRIMARY KEY, dt2 DATETIME2)"); - stmt.executeUpdate( - "INSERT INTO " + tableName + " (id, dt2) VALUES (1, '" + testValueDateTime + "')"); + + stmt.executeUpdate("CREATE TABLE " + tableName + " (id INT PRIMARY KEY, dt2 DATETIME2)"); + stmt.executeUpdate("INSERT INTO " + tableName + " (id, dt2) VALUES (1, '" + testValueDateTime + "')"); try (ResultSet rs = stmt.executeQuery("SELECT dt2 FROM " + tableName + " WHERE id=1")) { rs.next(); - + LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); LocalDateTime actualLocalDateTime = rs.getObject(1, LocalDateTime.class); assertEquals(expectedLocalDateTime, actualLocalDateTime); - + LocalDate expectedLocalDate = LocalDate.parse(testValueDate); LocalDate actualLocalDate = rs.getObject(1, LocalDate.class); assertEquals(expectedLocalDate, actualLocalDate); - + LocalTime expectedLocalTime = LocalTime.parse(testValueTime); LocalTime actualLocalTime = rs.getObject(1, LocalTime.class); assertEquals(expectedLocalTime, actualLocalTime);