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(); }