diff --git a/pom.xml b/pom.xml
index 7a935aa..263cf07 100755
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.javax0.license3j
license3j
- 3.1.0
+ 3.1.1
jar
License3j
diff --git a/readme.md b/readme.md
index 3f78c44..67d636d 100644
--- a/readme.md
+++ b/readme.md
@@ -324,6 +324,12 @@ The type is written in all capital letters as listed above `BINARY`,
value of the feature. The type along with the separating `:` can be
missing in case it is `STRING`.
+When a `DATE` feature is converted to and from text then the actual
+value should be interpreted as time zone independent value. (Note that
+there was a bug in 3.X.X releases prior version 3.1.1 that used the
+local time zone to interpret text representation of the date/time
+values.)
+
The values are encoded as text in a human-readable and editable way.
When a value cannot fit on a single line, for example, a multi-line
string then the feature value starts with the characters `<<` and it is
diff --git a/src/main/java/javax0/license3j/Feature.java b/src/main/java/javax0/license3j/Feature.java
index 280792d..6b20ccf 100644
--- a/src/main/java/javax0/license3j/Feature.java
+++ b/src/main/java/javax0/license3j/Feature.java
@@ -11,6 +11,7 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
+import java.util.TimeZone;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -39,12 +40,12 @@
*/
public class Feature {
private static final String[] DATE_FORMAT =
- {"yyyy-MM-dd HH:mm:ss.SSS",
- "yyyy-MM-dd HH:mm:ss",
- "yyyy-MM-dd HH:mm",
- "yyyy-MM-dd HH",
- "yyyy-MM-dd"
- };
+ {"yyyy-MM-dd HH:mm:ss.SSS",
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd HH:mm",
+ "yyyy-MM-dd HH",
+ "yyyy-MM-dd"
+ };
private static final int VARIABLE_LENGTH = -1;
private final String name;
private final Type type;
@@ -56,14 +57,20 @@ private Feature(String name, Type type, byte[] value) {
this.value = value;
}
+ private static SimpleDateFormat getUTCDateFormat(String format){
+ final var simpleDateFormat = new SimpleDateFormat(format);
+ simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return simpleDateFormat;
+ }
+
private static String dateFormat(Object date) {
- return new SimpleDateFormat(DATE_FORMAT[0]).format(date);
+ return getUTCDateFormat(DATE_FORMAT[0]).format(date);
}
private static Date dateParse(String date) {
for (var format : DATE_FORMAT) {
try {
- return new SimpleDateFormat(format).parse(date);
+ return getUTCDateFormat(format).parse(date);
} catch (ParseException ignored) {
}
}
@@ -144,8 +151,8 @@ public byte[] serialized() {
final var nameLength = Integer.BYTES + nameBuffer.length;
final var valueLength = type.fixedSize == VARIABLE_LENGTH ? Integer.BYTES + value.length : type.fixedSize;
final var buffer = ByteBuffer.allocate(typeLength + nameLength + valueLength)
- .putInt(type.serialized)
- .putInt(nameBuffer.length);
+ .putInt(type.serialized)
+ .putInt(nameBuffer.length);
if (type.fixedSize == VARIABLE_LENGTH) {
buffer.putInt(value.length);
}
@@ -293,56 +300,56 @@ public Date getDate() {
private enum Type {
BINARY(1, VARIABLE_LENGTH,
- Feature::getBinary,
- (name, value) -> Create.binaryFeature(name, (byte[]) value),
- ba -> Base64.getEncoder().encodeToString((byte[]) ba), enc -> Base64.getDecoder().decode(enc)),
+ Feature::getBinary,
+ (name, value) -> Create.binaryFeature(name, (byte[]) value),
+ ba -> Base64.getEncoder().encodeToString((byte[]) ba), enc -> Base64.getDecoder().decode(enc)),
STRING(2, VARIABLE_LENGTH,
- Feature::getString,
- (name, value) -> Create.stringFeature(name, (String) value),
- Object::toString, s -> s),
+ Feature::getString,
+ (name, value) -> Create.stringFeature(name, (String) value),
+ Object::toString, s -> s),
BYTE(3, Byte.BYTES,
- Feature::getByte,
- (name, value) -> Create.byteFeature(name, (Byte) value),
- b -> String.format("0x%02X", (byte) (Byte) b), NumericParser.Byte::parse),
+ Feature::getByte,
+ (name, value) -> Create.byteFeature(name, (Byte) value),
+ b -> String.format("0x%02X", (byte) (Byte) b), NumericParser.Byte::parse),
SHORT(4, Short.BYTES,
- Feature::getShort,
- (name, value) -> Create.shortFeature(name, (Short) value),
- Object::toString, NumericParser.Short::parse),
+ Feature::getShort,
+ (name, value) -> Create.shortFeature(name, (Short) value),
+ Object::toString, NumericParser.Short::parse),
INT(5, Integer.BYTES,
- Feature::getInt,
- (name, value) -> Create.intFeature(name, (Integer) value),
- Object::toString, NumericParser.Int::parse),
+ Feature::getInt,
+ (name, value) -> Create.intFeature(name, (Integer) value),
+ Object::toString, NumericParser.Int::parse),
LONG(6, Long.BYTES,
- Feature::getLong,
- (name, value) -> Create.longFeature(name, (Long) value),
- Object::toString, NumericParser.Long::parse),
+ Feature::getLong,
+ (name, value) -> Create.longFeature(name, (Long) value),
+ Object::toString, NumericParser.Long::parse),
FLOAT(7, Float.BYTES,
- Feature::getFloat,
- (name, value) -> Create.floatFeature(name, (Float) value),
- Object::toString, Float::parseFloat),
+ Feature::getFloat,
+ (name, value) -> Create.floatFeature(name, (Float) value),
+ Object::toString, Float::parseFloat),
DOUBLE(8, Double.BYTES,
- Feature::getDouble,
- (name, value) -> Create.doubleFeature(name, (Double) value),
- Object::toString, Double::parseDouble),
+ Feature::getDouble,
+ (name, value) -> Create.doubleFeature(name, (Double) value),
+ Object::toString, Double::parseDouble),
BIGINTEGER(9, VARIABLE_LENGTH,
- Feature::getBigInteger,
- (name, value) -> Create.bigIntegerFeature(name, (BigInteger) value),
- Object::toString, BigInteger::new),
+ Feature::getBigInteger,
+ (name, value) -> Create.bigIntegerFeature(name, (BigInteger) value),
+ Object::toString, BigInteger::new),
BIGDECIMAL(10, VARIABLE_LENGTH,
- Feature::getBigDecimal,
- (name, value) -> Create.bigDecimalFeature(name, (BigDecimal) value),
- Object::toString, BigDecimal::new),
+ Feature::getBigDecimal,
+ (name, value) -> Create.bigDecimalFeature(name, (BigDecimal) value),
+ Object::toString, BigDecimal::new),
DATE(11, Long.BYTES,
- Feature::getDate,
- (name, value) -> Create.dateFeature(name, (Date) value),
- Feature::dateFormat, Feature::dateParse),
+ Feature::getDate,
+ (name, value) -> Create.dateFeature(name, (Date) value),
+ Feature::dateFormat, Feature::dateParse),
UUID(12, 2 * Long.BYTES,
- Feature::getUUID,
- (name, value) -> Create.uuidFeature(name, (java.util.UUID) value),
- Object::toString, java.util.UUID::fromString);
+ Feature::getUUID,
+ (name, value) -> Create.uuidFeature(name, (java.util.UUID) value),
+ Object::toString, java.util.UUID::fromString);
final int fixedSize;
final int serialized;
@@ -426,17 +433,17 @@ public static Feature bigDecimalFeature(String name, BigDecimal value) {
notNull(value);
byte[] b = value.unscaledValue().toByteArray();
return new Feature(name, Type.BIGDECIMAL, ByteBuffer.allocate(Integer.BYTES + b.length)
- .put(b)
- .putInt(value.scale())
- .array());
+ .put(b)
+ .putInt(value.scale())
+ .array());
}
public static Feature uuidFeature(String name, java.util.UUID value) {
notNull(value);
return new Feature(name, Type.UUID, ByteBuffer.allocate(2 * Long.BYTES)
- .putLong(value.getLeastSignificantBits())
- .putLong(value.getMostSignificantBits())
- .array());
+ .putLong(value.getLeastSignificantBits())
+ .putLong(value.getMostSignificantBits())
+ .array());
}
public static Feature dateFeature(String name, Date value) {
@@ -470,7 +477,7 @@ public static Feature from(String s) {
public static Feature from(byte[] serialized) {
if (serialized.length < Integer.BYTES * 2) {
throw new IllegalArgumentException("Cannot load feature from a byte array that has "
- + serialized.length + " bytes which is < " + (2 * Integer.BYTES));
+ + serialized.length + " bytes which is < " + (2 * Integer.BYTES));
}
var bb = ByteBuffer.wrap(serialized);
var typeSerialized = bb.getInt();
@@ -499,7 +506,7 @@ public static Feature from(byte[] serialized) {
}
if (bb.remaining() > 0) {
throw new IllegalArgumentException("Cannot load feature from a byte array that has "
- + serialized.length + " bytes which is " + bb.remaining() + " bytes too long");
+ + serialized.length + " bytes which is " + bb.remaining() + " bytes too long");
}
final var name = new String(nameBuffer, StandardCharsets.UTF_8);
return new Feature(name, type, value);
diff --git a/src/test/java/javax0/license3j/LicenseTest.java b/src/test/java/javax0/license3j/LicenseTest.java
index eb92862..4450431 100644
--- a/src/test/java/javax0/license3j/LicenseTest.java
+++ b/src/test/java/javax0/license3j/LicenseTest.java
@@ -43,7 +43,7 @@ void licenseSerializeAndDeserialize() {
final var restored = License.Create.from(buffer);
Assertions.assertEquals("Peter Verhas", restored.get("owner").getString());
Assertions.assertEquals(now, restored.get("expiry").getDate());
- Assertions.assertEquals("expiry:DATE=2018-12-17 12:55:19.295\n" +
+ Assertions.assertEquals("expiry:DATE=2018-12-17 11:55:19.295\n" +
"owner=Peter Verhas\n" +
"template=<>\n" +
@@ -64,7 +64,7 @@ void licenseStringifyAndDestringify() {
final var restored = License.Create.from(string);
Assertions.assertEquals("Peter Verhas", restored.get("owner").getString());
Assertions.assertEquals(now, restored.get("expiry").getDate());
- Assertions.assertEquals("expiry:DATE=2018-12-17 12:55:19.295\n" +
+ Assertions.assertEquals("expiry:DATE=2018-12-17 11:55:19.295\n" +
"owner=Peter Verhas\n" +
"template=<>\n" +
diff --git a/src/test/java/javax0/license3j/TestFeature.java b/src/test/java/javax0/license3j/TestFeature.java
index 3458c53..8fbdbb3 100644
--- a/src/test/java/javax0/license3j/TestFeature.java
+++ b/src/test/java/javax0/license3j/TestFeature.java
@@ -474,17 +474,17 @@ public void testBigDecimalFromString() {
@Test
@DisplayName("date feature is converted from string")
public void testDateFromString() {
- final var sut1 = Feature.Create.from("name:DATE=2018-12-17 12:55:19.295");
+ final var sut1 = Feature.Create.from("name:DATE=2018-12-17 11:55:19.295");
Assertions.assertEquals("name", sut1.name());
Assertions.assertTrue(sut1.isDate());
Assertions.assertEquals(new Date(1545047719295L), sut1.getDate());
- final var sut2 = Feature.Create.from("name:DATE=2018-12-17 12:55:19");
+ final var sut2 = Feature.Create.from("name:DATE=2018-12-17 11:55:19");
Assertions.assertEquals("name", sut2.name());
Assertions.assertTrue(sut2.isDate());
Assertions.assertEquals(new Date(1545047719000L), sut2.getDate());
- final var sut3 = Feature.Create.from("name:DATE=2018-12-17 12:55");
+ final var sut3 = Feature.Create.from("name:DATE=2018-12-17 11:55");
Assertions.assertEquals("name", sut3.name());
Assertions.assertTrue(sut3.isDate());
Assertions.assertEquals(new Date(1545047700000L), sut3.getDate());
@@ -560,7 +560,7 @@ public void testBigdecimalToString() {
@DisplayName("Date feature is converted to string")
public void testDateToString() {
final var sut = Feature.Create.dateFeature("now", new Date(1545047719295L));
- Assertions.assertEquals("now:DATE=2018-12-17 12:55:19.295", sut.toString());
+ Assertions.assertEquals("now:DATE=2018-12-17 11:55:19.295", sut.toString());
}
}
diff --git a/src/test/java/javax0/license3j/hardware/TestInterfaceSelector.java b/src/test/java/javax0/license3j/hardware/TestInterfaceSelector.java
index be2a2d6..9293197 100644
--- a/src/test/java/javax0/license3j/hardware/TestInterfaceSelector.java
+++ b/src/test/java/javax0/license3j/hardware/TestInterfaceSelector.java
@@ -24,7 +24,7 @@ boolean isSpecial(NetworkInterface netIf) {
};
}
- private static IfTest test(final String ifName) throws NoSuchFieldException, InstantiationException, IllegalAccessException, InvocationTargetException {
+ private static IfTest test(final String ifName) throws NoSuchFieldException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return new IfTest(ifName);
}
@@ -32,8 +32,8 @@ private static NetworkInterface mockInterface(final String name)
throws NoSuchFieldException,
IllegalAccessException,
InvocationTargetException,
- InstantiationException {
- Constructor constructor = NetworkInterface.class.getDeclaredConstructors()[0];
+ InstantiationException, NoSuchMethodException {
+ Constructor constructor = NetworkInterface.class.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
NetworkInterface ni = (NetworkInterface) constructor.newInstance();
Field field = NetworkInterface.class.getDeclaredField("displayName");
@@ -44,51 +44,51 @@ private static NetworkInterface mockInterface(final String name)
@Test
@DisplayName("If there is no regular expression defined as allowed nor as denied then everything is allowed")
- public void testJustAnyName() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void testJustAnyName() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("just-any-name").isUsable();
}
@Test
@DisplayName("If there is a regular expression allowing an interface then an interface matching the regex will be allowed")
- public void explicitlyAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void explicitlyAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("allowed").allowed("allowed").isUsable();
}
@Test
@DisplayName("If there is a regular expression allowing an interface then an interface NOT matching the regex will be denied")
- public void explicitlyNotAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void explicitlyNotAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("not allowed").allowed("allowed").isDenied();
}
@Test
@DisplayName("If there is a regular expression denying an interface then an interface matching the regex will be denied")
- public void explicitlyDenied() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void explicitlyDenied() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("denied").allowed("allowed").denied("denied").isDenied();
}
@Test
@DisplayName("If there is a regular expression denying an interface then an interface matching the regex will be denied EVEN if it matches an regex allowing it")
- public void explicitlyDeniedEvenIfAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void explicitlyDeniedEvenIfAllowed() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("denied").allowed("denied").denied("denied").isDenied();
}
@Test
@DisplayName("If there is a regular expression allowing it and the denying regex does not match then it is allowed")
- public void explicitlyAllowedNotDenied() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ public void explicitlyAllowedNotDenied() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
test("allowed").allowed("allowed").denied("denied").isUsable();
}
@Test
@DisplayName("If there is a regular expression allowing it and the denying regexes do not match then it is allowed")
- public void explicitlyAllowedNotDeniedByAny() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
- test("allowed").allowed("allowed").denied("denied","denied2").isUsable();
+ public void explicitlyAllowedNotDeniedByAny() throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
+ test("allowed").allowed("allowed").denied("denied", "denied2").isUsable();
}
private static class IfTest {
NetworkInterface ni;
Network.Interface.Selector sut;
- IfTest(String name) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
+ IfTest(String name) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException {
ni = mockInterface(name);
sut = newSut();
}