Skip to content

Commit

Permalink
DEV-23981: Make public release b2-sdk-java with event notifications f…
Browse files Browse the repository at this point in the history
…eature (#2)

Merging private changes to public SDK

Added support for Event Notifications
Lifecycle rule for cancelling unfinished large files
Performance improvements
  • Loading branch information
kmadsenbz authored Apr 12, 2024
1 parent d46d348 commit 91e0b73
Show file tree
Hide file tree
Showing 83 changed files with 5,105 additions and 515 deletions.
47 changes: 46 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,50 @@
# Changelog

## [Unreleased] - TBD
### Added
* Added support to specify B2Json union types using annotations. Annotation support for union types is required because
Java records do not support inheritance. Example usage:
```java
@B2Json.union(typeField = "type")
@B2Json.unionSubtypes({
@B2Json.unionSubtypes.type(name = "email", clazz = Email.class),
@B2Json.unionSubtypes.type(name = "sms", clazz = Sms.class)
})
sealed interface Message permits Email, Sms {
String subject();
}

@B2Json.type
private record Email(@B2Json.required String subject, @B2Json.required String email) implements Message {
}

@B2Json.type
private record Sms(@B2Json.required String subject, @B2Json.required String phoneNumber) implements Message {
}
```
* Added `@B2Json.type` annotation that can be used with Java records. Using `@B2Json.type` allows for the implicit
Java constructor of Java records to not require the `@B2Json.constructor` annotation. Example usage:
```java
@B2Json.type
record Point(@B2Json.required int x, @B2Json.required int y) { }
```
* Optimized B2DateTimeUtil.formatFguidDateTime
* Reduced memory allocation for small input when deserializing byte[] to JSON
* Reduced lock contention in B2Clock
* Added support for B2 Event Notifications
* Added B2Json `fromJson` methods that take a `java.io.Reader` as input for JSON
* Updated B2Json `fromJson` methods to utilize a BufferedReader when deserializing JSON for performance improvement
* Added B2StorageClient.storePartsForLargeFile
* Added support for daysFromStartingToCancelingUnfinishedLargeFiles to B2LifecycleRule
* Reduced lock contention in B2AccountAuthorizationCache
* Added the `serializedName` annotation to rename the serialized Json member name
* Added support for AtomicLongArray in B2Json
* Reduced lock contention in B2Json
* Updated internal python for building to python3
* Added support for custom upload timestamps

### Fixed
* Fixed union types to ignore extra and discarded fields when deserializing JSON to Java objects

## [6.1.1] - 2022-11-10
### Added
Expand All @@ -11,8 +55,9 @@
* Fixed B2ListFilesIterableBase assuming a response with 0 results was the end. It now looks for
`nextFileName` being null to indicate the end.

## [6.1.0] - 2022-09-19
### [6.1.0] - 2022-09-19
### Added
* Added support for custom upload timestamps
* Added support for Java 8's `-parameters` option so constructor parameters do not need to be reiterated in `B2Json.constructor#params`
* Added `fileLockEnabled` to `B2UpdateBucketRequest` to support enabling file lock on existing buckets

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// License https://www.backblaze.com/using_b2_code.html

plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}

nexusPublishing {
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/b2sdk.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ tasks.build {
val checkCode by tasks.registering(Exec::class) {
val script = rootProject.layout.projectDirectory.file("check_code").asFile.absolutePath
val targetDir = layout.projectDirectory.dir("src/main").asFile.absolutePath
commandLine("python", script, targetDir)
commandLine("python3", script, targetDir)
}
tasks.classes {
dependsOn(checkCode)
Expand Down
2 changes: 1 addition & 1 deletion check_code
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3

######################################################################
#
Expand Down
29 changes: 29 additions & 0 deletions core-test-jdk17/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024, Backblaze Inc. All Rights Reserved.
// License https://www.backblaze.com/using_b2_code.html

plugins {
`java-library`
b2sdk
idea
}

description = "JDK 17 testing of b2-sdk-core"

b2sdk {
pomName.set("JDK 17 testing of b2-sdk-core")
description.set(project.description)
}

dependencies {
testImplementation(projects.b2SdkCore)
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}

tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
4 changes: 4 additions & 0 deletions core-test-jdk17/src/main/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*
* Copyright 2024, Backblaze Inc. All Rights Reserved.
* License https://www.backblaze.com/using_b2_code.html
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024, Backblaze Inc. All Rights Reserved.
* License https://www.backblaze.com/using_b2_code.html
*/
package com.backblaze.b2.json;

import com.backblaze.b2.util.B2BaseTest;
import org.junit.Test;

import java.lang.reflect.Constructor;

import static org.junit.Assert.*;

public class B2JsonDeserializationUtilTest extends B2BaseTest {

@Test
public void findConstructor_withJavaRecord() throws B2JsonException {
final Constructor<B2JsonRecord> constructor = B2JsonDeserializationUtil.findConstructor(B2JsonRecord.class);
assertNotNull(constructor);
}

@Test
public void findConstructor_withJavaRecordWithoutB2JsonTypeAnnotation() {
final B2JsonException exception = assertThrows(B2JsonException.class, () -> B2JsonDeserializationUtil.findConstructor(B2JsonRecordWithoutTypeAnnotation.class));
assertTrue(exception.getMessage().contains("has no constructor annotated with B2Json.constructor"));
}

@B2Json.type
public record B2JsonRecord(@B2Json.required String name) {

}

public record B2JsonRecordWithoutTypeAnnotation(@B2Json.required String name) {

}
}
211 changes: 211 additions & 0 deletions core-test-jdk17/src/test/java/com/backblaze/b2/json/B2JsonTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright 2024, Backblaze Inc. All Rights Reserved.
* License https://www.backblaze.com/using_b2_code.html
*/

package com.backblaze.b2.json;

import com.backblaze.b2.util.B2BaseTest;
import org.junit.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;

import static org.junit.Assert.*;

/**
* Unit tests for B2Json utilizing Java 17 features.
*/
public class B2JsonTest extends B2BaseTest {

private static final B2Json b2Json = B2Json.get();

@Test
public void testRecord() throws B2JsonException {
final String json = """
{
"@d": "goodbye",
"a": 41,
"b": "hello"
}""";
final RecordContainer obj = new RecordContainer(41, "hello", "goodbye");
assertEquals(json, b2Json.toJson(obj));
assertEquals(obj, b2Json.fromJson(json, RecordContainer.class));

final String alternateJson = """
{
"a": 41,
"b": "hello",
"\\u0040d": "goodbye"
}
""";
assertEquals(obj, b2Json.fromJson(alternateJson, RecordContainer.class));
}

@Test
public void testRecordUnionWithTypeFieldLast() throws IOException, B2JsonException {
final String json = """
{
"a": 5,
"b": null,
"type": "a"
}""";
checkDeserializeSerialize(json, UnionRecordAZ.class);
}

@B2Json.union(typeField = "type")
interface UnionRecordWithOutSubtypes {
}

@Test
public void testRecordUnionWithNoSubtypes() {
final String json = """
{
"a": 5,
"b": null,
"type": "a"
}""";
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.fromJson(json, UnionRecordWithOutSubtypes.class));
assertEquals("union base class interface com.backblaze.b2.json.B2JsonTest$UnionRecordWithOutSubtypes does not have a method getUnionTypeMap", exception.getMessage());
}

@B2Json.union(typeField = "type")
sealed interface UnionRecordNoSubtypes permits SubtypeRecord {
}

@B2Json.type
private record SubtypeRecord(@B2Json.required int a,
@B2Json.optional Set<Integer> b) implements UnionRecordNoSubtypes {
}

@Test
public void testRecordUnionWithNoSubtypes_toJson() {
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.toJson(new SubtypeRecord(5, null)));
assertEquals("interface com.backblaze.b2.json.B2JsonTest$UnionRecordNoSubtypes has B2Json.union annotation, but does not have @B2Json.unionSubtypes annotation", exception.getMessage());
}

@B2Json.union(typeField = "type")
@B2Json.unionSubtypes({
@B2Json.unionSubtypes.type(name = "a", clazz = UnionRecordWithUnknownType.definedSubtype.class)
})
interface UnionRecordWithUnknownType {
@B2Json.type
record definedSubtype(@B2Json.required int a,
@B2Json.optional Set<Integer> b) implements UnionRecordWithUnknownType {
}

@B2Json.type
record UndefinedSubtype(@B2Json.required int a,
@B2Json.optional Set<Integer> b) implements UnionRecordWithUnknownType {

}
}

@Test
public void testRecordUnionWithNoMatchingSubtype_fromJson() {
final String json = """
{
"a": 5,
"b": null,
"type": "b"
}""";
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.fromJson(json, UnionRecordWithUnknownType.class));
assertEquals("unknown 'type' in UnionRecordWithUnknownType: 'b'", exception.getMessage());
}

@Test
public void testRecordUnionWithNoMatchingSubtype_toJson() {
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.toJson(new UnionRecordWithUnknownType.UndefinedSubtype(5, null)));
assertEquals("interface com.backblaze.b2.json.B2JsonTest$UnionRecordWithUnknownType does not contain mapping for class com.backblaze.b2.json.B2JsonTest$UnionRecordWithUnknownType$UndefinedSubtype in the @B2Json.unionSubtypes annotation", exception.getMessage());
}

@B2Json.union(typeField = "type")
@B2Json.unionSubtypes({
})
interface UnionRecordWithEmptySubtypes {
}

@B2Json.type
private record SubclassRecordWithEmptySubtypes(@B2Json.required int a,
@B2Json.optional Set<Integer> b) implements UnionRecordWithEmptySubtypes {
}

@Test
public void testRecordUnionWithEmptySubtypes_fromJson() {
final String json = """
{
"a": 5,
"b": null,
"type": "b"
}""";
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.fromJson(json, SubclassRecordWithEmptySubtypes.class));
assertEquals("UnionRecordWithEmptySubtypes - at least one type must be configured set in @B2Json.unionSubtypes", exception.getMessage());
}

@Test
public void testRecordUnionWithEmptySubtypes_toJson() {
final B2JsonException exception = assertThrows(B2JsonException.class, () -> b2Json.toJson(new SubclassRecordWithEmptySubtypes(5, null)));
assertEquals("UnionRecordWithEmptySubtypes - at least one type must be configured set in @B2Json.unionSubtypes", exception.getMessage());
}

@B2Json.union(typeField = "type")
@B2Json.unionSubtypes({
@B2Json.unionSubtypes.type(name = "a", clazz = SubclassRecordA.class),
@B2Json.unionSubtypes.type(name = "z", clazz = SubclassRecordZ.class)
})
sealed interface UnionRecordAZ permits SubclassRecordA, SubclassRecordZ {
}

@B2Json.type
private record SubclassRecordA(@B2Json.required int a, @B2Json.optional Set<Integer> b) implements UnionRecordAZ {
}

@B2Json.type
private record SubclassRecordZ(@B2Json.required String z) implements UnionRecordAZ {
}

@Test
public void testRecordContainingRecord() throws B2JsonException, IOException {
final String json = """
{
"age": 10,
"name": "Sam",
"record": {
"@d": "b",
"a": 5,
"b": "test"
}
}""";
checkDeserializeSerialize(json, RecordContainingRecord.class);
}

@B2Json.type
record RecordContainingRecord(@B2Json.required int age,
@B2Json.optional String name,
@B2Json.optional RecordContainer record) {
}

record RecordContainer(@B2Json.required int a,
@B2Json.optional String b,
@B2Json.ignored int c,
@B2Json.optional @B2Json.serializedName(value = "@d") String d) {
@B2Json.constructor
RecordContainer(int a, String b, String d) {
this(a, b, 5, d);
}
}

private <T> void checkDeserializeSerialize(String json, Class<T> clazz) throws IOException, B2JsonException {
final T obj = b2Json.fromJson(json, clazz);
assertEquals(json, b2Json.toJson(obj));

final byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
final T obj2 = b2Json.fromJson(bytes, clazz);
assertArrayEquals(bytes, b2Json.toJsonUtf8Bytes(obj2));

final T obj3 = b2Json.fromJson(bytes, clazz);
final byte[] bytesWithNewline = (json + "\n").getBytes(StandardCharsets.UTF_8);
assertArrayEquals(bytesWithNewline, b2Json.toJsonUtf8BytesWithNewline(obj3));
}
}
Loading

0 comments on commit 91e0b73

Please sign in to comment.