├── .idea ├── .gitignore ├── vcs.xml ├── google-java-format.xml ├── encodings.xml ├── modules.xml ├── misc.xml ├── libraries │ ├── Maven__org_opentest4j_opentest4j_1_2_0.xml │ ├── Maven__org_apiguardian_apiguardian_api_1_1_0.xml │ ├── Maven__org_junit_jupiter_junit_jupiter_api_5_7_0.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_core_2_12_1.xml │ ├── Maven__org_junit_jupiter_junit_jupiter_engine_5_7_0.xml │ ├── Maven__org_junit_platform_junit_platform_engine_1_7_0.xml │ └── Maven__org_junit_platform_junit_platform_commons_1_7_0.xml ├── compiler.xml ├── jarRepositories.xml ├── record-util.iml └── inspectionProfiles │ └── Project_Default.xml ├── src ├── main │ └── java │ │ ├── module-info.java │ │ └── com │ │ └── github │ │ └── forax │ │ └── recordutil │ │ ├── JSONParsing.java │ │ ├── WithTrait.java │ │ ├── WitherImpl.java │ │ ├── JSONTrait.java │ │ ├── TraitImpl.java │ │ ├── MapTrait.java │ │ └── Wither.java └── test │ └── java │ └── com │ └── github │ └── forax │ └── recordutil │ ├── WithTraitTest.java │ ├── WithShapeTest.java │ ├── MapShapeTest.java │ ├── JSONTraitTest.java │ ├── MapTraitTest.java │ └── WitherTest.java ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── maven.yml ├── README.md └── pom.xml /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/google-java-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * A module containing utility classes that sweeten the use of records. 3 | * 4 | * @see com.github.forax.recordutil.MapTrait 5 | * @see com.github.forax.recordutil.WithTrait 6 | * @see com.github.forax.recordutil.Wither 7 | */ 8 | module com.github.forax.recordutil { 9 | requires static com.fasterxml.jackson.core; 10 | 11 | exports com.github.forax.recordutil; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Maven target directory 26 | target/ 27 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_opentest4j_opentest4j_1_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_apiguardian_apiguardian_api_1_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_junit_jupiter_junit_jupiter_api_5_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_12_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_junit_jupiter_junit_jupiter_engine_5_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_junit_platform_junit_platform_engine_1_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_junit_platform_junit_platform_commons_1_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rémi Forax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | java: [ 16-ea ] 15 | name: Java ${{ matrix.java }} 16 | steps: 17 | - name: 'Check out repository' 18 | uses: actions/checkout@v2 19 | - name: 'Setup Java ${{ matrix.java }}' 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: ${{ matrix.java }} 23 | - name: 'Build with Maven' 24 | run: | 25 | mvn --batch-mode --no-transfer-progress verify 26 | - name: 'Release SNAPSHOT' 27 | if: github.event_name == 'push' && github.repository == 'forax/record-util' && github.ref == 'refs/heads/master' 28 | uses: marvinpinto/action-automatic-releases@latest 29 | with: 30 | automatic_release_tag: SNAPSHOT 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | prerelease: true 33 | title: "Release SNAPSHOT" 34 | files: | 35 | target/*.jar 36 | -------------------------------------------------------------------------------- /.idea/record-util.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # record-util 2 | Some utility classes around java records 3 | 4 | ### On the menu 5 | 6 | - **MapTrait** 7 | 8 | Transform any record to a `java.util.Map` just by implementing the interface `MapTrait` 9 | ```java 10 | record Person(String name, int age) implements MapTrait {} 11 | ... 12 | Map map = new Person("Bob", 42); 13 | ``` 14 | 15 | - **WithTrait** 16 | 17 | Implementing the interface `WithTrait` adds several methods `with` that allow to duplicate 18 | a record instance and update several record components in the process 19 | ```java 20 | record Person(String name, int age) implements WithTrait {} 21 | ... 22 | var bob = new Person("Bob", 42); 23 | var ana = bob.with("name", "Ana"); 24 | ``` 25 | 26 | - **Wither** 27 | 28 | A very fast but more cumbersome way to duplicate/update a record instance 29 | ```java 30 | record Person(String name, int age) {} 31 | ... 32 | private static final Wither wither = Wither.of(MethodHandles.lookup(), Person.class); 33 | ... 34 | var bob = new Person("Bob", 42); 35 | var ana = wither.with(bob, "name", "Ana"); 36 | ``` 37 | 38 | - **JSONTrait** 39 | 40 | Implementing the interface `JSONTrait` adds two methods `toJSON` and `toHumanReadableJSON`that 41 | enable to output a record instance using the JSON format 42 | ```java 43 | record Person(String name, int age) implements JSONTrait { } 44 | ... 45 | var person = new Person("Bob", 42); 46 | System.out.println(person.toHumanReadableJSON()); 47 | ``` 48 | 49 | `JSONTrait` also defines a method `parse(reader, recordType)` to decode a JSON file 50 | to a record instance 51 | ```java 52 | var reader = ... // a FileReader or a StringReader 53 | var person = JSONTrait.parse(reader, Person.class); 54 | ``` 55 | 56 | ### How to build 57 | ``` 58 | mvn package 59 | ``` 60 | -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/WithTraitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | public class WithTraitTest { 8 | @Test 9 | public void with1() { 10 | record Person(String name, int age) implements WithTrait {} 11 | var person = new Person("Bob", 42); 12 | assertEquals(new Person("Ana", 23), person.with("name", "Ana").with("age", 23)); 13 | } 14 | 15 | @Test 16 | public void with2() { 17 | record Person(String name, int age) implements WithTrait {} 18 | var person = new Person("Bob", 42); 19 | assertEquals(new Person("Ana", 23), person.with("age", 23, "name", "Ana")); 20 | } 21 | 22 | @Test 23 | public void with3() { 24 | record Point3D(int x, int y, int z) implements WithTrait {} 25 | var point = new Point3D(1, 2, 3); 26 | assertEquals(new Point3D(4, 7, -3), point.with("z", -3, "x", 4, "y", 7)); 27 | } 28 | 29 | @Test 30 | public void with4() { 31 | record Address(int number, String street, String city, String state, String country) 32 | implements WithTrait {} 33 | 34 | var address = new Address(13, "baker street", "san jose", "CA", "United States"); 35 | var address2 = address.with( 36 | "country", "Spain", 37 | "number", 354, 38 | "city", "Madrid", 39 | "state", "n/a"); 40 | assertEquals(new Address(354, "baker street", "Madrid", "n/a", "Spain"), address2); 41 | } 42 | 43 | @Test 44 | public void withAll() { 45 | record Address(int number, String street, String city, String state, String country) 46 | implements WithTrait {} 47 | 48 | var address = new Address(13, "baker street", "san jose", "CA", "United States"); 49 | var address2 = address.with( 50 | "country", "Spain", 51 | "number", 354, 52 | "city", "Madrid", 53 | "state", "n/a", 54 | "street", "5th avenue"); 55 | assertEquals(new Address(354, "5th avenue", "Madrid", "n/a", "Spain"), address2); 56 | } 57 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | com.github.forax.recordutil 9 | com.github.forax.recordutil 10 | 1.0-SNAPSHOT 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | 18 | com.fasterxml.jackson.core 19 | jackson-core 20 | 2.12.1 21 | provided 22 | true 23 | 24 | 25 | org.junit.jupiter 26 | junit-jupiter-api 27 | 5.7.0 28 | test 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter-engine 33 | 5.7.0 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 3.8.1 44 | 45 | 16 46 | 47 | 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-surefire-plugin 52 | 3.0.0-M5 53 | 54 | --add-modules com.fasterxml.jackson.core 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/WithShapeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import com.github.forax.recordutil.TraitImpl.WithShape; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodType; 9 | import java.util.stream.IntStream; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class WithShapeTest { 14 | record Person(String name, int age) { } 15 | 16 | private static final MethodHandle NAME, AGE, CONSTRUCTOR; 17 | 18 | static { 19 | var lookup = MethodHandles.lookup(); 20 | try { 21 | NAME = lookup.findVirtual(Person.class, "name", MethodType.methodType(String.class)); 22 | AGE = lookup.findVirtual(Person.class, "age", MethodType.methodType(int.class)); 23 | CONSTRUCTOR = lookup.findConstructor(Person.class, MethodType.methodType(void.class, String.class, int.class)); 24 | } catch (NoSuchMethodException | IllegalAccessException e) { 25 | throw new AssertionError(e); 26 | } 27 | } 28 | 29 | @Test 30 | public void getEmptySize() { 31 | var shape = new WithShape(0, CONSTRUCTOR); 32 | assertEquals(-1, shape.getSlot("foo")); 33 | assertEquals(-1, shape.getSlot("bar")); 34 | assertEquals(-1, shape.getSlot("baz")); 35 | } 36 | 37 | @Test 38 | public void getEmpty1() { 39 | var shape = new WithShape(1, CONSTRUCTOR); 40 | assertEquals(-1, shape.getSlot("foo")); 41 | assertEquals(-1, shape.getSlot("bar")); 42 | assertEquals(-1, shape.getSlot("baz")); 43 | } 44 | 45 | @Test 46 | public void getEmpty6() { 47 | var shape = new WithShape(6, CONSTRUCTOR); 48 | assertEquals(-1, shape.getSlot("foo")); 49 | assertEquals(-1, shape.getSlot("bar")); 50 | assertEquals(-1, shape.getSlot("baz")); 51 | } 52 | 53 | @Test 54 | public void getPut1() { 55 | var shape = new WithShape(1, CONSTRUCTOR); 56 | shape.put(0, "age", AGE); 57 | assertEquals(1, shape.size()); 58 | assertEquals(0, shape.getSlot("age")); 59 | assertEquals(AGE, shape.getValue(0)); 60 | } 61 | 62 | @Test 63 | public void getPut2() { 64 | var shape = new WithShape(2, CONSTRUCTOR); 65 | shape.put(0, "name", NAME); 66 | shape.put(1, "age", AGE); 67 | assertEquals(2, shape.size()); 68 | assertEquals(0, shape.getSlot("name")); 69 | assertEquals(1, shape.getSlot("age")); 70 | assertEquals(NAME, shape.getValue(0)); 71 | assertEquals(AGE, shape.getValue(1)); 72 | assertEquals(-1, shape.getSlot("baz")); 73 | assertEquals(-1, shape.getSlot("joy")); 74 | assertEquals(-1, shape.getSlot("love")); 75 | } 76 | 77 | @Test 78 | public void getKeyPutALot() { 79 | var capacity = 100_000; 80 | var shape = new WithShape(capacity, CONSTRUCTOR); 81 | IntStream.range(0, capacity).forEach(i -> shape.put(i, "" + i, i %2 == 0? NAME: AGE)); 82 | 83 | // hit 84 | IntStream.range(0, capacity).forEach(i -> assertEquals(i, shape.getSlot("" + i))); 85 | 86 | // miss 87 | IntStream.range(0, capacity).forEach(i -> assertEquals(-1, shape.getSlot("foo" + i))); 88 | } 89 | 90 | @Test 91 | public void getIndexPutALot() { 92 | var capacity = 100_000; 93 | var shape = new WithShape(capacity, CONSTRUCTOR); 94 | IntStream.range(0, capacity).forEach(i -> shape.put(i, "" + i, i %2 == 0? NAME: AGE)); 95 | 96 | // linear scan 97 | IntStream.range(0, capacity).forEach(i -> assertEquals(i %2 == 0? NAME: AGE, shape.getValue(i))); 98 | } 99 | 100 | @Test 101 | public void size() { 102 | var shape = new WithShape(77, CONSTRUCTOR); 103 | assertEquals(77, shape.size()); 104 | } 105 | } -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/MapShapeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import com.github.forax.recordutil.TraitImpl.MapShape; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodType; 9 | import java.util.stream.IntStream; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | public class MapShapeTest { 14 | private static final MethodHandle STRING_LENGTH; 15 | private static final MethodHandle STRING_EQUALS; 16 | static { 17 | var lookup = MethodHandles.lookup(); 18 | try { 19 | STRING_LENGTH = lookup.findVirtual(String.class, "length", MethodType.methodType(int.class)); 20 | STRING_EQUALS = lookup.findVirtual(String.class, "equals", MethodType.methodType(boolean.class, Object.class)); 21 | } catch (NoSuchMethodException | IllegalAccessException e) { 22 | throw new AssertionError(e); 23 | } 24 | } 25 | 26 | @Test 27 | public void getEmptySize() { 28 | var shape = new MapShape(0); 29 | assertNull(shape.getValue("foo")); 30 | assertNull(shape.getValue("bar")); 31 | assertNull(shape.getValue("baz")); 32 | } 33 | 34 | @Test 35 | public void getEmpty1() { 36 | var shape = new MapShape(1); 37 | assertNull(shape.getValue("foo")); 38 | assertNull(shape.getValue("bar")); 39 | assertNull(shape.getValue("baz")); 40 | } 41 | 42 | @Test 43 | public void getEmpty6() { 44 | var shape = new MapShape(6); 45 | assertNull(shape.getValue("foo")); 46 | assertNull(shape.getValue("bar")); 47 | assertNull(shape.getValue("baz")); 48 | } 49 | 50 | @Test 51 | public void getPut1() { 52 | var shape = new MapShape(1); 53 | shape.put(0, "hello", STRING_LENGTH); 54 | assertEquals(1, shape.size()); 55 | assertEquals(STRING_LENGTH, shape.getValue("hello")); 56 | assertEquals("hello", shape.getKey(0)); 57 | assertEquals(STRING_LENGTH, shape.getValue(0)); 58 | } 59 | 60 | @Test 61 | public void getPut2() { 62 | var shape = new MapShape(2); 63 | shape.put(0, "foo", STRING_LENGTH); 64 | shape.put(1, "bar", STRING_EQUALS); 65 | assertEquals(2, shape.size()); 66 | assertEquals(STRING_LENGTH, shape.getValue("foo")); 67 | assertEquals(STRING_EQUALS, shape.getValue("bar")); 68 | assertNull(shape.getValue("baz")); 69 | assertNull(shape.getValue("joy")); 70 | assertNull(shape.getValue("love")); 71 | assertEquals("foo", shape.getKey(0)); 72 | assertEquals(STRING_LENGTH, shape.getValue(0)); 73 | assertEquals("bar", shape.getKey(1)); 74 | assertEquals(STRING_EQUALS, shape.getValue(1)); 75 | } 76 | 77 | @Test 78 | public void getKeyPutALot() { 79 | var capacity = 100_000; 80 | var shape = new MapShape(capacity); 81 | IntStream.range(0, capacity).forEach(i -> shape.put(i, "" + i, i %2 == 0? STRING_LENGTH: STRING_EQUALS)); 82 | 83 | // hit 84 | IntStream.range(0, capacity).forEach(i -> assertEquals(i %2 == 0? STRING_LENGTH: STRING_EQUALS, shape.getValue("" + i))); 85 | 86 | // miss 87 | IntStream.range(0, capacity).forEach(i -> assertNull(shape.getValue("foo" + i))); 88 | } 89 | 90 | @Test 91 | public void getIndexPutALot() { 92 | var capacity = 100_000; 93 | var shape = new MapShape(capacity); 94 | IntStream.range(0, capacity).forEach(i -> shape.put(i, "" + i, i %2 == 0? STRING_LENGTH: STRING_EQUALS)); 95 | 96 | // linear scan 97 | IntStream.range(0, capacity).forEach(i -> assertEquals("" + i, shape.getKey(i))); 98 | IntStream.range(0, capacity).forEach(i -> assertEquals(i %2 == 0? STRING_LENGTH: STRING_EQUALS, shape.getValue(i))); 99 | } 100 | 101 | @Test 102 | public void size() { 103 | var shape = new MapShape(77); 104 | assertEquals(77, shape.size()); 105 | } 106 | } -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/JSONTraitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | import java.io.StringReader; 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | public class JSONTraitTest { 13 | 14 | @Test 15 | public void toJSON() { 16 | record Person(String name, int age) implements JSONTrait {} 17 | var person = new Person("Bob", 42); 18 | 19 | assertEquals(""" 20 | {"name": "Bob", "age": 42}\ 21 | """, person.toJSON()); 22 | } 23 | 24 | @Test 25 | public void toHumanReadableJSON() { 26 | record Person(String name, int age) implements JSONTrait {} 27 | var person = new Person("Bob", 42); 28 | 29 | assertEquals(""" 30 | { 31 | "name": "Bob", 32 | "age": 42 33 | }\ 34 | """, person.toHumanReadableJSON()); 35 | } 36 | 37 | @Test 38 | public void parseJSON() throws IOException { 39 | record Person(String name, int age) {} 40 | var person = JSONTrait.parse(new StringReader(""" 41 | { 42 | "name": "Bob", 43 | "age": 42 44 | }\ 45 | """), Person.class); 46 | 47 | var expected = new Person("Bob", 42); 48 | assertEquals(expected, person); 49 | } 50 | 51 | @Test 52 | public void toJSONWithPrimitiveTypes() { 53 | record Foo(boolean b, char c, int i, long l, float f, double d, String s) implements JSONTrait {} 54 | var foo = new Foo(true, 'f', 2, 3L, 4f, 8.0, null); 55 | 56 | assertEquals(""" 57 | {"b": true, "c": "f", "i": 2, "l": 3, "f": 4.0, "d": 8.0, "s": null}\ 58 | """, foo.toJSON()); 59 | } 60 | 61 | @Test 62 | public void toHumanReadableJSONWithPrimitiveTypes() { 63 | record Foo(boolean b, char c, int i, long l, float f, double d, String s) implements JSONTrait {} 64 | var foo = new Foo(true, 'f', 2, 3L, 4f, 8.0, null); 65 | 66 | assertEquals(""" 67 | { 68 | "b": true, 69 | "c": "f", 70 | "i": 2, 71 | "l": 3, 72 | "f": 4.0, 73 | "d": 8.0, 74 | "s": null 75 | }\ 76 | """, foo.toHumanReadableJSON()); 77 | } 78 | 79 | @Test 80 | public void parseJSONWithPrimitiveTypes() throws IOException { 81 | record Foo(boolean b, char c, int i, long l, float f, double d, String s) {} 82 | var person = JSONTrait.parse(new StringReader(""" 83 | { 84 | "b": true, 85 | "c": "f", 86 | "i": 2, 87 | "l": 3, 88 | "f": 4.0, 89 | "d": 8.0, 90 | "s": null 91 | }\ 92 | """), Foo.class); 93 | 94 | var expected = new Foo(true, 'f', 2, 3L, 4f, 8.0, null); 95 | assertEquals(expected, person); 96 | } 97 | 98 | @Test 99 | public void toJSONWithUnknownType() { 100 | record Timestamp(LocalDate date) implements JSONTrait {} 101 | var timestamp = new Timestamp(LocalDate.of(2000, 1, 1)); 102 | 103 | assertEquals(""" 104 | {"date": "2000-01-01"}\ 105 | """, timestamp.toJSON()); 106 | } 107 | 108 | @Test 109 | public void toHumanReadableJSONWithUnknownType() { 110 | record Timestamp(LocalDate date) implements JSONTrait {} 111 | var timestamp = new Timestamp(LocalDate.of(2000, 1, 1)); 112 | 113 | assertEquals(""" 114 | { 115 | "date": "2000-01-01" 116 | }\ 117 | """, timestamp.toHumanReadableJSON()); 118 | } 119 | 120 | @Test 121 | public void parseJSONWithUnknownType() throws IOException { 122 | record Timestamp(LocalDate date) {} 123 | var timestamp = JSONTrait.parse(new StringReader(""" 124 | { 125 | "date": "2000-01-01" 126 | }\ 127 | """), Timestamp.class, (valueAsString, type, downstreamConverter) -> { 128 | if (type == LocalDate.class) { 129 | return LocalDate.parse(valueAsString); 130 | } 131 | return downstreamConverter.convert(valueAsString, type); 132 | }); 133 | 134 | var expected = new Timestamp(LocalDate.of(2000, 1, 1)); 135 | assertEquals(expected, timestamp); 136 | } 137 | 138 | @Test 139 | public void toEnclosedJSON() { 140 | record Address(int number, String street) {} 141 | record Person(String name, int age, Address address) implements JSONTrait {} 142 | var person = new Person("Bob", 42, new Address(13, "civic street")); 143 | 144 | assertEquals(""" 145 | {"name": "Bob", "age": 42, "address": {"number": 13, "street": "civic street"}}\ 146 | """, person.toJSON()); 147 | } 148 | 149 | @Test 150 | public void toHumanReadableEnclosedJSON() { 151 | record Address(int number, String street) {} 152 | record Person(String name, int age, Address address) implements JSONTrait {} 153 | var person = new Person("Bob", 42, new Address(13, "civic street")); 154 | 155 | assertEquals(""" 156 | { 157 | "name": "Bob", 158 | "age": 42, 159 | "address": { 160 | "number": 13, 161 | "street": "civic street" 162 | } 163 | }\ 164 | """, person.toHumanReadableJSON()); 165 | } 166 | 167 | @Test 168 | public void parseEnclosedJSON() throws IOException { 169 | record Address(int number, String street) {} 170 | record Person(String name, int age, Address address) {} 171 | var person = JSONTrait.parse(new StringReader(""" 172 | { 173 | "name": "Bob", 174 | "age": 42, 175 | "address": { 176 | "number": 13, 177 | "street": "civic street" 178 | } 179 | }\ 180 | """), Person.class); 181 | 182 | var expected = new Person("Bob", 42, new Address(13, "civic street")); 183 | assertEquals(expected, person); 184 | } 185 | 186 | @Test 187 | public void streamArrayOfJSONObject() { 188 | record Person(String name, int age) {} 189 | var reader = new StringReader(""" 190 | [ 191 | { "name": "Bob", "age": 42 }, 192 | { "name": "Ana", "age": 27 } 193 | ]\ 194 | """); 195 | 196 | List list; 197 | try(var stream = JSONTrait.stream(reader, Person.class)) { 198 | list = stream.toList(); 199 | } 200 | 201 | var expected = List.of(new Person("Bob", 42), new Person("Ana", 27)); 202 | assertEquals(expected, list); 203 | } 204 | } -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/MapTraitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | public class MapTraitTest { 13 | 14 | @Test 15 | public void size() { 16 | record Point(int x, int y) implements MapTrait {} 17 | var point = new Point(1, 2); 18 | assertEquals(2, point.size()); 19 | } 20 | 21 | @Test 22 | public void isEmpty() { 23 | record Person(String name, int age) implements MapTrait { } 24 | var person = new Person("Bob", 42); 25 | assertFalse(person.isEmpty()); 26 | } 27 | 28 | @Test 29 | public void get() { 30 | record Person(String name, int age) implements MapTrait { } 31 | var person = new Person("Bob", 42); 32 | assertAll( 33 | () -> assertEquals("Bob", person.get("name")), 34 | () -> assertEquals(42, person.get("age")), 35 | () -> assertNull(person.get("whatever")), 36 | () -> assertNull(person.get(null)) 37 | ); 38 | } 39 | 40 | @Test 41 | public void getOrDefault() { 42 | record Person(String name, int age) implements MapTrait { } 43 | var person = new Person("Bob", 42); 44 | assertAll( 45 | () -> assertEquals("Bob", person.getOrDefault("name", "oops")), 46 | () -> assertEquals(42, person.getOrDefault("age", 0)), 47 | () -> assertEquals("boom", person.getOrDefault("whatever", "boom")), 48 | () -> assertEquals("boom", person.getOrDefault(null, "boom")) 49 | ); 50 | } 51 | 52 | @Test 53 | public void containsKey() { 54 | record Person(String name, int age) implements MapTrait { } 55 | var person = new Person("Bob", 42); 56 | assertAll( 57 | () -> assertTrue(person.containsKey("name")), 58 | () -> assertTrue(person.containsKey("age")), 59 | () -> assertFalse(person.containsKey("whatever")), 60 | () -> assertFalse(person.containsKey(null)) 61 | ); 62 | } 63 | 64 | @Test 65 | public void containsValue() { 66 | record Person(String name, int age) implements MapTrait { } 67 | var person = new Person("Bob", 42); 68 | assertAll( 69 | () -> assertTrue(person.containsValue("Bob")), 70 | () -> assertTrue(person.containsValue(42)), 71 | () -> assertFalse(person.containsValue("whatever")), 72 | () -> assertFalse(person.containsValue(null)) 73 | ); 74 | } 75 | 76 | @Test 77 | public void entrySet() { 78 | record Person(String name, int age) implements MapTrait { } 79 | var person = new Person("Bob", 42); 80 | assertEquals(Set.of(Map.entry("name", "Bob"), Map.entry("age", 42)), person.entrySet()); 81 | assertEquals(List.of(Map.entry("name", "Bob"), Map.entry("age", 42)), person.entrySet().stream().toList()); 82 | } 83 | 84 | @Test 85 | public void keySet() { 86 | record Person(String name, int age) implements MapTrait { } 87 | var person = new Person("Bob", 42); 88 | assertAll( 89 | () -> assertEquals(Set.of("name", "age"), person.keySet()), 90 | () -> assertEquals(List.of("name", "age"), person.keySet().stream().toList()), 91 | () -> assertTrue(person.keySet().contains("name")), 92 | () -> assertTrue(person.keySet().contains("age")), 93 | () -> assertFalse(person.keySet().contains("foo")) 94 | ); 95 | } 96 | 97 | @Test 98 | public void values() { 99 | record Person(String name, int age) implements MapTrait { } 100 | var person = new Person("Bob", 42); 101 | assertEquals(List.of("Bob", 42), person.values()); 102 | } 103 | 104 | @Test 105 | public void keys() { 106 | record Person(String name, int age) implements MapTrait { } 107 | var person = new Person("Bob", 42); 108 | assertAll( 109 | () -> assertEquals(List.of("name", "age"), person.keys()), 110 | () -> assertTrue(person.keys().contains("name")), 111 | () -> assertTrue(person.keys().contains("age")), 112 | () -> assertFalse(person.keys().contains("foo")) 113 | ); 114 | } 115 | 116 | @Test 117 | public void unsupported() { 118 | record Person(String name, int age) implements MapTrait { } 119 | var person = new Person("Bob", 42); 120 | assertAll( 121 | () -> assertThrows(UnsupportedOperationException.class, () -> person.clear()), 122 | () -> assertThrows(UnsupportedOperationException.class, () -> person.put("foo", "bar")), 123 | () -> assertThrows(UnsupportedOperationException.class, () -> person.putAll(Map.of())), 124 | () -> assertThrows(UnsupportedOperationException.class, () -> person.remove("name")), 125 | () -> assertThrows(UnsupportedOperationException.class, () -> person.remove("name", "Bob")), 126 | () -> assertThrows(UnsupportedOperationException.class, () -> person.replace("name", "Ana")), 127 | () -> assertThrows(UnsupportedOperationException.class, () -> person.replace("name", "Bob", "Ana")), 128 | () -> assertThrows(UnsupportedOperationException.class, () -> person.replaceAll((key, value) -> value)), 129 | () -> assertThrows(UnsupportedOperationException.class, () -> person.compute("Bob", (key, value) -> value)), 130 | () -> assertThrows(UnsupportedOperationException.class, () -> person.computeIfAbsent("Bob", key -> 77)), 131 | () -> assertThrows(UnsupportedOperationException.class, () -> person.computeIfPresent("Bob", (key, value) -> 77)) 132 | ); 133 | } 134 | 135 | @Test 136 | public void equalsHashCode() { 137 | record Person(String name, int age) implements MapTrait { 138 | @Override 139 | public boolean equals(Object o) { 140 | return MapTrait.super.equalsOfMap(o); 141 | } 142 | 143 | @Override 144 | public int hashCode() { 145 | return MapTrait.super.hashCodeOfMap(); 146 | } 147 | } 148 | var person = new Person("Bob", 42); 149 | assertAll( 150 | () -> assertEquals(Map.of("name", "Bob", "age", 42), person), 151 | () -> assertFalse(person.equals(null)), 152 | () -> assertEquals(Map.of("name", "Bob", "age", 42).hashCode(), person.hashCode()) 153 | ); 154 | } 155 | 156 | @Test 157 | public void testToString() { 158 | record Person(String name, int age) implements MapTrait { 159 | @Override 160 | public String toString() { 161 | return MapTrait.super.toStringOfMap(); 162 | } 163 | } 164 | var person = new Person("Bob", 42); 165 | var expectedMap = new LinkedHashMap(); 166 | expectedMap.put("name", "Bob"); 167 | expectedMap.put("age", 42); 168 | assertEquals(expectedMap.toString(), person.toString()); 169 | } 170 | } -------------------------------------------------------------------------------- /src/test/java/com/github/forax/recordutil/WitherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.lang.invoke.MethodHandles; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | public class WitherTest { 10 | 11 | @Test 12 | public void with1() { 13 | record Person(String name, int age) {} 14 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 15 | 16 | var bob = new Person("Bob", 42); 17 | assertAll( 18 | () -> assertEquals(new Person("Ana", 42), wither.with(bob, "name", "Ana")), 19 | () -> assertEquals(new Person("Bob", 23), wither.with(bob, "age", 23)) 20 | ); 21 | } 22 | 23 | @Test 24 | public void with2() { 25 | record Person(String name, int age) {} 26 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 27 | 28 | var bob = new Person("Bob", 42); 29 | assertAll( 30 | () -> assertEquals(new Person("Ana", 23), wither.with(bob, "name", "Ana", "age", 23)), 31 | () -> assertEquals(new Person("Ana", 23), wither.with(bob, "age", 23, "name", "Ana")) 32 | ); 33 | } 34 | 35 | @Test 36 | public void with3() { 37 | record Point3D(double x, double y, double z) {} 38 | var wither = Wither.of(MethodHandles.lookup(), Point3D.class); 39 | 40 | var point = new Point3D(1.0, 2.0, 4.0); 41 | assertAll( 42 | () -> assertEquals(new Point3D(3, 7, 13), wither.with(point, "x", 3.0, "y", 7.0, "z", 13.0)), 43 | () -> assertEquals(new Point3D(3, 7, 13), wither.with(point, "y", 7.0, "z", 13.0, "x", 3.0)) 44 | ); 45 | } 46 | 47 | @Test 48 | public void with4() { 49 | record Address(int number, String street, String city, String state, String country) {} 50 | var wither = Wither.of(MethodHandles.lookup(), Address.class); 51 | 52 | var address = new Address(13, "baker street", "san jose", "CA", "United States"); 53 | var expected = new Address(71, "river street", "banshee", "TX", "United States"); 54 | assertAll( 55 | () -> assertEquals(expected, wither.with(address, "number", 71, "street", "river street", "city", "banshee", "state", "TX")), 56 | () -> assertEquals(expected, wither.with(address, "state", "TX", "street", "river street", "number", 71, "city", "banshee")) 57 | ); 58 | } 59 | 60 | @Test 61 | public void with5() { 62 | record Address(int number, String street, String city, String state, String country) {} 63 | var wither = Wither.of(MethodHandles.lookup(), Address.class); 64 | 65 | var address = new Address(13, "baker street", "san jose", "CA", "United States"); 66 | var expected = new Address(71, "river street", "banshee", "n/a", "Poland"); 67 | assertAll( 68 | () -> assertEquals(expected, wither.with(address, "number", 71, "street", "river street", "city", "banshee", "state", "n/a", "country", "Poland")), 69 | () -> assertEquals(expected, wither.with(address, "state", "n/a", "country", "Poland", "street", "river street", "number", 71, "city", "banshee")) 70 | ); 71 | } 72 | 73 | @Test 74 | public void with6() { 75 | record Foo(int a, long b, float c, double d, boolean e, byte f, char g, short h, Object i) {} 76 | var wither = Wither.of(MethodHandles.lookup(), Foo.class); 77 | 78 | var foo = new Foo(1, 2, 3, 5, true, (byte) 5, '6', (short) 7, null); 79 | var expected = new Foo(1, 3, 4, 5, false, (byte) 6, '7', (short) 8, null); 80 | assertAll( 81 | () -> assertEquals(expected, wither.with(foo, "b", 3L, "c", 4f, "e", false, "f", (byte) 6, "g", '7', "h", (short) 8)), 82 | () -> assertEquals(expected, wither.with(foo, "h", (short) 8, "e", false, "f", (byte) 6, "g", '7', "b", 3L, "c", 4f)) 83 | ); 84 | } 85 | 86 | @Test 87 | public void with7() { 88 | record Foo(int a, long b, float c, double d, boolean e, byte f, char g, short h, Object i) {} 89 | var wither = Wither.of(MethodHandles.lookup(), Foo.class); 90 | 91 | var foo = new Foo(1, 2, 3, 4, true, (byte) 5, '6', (short) 7, null); 92 | var expected = new Foo(1, 3, 4, 5, false, (byte) 6, '7', (short) 8, null); 93 | assertAll( 94 | () -> assertEquals(expected, wither.with(foo, "b", 3L, "c", 4f, "d", 5.0, "e", false, "f", (byte) 6, "g", '7', "h", (short) 8)), 95 | () -> assertEquals(expected, wither.with(foo, "e", false, "d", 5.0,"f", (byte) 6, "g", '7', "b", 3L, "c", 4f, "h", (short) 8)) 96 | ); 97 | } 98 | 99 | @Test 100 | public void with8() { 101 | record Foo(int a, long b, float c, double d, boolean e, byte f, char g, short h, Object i) {} 102 | var wither = Wither.of(MethodHandles.lookup(), Foo.class); 103 | 104 | var foo = new Foo(1, 2, 3, 4, true, (byte) 5, '6', (short) 7, null); 105 | var expected = new Foo(2, 3, 4, 5, false, (byte) 6, '7', (short) 8, null); 106 | assertAll( 107 | () -> assertEquals(expected, wither.with(foo, "a", 2, "b", 3L, "c", 4f, "d", 5.0, "e", false, "f", (byte) 6, "g", '7', "h", (short) 8)), 108 | () -> assertEquals(expected, wither.with(foo, "d", 5.0,"f", (byte) 6, "g", '7', "a", 2, "e", false, "b", 3L, "c", 4f, "h", (short) 8)) 109 | ); 110 | } 111 | 112 | @Test 113 | public void with9() { 114 | record Foo(int a, long b, float c, double d, boolean e, byte f, char g, short h, Object i) {} 115 | var wither = Wither.of(MethodHandles.lookup(), Foo.class); 116 | 117 | var foo = new Foo(1, 2, 3, 4, true, (byte) 5, '6', (short) 7, new Object()); 118 | var expected = new Foo(2, 3, 4, 5, false, (byte) 6, '7', (short) 8, null); 119 | assertAll( 120 | () -> assertEquals(expected, wither.with(foo, "a", 2, "b", 3L, "c", 4f, "d", 5.0, "e", false, "f", (byte) 6, "g", '7', "h", (short) 8, "i", null)), 121 | () -> assertEquals(expected, wither.with(foo, "f", (byte) 6, "g", '7', "a", 2, "d", 5.0, "e", false, "i", null, "b", 3L, "c", 4f, "h", (short) 8)) 122 | ); 123 | } 124 | 125 | @Test 126 | public void withDifferentKeyCounts() { 127 | record Person(String name, int age) {} 128 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 129 | 130 | var bob = new Person("Bob", 42); 131 | assertAll( 132 | () -> assertEquals(new Person("Ana", 42), wither.with(bob, "name", "Ana")), 133 | () -> assertEquals(new Person("Ana", 23), wither.with(bob, "age", 23, "name", "Ana")) 134 | ); 135 | } 136 | 137 | @Test 138 | public void withNullRecord() { 139 | record Person(String name, int age) {} 140 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 141 | 142 | assertThrows(NullPointerException.class, () -> wither.with(null, "name", "Ana")); 143 | } 144 | 145 | @Test 146 | public void withANullKey() { 147 | record Person(String name, int age) {} 148 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 149 | 150 | var bob = new Person("Bob", 42); 151 | assertThrows(NullPointerException.class, () -> wither.with(bob, "name", "foo", null, "bar")); 152 | } 153 | 154 | @Test 155 | public void withInvalidKey() { 156 | record Person(String name, int age) {} 157 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 158 | 159 | var bob = new Person("Bob", 42); 160 | assertThrows(IllegalArgumentException.class, () -> wither.with(bob, "foo", "Bar")); 161 | } 162 | 163 | @Test 164 | public void withNonInternedKey() { 165 | record Person(String name, int age) {} 166 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 167 | 168 | var bob = new Person("Bob", 42); 169 | assertThrows(IllegalArgumentException.class, () -> wither.with(bob, new String("name"), "Ana")); 170 | } 171 | 172 | @Test 173 | public void withDuplicateKeys() { 174 | record Person(String name, int age) {} 175 | var wither = Wither.of(MethodHandles.lookup(), Person.class); 176 | 177 | var bob = new Person("Bob", 42); 178 | assertThrows(IllegalArgumentException.class, () -> wither.with(bob, "name", "Ana", "name", "Elis")); 179 | } 180 | } -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/JSONParsing.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import com.fasterxml.jackson.core.JsonFactory; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | import com.fasterxml.jackson.core.JsonToken; 6 | 7 | import java.io.IOException; 8 | import java.io.Reader; 9 | import java.io.UncheckedIOException; 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.reflect.UndeclaredThrowableException; 12 | import java.math.BigDecimal; 13 | import java.math.BigInteger; 14 | import java.util.ArrayList; 15 | import java.util.LinkedHashSet; 16 | import java.util.Spliterator; 17 | import java.util.function.Consumer; 18 | import java.util.stream.Stream; 19 | import java.util.stream.StreamSupport; 20 | 21 | class JSONParsing { 22 | public static Object parse(Reader reader, Class> recordType, JSONTrait.Converter converter) throws IOException { 23 | try (var parser = createFactory().createParser(reader)) { 24 | if (parser.nextToken() != JsonToken.START_OBJECT) { 25 | throw new IOException("invalid start token for a JSON Object" + parser.getText()); 26 | } 27 | return parseRecord(parser, recordType, converter); 28 | } 29 | } 30 | 31 | private static JsonFactory createFactory() { 32 | try { 33 | return new JsonFactory(); 34 | } catch(NoClassDefFoundError error) { 35 | error.addSuppressed(new ClassNotFoundException(""" 36 | 37 | This feature requires the JSON parser named 'jackson'. To enable it, you have to 38 | add `requires requires com.fasterxml.jackson.core;` to your module-info 39 | and also add the dependency to `com.fasterxml.jackson.core:jackson-core` in your POM file 40 | """)); 41 | throw error; 42 | } 43 | } 44 | 45 | public static Stream stream(Reader reader, Class extends R> recordType, JSONTrait.Converter converter) { 46 | JsonParser _parser = null; 47 | try { 48 | _parser = createFactory().createParser(reader); 49 | if (_parser.nextToken() != JsonToken.START_ARRAY) { 50 | throw new UncheckedIOException(new IOException("invalid start token for a JSON Array" + _parser.getText())); 51 | } 52 | } catch(IOException e) { 53 | try { 54 | if (_parser != null) { 55 | _parser.close(); 56 | } 57 | } catch(IOException e2) { 58 | e.addSuppressed(e2); 59 | } 60 | throw new UncheckedIOException(e); 61 | } 62 | 63 | var parser = _parser; 64 | var stream = StreamSupport.stream(new Spliterator() { 65 | @Override 66 | public boolean tryAdvance(Consumer super R> action) { 67 | try { 68 | var token = parser.nextToken(); 69 | if (token == JsonToken.END_ARRAY) { 70 | return false; 71 | } 72 | var record = recordType.cast(parseRecord(parser, recordType, converter)); 73 | action.accept(record); 74 | return true; 75 | } catch(IOException e) { 76 | throw new UncheckedIOException(e); 77 | } 78 | } 79 | @Override 80 | public Spliterator trySplit() { 81 | return null; 82 | } 83 | @Override 84 | public long estimateSize() { 85 | return Long.MAX_VALUE; 86 | } 87 | @Override 88 | public int characteristics() { 89 | return ORDERED; 90 | } 91 | }, false); 92 | return stream.onClose(() -> { 93 | try { 94 | parser.close(); 95 | } catch(IOException e) { 96 | // silently ignore it 97 | } 98 | }); 99 | } 100 | 101 | private static Object parseRecord(JsonParser parser, Class> recordType, JSONTrait.Converter converter) throws IOException { 102 | var shape = TraitImpl.jsonShape(recordType); 103 | var array = new Object[shape.size()]; 104 | for(;;) { 105 | var token = parser.nextToken(); 106 | switch (token) { 107 | case END_OBJECT: 108 | return invokeArray(shape.constructor(), array); 109 | case FIELD_NAME: { 110 | var name = parser.getCurrentName(); 111 | var slot = shape.getSlot(name); 112 | if (slot == -1) { 113 | throw new IOException("invalid key name " + name + " for record " + recordType.getName()); 114 | } 115 | var type = shape.getType(slot); 116 | array[slot] = parseValue(parser, parser.nextValue(), type, converter); 117 | continue; 118 | } 119 | default: 120 | throw new IOException("invalid token " + parser.getText()); 121 | } 122 | } 123 | } 124 | 125 | private static Object invokeArray(MethodHandle constructor, Object[] array) { 126 | try { 127 | return constructor.invokeExact(array); 128 | } catch (RuntimeException | Error e) { 129 | throw e; 130 | } catch (Throwable throwable) { 131 | throw new UndeclaredThrowableException(throwable); 132 | } 133 | } 134 | 135 | private static Object parseArray(JsonParser parser, Class> type, JSONTrait.Converter converter) throws IOException { 136 | var list = new ArrayList<>(); 137 | for(;;) { 138 | var token = parser.nextToken(); 139 | if (token == JsonToken.END_ARRAY) { 140 | return defaultListConversion(list, type); 141 | } else { 142 | list.add(parseValue(parser, token, type, converter)); 143 | } 144 | } 145 | } 146 | 147 | private static Object parseValue(JsonParser parser, JsonToken token, Class> type, JSONTrait.Converter converter) throws IOException { 148 | return switch (token) { 149 | case VALUE_TRUE -> true; 150 | case VALUE_FALSE -> false; 151 | case VALUE_NULL -> null; 152 | case VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT -> convertValue(parser.getValueAsString(), type, converter); 153 | case VALUE_STRING -> convertValue(parser.getText(), type, converter); 154 | case START_OBJECT -> parseRecord(parser, type, converter); 155 | case START_ARRAY -> parseArray(parser, type, converter); 156 | default -> throw new IOException("invalid value " + parser.getValueAsString()); 157 | }; 158 | } 159 | 160 | private static Object convertValue(String valueAsString, Class> type, JSONTrait.Converter converter) throws IOException { 161 | return converter.convert(valueAsString, type, JSONParsing::defaultValueConversions); 162 | } 163 | 164 | private static Object defaultValueConversions(String valueAsString, Class> type) throws IOException { 165 | try { 166 | return switch (type.getName()) { 167 | case "java.lang.String" -> valueAsString; 168 | case "char", "java.lang.Character" -> { 169 | if (valueAsString.length() != 1) { 170 | throw new IOException("can not convert " + valueAsString + " to a char or a java.lang.Character"); 171 | } 172 | yield valueAsString.charAt(0); 173 | } 174 | case "byte", "java.lang.Byte" -> Byte.parseByte(valueAsString); 175 | case "short", "java.lang.Short" -> Short.parseShort(valueAsString); 176 | case "int", "java.lang.Integer" -> Integer.parseInt(valueAsString); 177 | case "double", "java.lang.Double" -> Double.parseDouble(valueAsString); 178 | case "long", "java.lang.Long" -> Long.parseLong(valueAsString); 179 | case "float", "java.lang.Float" -> Float.parseFloat(valueAsString); 180 | case "java.math.BigInteger" -> new BigInteger(valueAsString); 181 | case "java.math.BigDecimal" -> new BigDecimal(valueAsString); 182 | default -> throw new IOException("unknown conversion from " + valueAsString + " to " + type.getName()); 183 | }; 184 | } catch(NumberFormatException e) { 185 | throw new IOException("invalid conversion from " + valueAsString + " to " + type.getName(), e); 186 | } 187 | } 188 | 189 | private static Object defaultListConversion(ArrayList list, Class> type) throws IOException { 190 | return switch(type.getName()) { 191 | case "java.util.Collection", "java.util.List", "java.util.ArrayList" -> list; 192 | case "java.util.Set", "java.util.HashSet", "java.util.LinkedHashSet" -> new LinkedHashSet<>(list); 193 | default -> throw new IOException("unknown conversion from array to " + type.getName()); 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/WithTrait.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import com.github.forax.recordutil.TraitImpl.WithShape; 4 | 5 | import java.lang.reflect.UndeclaredThrowableException; 6 | import java.util.Objects; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | /** 11 | * An interface that provides several methods {@code with} that create a new record 12 | * from an existing record instance and a list of the record component names and values 13 | * that need to be updated 14 | * 15 | * Adding this interface add several methods {@code with} to any records. 16 | * 17 | * record Person(String name, int age) implements WithTrait<Person> {} 18 | * ... 19 | * Person bob = new Person("Bob", 42); 20 | * Person ana = bob.with("name", "Ana"); 21 | * 22 | * 23 | * 24 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 25 | * in a package in a module which does not open the package to the module 26 | * {@code com.github.forax.recordutil}. 27 | * By example, if the record is declared in a module mymodule in a package mypackage, 28 | * the module-info of this module should contains the following declaration 29 | * 30 | * module mymodule { 31 | * ... 32 | * open mypackage to com.github.forax.recordutil; 33 | * } 34 | * 35 | * 36 | * 37 | * This implementation is not very efficient, use {@link Wither} for a more cumbersome 38 | * but more performant implementation. 39 | * 40 | * @param type of the record 41 | * 42 | * @see Wither 43 | */ 44 | public interface WithTrait { 45 | private Object[] initArray(WithShape shape) { 46 | var array = new Object[shape.size()]; 47 | try { 48 | for (var i = 0; i < shape.size(); i++) { 49 | array[i] = shape.getValue(i).invokeExact((Object) this); 50 | } 51 | return array; 52 | } catch (RuntimeException | Error e) { 53 | throw e; 54 | } catch (Throwable throwable) { 55 | throw new UndeclaredThrowableException(throwable); 56 | } 57 | } 58 | 59 | private static Object invokeArray(WithShape shape, Object[] array) { 60 | try { 61 | return shape.constructor().invokeExact(array); 62 | } catch (RuntimeException | Error e) { 63 | throw e; 64 | } catch (Throwable throwable) { 65 | throw new UndeclaredThrowableException(throwable); 66 | } 67 | } 68 | 69 | private static int slot(WithShape shape, String name) { 70 | var slot = shape.getSlot(name); 71 | if (slot == -1) { 72 | throw new IllegalStateException("record component " + name + "not found"); 73 | } 74 | return slot; 75 | } 76 | 77 | /** 78 | * Returns a new record instance with the record component named {@code name} updated 79 | * to the value {@code value}. 80 | * 81 | * @param name a record component name 82 | * @param value the new value of the record component {@code name} 83 | * @return a new record instance with the record component value updated 84 | * 85 | * @throws NullPointerException if {@code name} is null 86 | * @throws ClassCastException if the value has not a class compatible with the record component type 87 | */ 88 | @SuppressWarnings("unchecked") 89 | default R with(String name, Object value) { 90 | requireNonNull(name, "name is null"); 91 | var shape = TraitImpl.withShape(getClass()); 92 | var array = initArray(shape); 93 | array[slot(shape, name)] = value; 94 | return (R) invokeArray(shape, array); 95 | } 96 | 97 | /** 98 | * Returns a new record instance with the record components named {@code name1} 99 | * and {@code name2} respectively updated to the value {@code value1} and {@code value2}. 100 | * 101 | * @param name1 a record component name 102 | * @param value1 the new value of the record component {@code name1} 103 | * @param name2 a record component name 104 | * @param value2 the new value of the record component {@code name2} 105 | * @return a new record instance with the record component values updated 106 | * 107 | * @throws NullPointerException if {@code name1} or {@code name2} is null 108 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type 109 | */ 110 | @SuppressWarnings("unchecked") 111 | default R with(String name1, Object value1, String name2, Object value2) { 112 | requireNonNull(name1, "name1 is null"); 113 | requireNonNull(name2, "name2 is null"); 114 | var shape = TraitImpl.withShape(getClass()); 115 | var array = initArray(shape); 116 | array[slot(shape, name1)] = value1; 117 | array[slot(shape, name2)] = value2; 118 | return (R) invokeArray(shape, array); 119 | } 120 | 121 | /** 122 | * Returns a new record instance with the record components named {@code name1} 123 | * {@code name2} and {@code name3} respectively updated to the value {@code value1}, {@code value2} 124 | * and {@code value3}. 125 | * 126 | * @param name1 a record component name 127 | * @param value1 the new value of the record component {@code name1} 128 | * @param name2 a record component name 129 | * @param value2 the new value of the record component {@code name2} 130 | * @param name3 a record component name 131 | * @param value3 the new value of the record component {@code name3} 132 | * @return a new record instance with the record component values updated 133 | * 134 | * @throws NullPointerException if {@code name1}, {@code name2} or {@code name3} is null 135 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type 136 | */ 137 | @SuppressWarnings("unchecked") 138 | default R with(String name1, Object value1, String name2, Object value2, String name3, Object value3) { 139 | requireNonNull(name1, "name1 is null"); 140 | requireNonNull(name2, "name2 is null"); 141 | requireNonNull(name3, "name3 is null"); 142 | var shape = TraitImpl.withShape(getClass()); 143 | var array = initArray(shape); 144 | array[slot(shape, name1)] = value1; 145 | array[slot(shape, name2)] = value2; 146 | array[slot(shape, name3)] = value3; 147 | return (R) invokeArray(shape, array); 148 | } 149 | 150 | /** 151 | * Returns a new record instance with the record components named {@code name1} 152 | * {@code name2}, {@code name3} and {@code name4} respectively updated to the value {@code value1}, 153 | * {@code value2}, {@code value3} and {@code value4}. 154 | * 155 | * @param name1 a record component name 156 | * @param value1 the new value of the record component {@code name1} 157 | * @param name2 a record component name 158 | * @param value2 the new value of the record component {@code name2} 159 | * @param name3 a record component name 160 | * @param value3 the new value of the record component {@code name3} 161 | * @param name4 a record component name 162 | * @param value4 the new value of the record component {@code name4} 163 | * @return a new record instance with the record component values updated 164 | * 165 | * @throws NullPointerException if {@code name1}, {@code name2}, {@code name3} or {@code name4} is null 166 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type. 167 | */ 168 | @SuppressWarnings("unchecked") 169 | default R with(String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) { 170 | requireNonNull(name1, "name1 is null"); 171 | requireNonNull(name2, "name2 is null"); 172 | requireNonNull(name3, "name3 is null"); 173 | requireNonNull(name4, "name4 is null"); 174 | var shape = TraitImpl.withShape(getClass()); 175 | var array = initArray(shape); 176 | array[slot(shape, name1)] = value1; 177 | array[slot(shape, name2)] = value2; 178 | array[slot(shape, name3)] = value3; 179 | array[slot(shape, name4)] = value4; 180 | return (R) invokeArray(shape, array); 181 | } 182 | 183 | /** 184 | * Returns a new record instance with the record components whose names are . 185 | * 186 | * @param pairs an array of pair of record name/new value 187 | * @return a new record instance with the record component values updated 188 | * 189 | * @throws NullPointerException if one of the name of the pairs is null 190 | * @throws IllegalArgumentException is the pairs array length is odd or one name of the pairs is not a String 191 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type. 192 | */ 193 | @SuppressWarnings("unchecked") 194 | default R with(Object... pairs) { 195 | if ((pairs.length & 1) != 0) { 196 | throw new IllegalArgumentException("invalid arguments, it should be pairs of name, value"); 197 | } 198 | var shape = TraitImpl.withShape(getClass()); 199 | var array = initArray(shape); 200 | for(var i = 0; i < pairs.length; i += 2) { 201 | var name = Objects.requireNonNull(pairs[i], "name " + i + " is null"); 202 | if (!(name instanceof String key)) { 203 | throw new IllegalArgumentException("name " + i + " is not a String: " + name); 204 | } 205 | var value = pairs[i + 1]; 206 | array[slot(shape, key)] = value; 207 | } 208 | return (R) invokeArray(shape, array); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/WitherImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodHandles.Lookup; 6 | import java.lang.invoke.MutableCallSite; 7 | import java.lang.reflect.RecordComponent; 8 | import java.util.Arrays; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.stream.Stream; 13 | 14 | import static java.lang.invoke.MethodHandles.dropArguments; 15 | import static java.lang.invoke.MethodHandles.filterArguments; 16 | import static java.lang.invoke.MethodHandles.guardWithTest; 17 | import static java.lang.invoke.MethodHandles.insertArguments; 18 | import static java.lang.invoke.MethodHandles.permuteArguments; 19 | import static java.lang.invoke.MethodType.methodType; 20 | import static java.util.stream.Collectors.toMap; 21 | import static java.util.stream.IntStream.range; 22 | 23 | class WitherImpl { 24 | public static MethodHandle createMH(Lookup lookup, Class> recordType) { 25 | var components = recordType.getRecordComponents(); 26 | if (components == null) { 27 | throw new LinkageError("the record class " + recordType.getName() + " is not a record "); 28 | } 29 | Map nameToIndexMap = range(0, components.length).boxed().collect(toMap(i -> components[i].getName(), i -> i)); 30 | 31 | MethodHandle constructor; 32 | try { 33 | constructor = lookup.findConstructor(recordType, methodType(void.class, Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new))); 34 | } catch (NoSuchMethodException e) { 35 | throw (NoSuchMethodError) new NoSuchMethodError().initCause(e); 36 | } catch (IllegalAccessException e) { 37 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 38 | } 39 | 40 | return new InliningCache(lookup, recordType, components, nameToIndexMap, constructor).dynamicInvoker().asType( 41 | methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 42 | } 43 | 44 | private static class InliningCache extends MutableCallSite { 45 | private static final MethodHandle NAME_CHECK, NAME_COUNT_CHECK, FALLBACK; 46 | static { 47 | var lookup = MethodHandles.lookup(); 48 | try { 49 | NAME_CHECK = lookup.findStatic(InliningCache.class, "nameCheck", 50 | methodType(boolean.class, String.class, String.class)); 51 | NAME_COUNT_CHECK = lookup.findStatic(InliningCache.class, "nameCountCheck", 52 | methodType(boolean.class, int.class, int.class)); 53 | FALLBACK = lookup.findVirtual(InliningCache.class, "fallback", 54 | methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 55 | } catch (NoSuchMethodException | IllegalAccessException e) { 56 | throw new AssertionError(e); 57 | } 58 | } 59 | 60 | private final Lookup lookup; 61 | private final Class> recordType; 62 | private final RecordComponent[] components; 63 | private final Map nameToIndexMap; 64 | private final MethodHandle constructor; 65 | 66 | private InliningCache(Lookup lookup, Class> recordType, RecordComponent[] components, Map nameToIndexMap, MethodHandle constructor) { 67 | super(methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 68 | this.lookup = lookup; 69 | this.recordType = recordType; 70 | this.components = components; 71 | this.nameToIndexMap = nameToIndexMap; 72 | this.constructor = constructor; 73 | setTarget(FALLBACK.bindTo(this).asType(type())); 74 | } 75 | 76 | private static boolean nameCheck(String name, String expected) { 77 | //noinspection StringEquality 78 | return name == expected; 79 | } 80 | 81 | private static boolean nameCountCheck(int nameCount, int expected) { 82 | return nameCount == expected; 83 | } 84 | 85 | private Object fallback(Object record, int nameCount, 86 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 87 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 88 | String name6, Object value6, String name7, Object value7, String name8, Object value8) throws Throwable { 89 | 90 | Objects.requireNonNull(record, "record is null"); 91 | var names = gatherKeys(nameCount, name0, name1, name2, name3, name4, name5, name6, name7, name8); 92 | 93 | // check null names, non interned names or duplicate names 94 | var duplicates = new HashSet(); 95 | for(var i = 0; i < names.length; i++) { 96 | var name = names[i]; 97 | Objects.requireNonNull(name, "name " + i + " is null"); 98 | //noinspection StringEquality 99 | if (name != name.intern()) { 100 | throw new IllegalArgumentException("name " + name + " should be a constant name"); 101 | } 102 | if (!duplicates.add(name)) { 103 | throw new IllegalArgumentException("duplicate name " + i); 104 | } 105 | } 106 | 107 | int[] reorder = new int[components.length]; 108 | Class>[] newTypes = new Class>[1 + names.length]; 109 | newTypes[0] = recordType; 110 | for(var i = 0; i < names.length; i++) { 111 | var name = names[i]; 112 | var componentIndex = nameToIndexMap.get(name); 113 | if (componentIndex == null) { 114 | throw new IllegalArgumentException("unknown record component " + name + " for record " + recordType.getName()); 115 | } 116 | 117 | reorder[componentIndex] = i + 1; 118 | newTypes[i + 1] = components[componentIndex].getType(); 119 | } 120 | 121 | // use getters 122 | var filters = range(0, components.length) 123 | .mapToObj(i -> (reorder[i] != 0)? null: asGetter(lookup, components[i])) 124 | .toArray(MethodHandle[]::new); 125 | var mh = filterArguments(constructor, 0, filters); 126 | 127 | // re-organise, duplicate the record if there is a getter 128 | mh = permuteArguments(mh, methodType(recordType, newTypes), reorder); 129 | 130 | // drop the names 131 | for(var i = names.length; --i >= 0;) { 132 | mh = dropArguments(mh, 1 + i, String.class); 133 | } 134 | 135 | // drop null name/value pair up to 9 136 | if (names.length != 9) { 137 | mh = dropArguments(mh, mh.type().parameterCount(), range(names.length, 9).boxed().flatMap(__ -> Stream.of(String.class, Object.class)).toArray(Class[]::new)); 138 | } 139 | 140 | var result = mh.invoke(record, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 141 | 142 | // drop nameCount 143 | mh = dropArguments(mh, 1, int.class); 144 | 145 | // mask all values as Object 146 | mh = mh.asType(type()); 147 | 148 | // install constant name guards 149 | var newTypes2 = mh.type().parameterArray(); 150 | var other = new InliningCache(lookup, recordType, components, nameToIndexMap, constructor).dynamicInvoker(); 151 | for(var i = 0; i < names.length; i++) { 152 | var name = names[i]; 153 | var check = insertArguments(NAME_CHECK, 1, name); 154 | var test = dropArguments(check, 0, Arrays.stream(newTypes2, 0, 2 + 2 * i).toArray(Class[]::new)); 155 | mh = guardWithTest(test, mh, other); 156 | } 157 | 158 | // install nameCount guard 159 | var check = insertArguments(NAME_COUNT_CHECK, 1, nameCount); 160 | var test = dropArguments(check, 0, Object.class); 161 | mh = guardWithTest(test, mh, other); 162 | 163 | // install the target 164 | setTarget(mh); 165 | 166 | return result; 167 | } 168 | 169 | private static MethodHandle asGetter(Lookup lookup, RecordComponent component) { 170 | try { 171 | return lookup.unreflect(component.getAccessor()); 172 | } catch (IllegalAccessException e) { 173 | throw (LinkageError) new LinkageError().initCause(e); 174 | } 175 | } 176 | 177 | @SuppressWarnings({"fallthrough", "DefaultNotLastCaseInSwitch"}) 178 | private static String[] gatherKeys(int nameCount, String name0, String name1, String name2, String name3, String name4, String name5, String name6, String name7, String name8) { 179 | var names = new String[nameCount]; 180 | switch (nameCount) { 181 | default: 182 | throw new IllegalArgumentException("invalid nameCount " + nameCount); 183 | case 9: 184 | names[8] = name8; // fallthrough 185 | case 8: 186 | names[7] = name7; // fallthrough 187 | case 7: 188 | names[6] = name6; // fallthrough 189 | case 6: 190 | names[5] = name5; // fallthrough 191 | case 5: 192 | names[4] = name4; // fallthrough 193 | case 4: 194 | names[3] = name3; // fallthrough 195 | case 3: 196 | names[2] = name2; // fallthrough 197 | case 2: 198 | names[1] = name1; // fallthrough 199 | case 1: 200 | names[0] = name0; // fallthrough 201 | } 202 | return names; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/JSONTrait.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.io.IOException; 4 | import java.io.Reader; 5 | import java.lang.invoke.MethodHandle; 6 | import java.lang.reflect.UndeclaredThrowableException; 7 | import java.util.Collection; 8 | import java.util.stream.Stream; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | /** 13 | * An interface that provides a method {@link #toJSON()} for any {@link Record record} that 14 | * implements this interface. 15 | * 16 | * Adding this interface add two methods {@link #toJSON()} and {@link #toHumanReadableJSON(String, String)} 17 | * 18 | * record Person(String name, int age) implements JSONTrait { } 19 | * ... 20 | * var person = new Person("Bob", 42); 21 | * System.out.println(person.toJSON()); 22 | * 23 | * 24 | * Moreover, JSONTrait defines two methods to parse a JSON file, {@link #parse(Reader, Class)} to decode 25 | * a JSON Object to a record instance and {@link #stream(Reader, Class)} to decode a JSON Array 26 | * to a {@link Stream} of record instances. 27 | * Both methods are implemented using Jackson but in order to avoid an unnecessary dependency 28 | * if those methods are not used , the dependency to Jackson is declared as optional. 29 | * So to use these methods, you have to add a `requires com.fasterxml.jackson.core;` into your module-info 30 | * and also adds the dependency to `com.fasterxml.jackson.core:jackson-core` in your POM file 31 | * 32 | * 33 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 34 | * in a package in a module which does not open the package to the module 35 | * {@code com.github.forax.recordutil}. 36 | * By example, if the record is declared in a module mymodule in a package mypackage, 37 | * the module-info of this module should contains the following declaration 38 | * 39 | * module mymodule { 40 | * ... 41 | * open mypackage to com.github.forax.recordutil; 42 | * } 43 | * 44 | */ 45 | public interface JSONTrait { 46 | /** 47 | * Returns the current record instance formatted using the JSON format 48 | * @return the current record instance formatted using the JSON format 49 | */ 50 | default String toJSON() { 51 | var builder = new StringBuilder(); 52 | toJSONRecord(builder, this, "", "", ""); 53 | return builder.toString(); 54 | } 55 | 56 | /** 57 | * Returns a human readable text using the JSON format of the current record, 58 | * using \n as line separator and " " as line indent. 59 | * 60 | * This is semantically equivalent to call 61 | * {@code toHumanReadableJSON(" ", "\n")} 62 | * 63 | * @return a human readable text using the JSON format of the current record 64 | * @see #toHumanReadableJSON(String, String) 65 | */ 66 | default String toHumanReadableJSON() { 67 | return toHumanReadableJSON(" ", "\n"); 68 | } 69 | 70 | /** 71 | * Returns a human readable text using the JSON format of the current record 72 | * @param lineIndent number of spaces to increment when entering a JSON Object or a JSON Array 73 | * @param lineSeparator the line separator (e.g. "\n" or "\r\n") 74 | * @return a human readable text using the JSON format of the current record 75 | */ 76 | default String toHumanReadableJSON(String lineIndent, String lineSeparator) { 77 | var builder = new StringBuilder(); 78 | toJSONRecord(builder, this, "", lineIndent, lineSeparator); 79 | return builder.toString(); 80 | } 81 | 82 | private static Object invokeValue(Object object, MethodHandle getter) { 83 | try { 84 | return getter.invokeExact(object); 85 | } catch(RuntimeException | Error e) { 86 | throw e; 87 | } catch (Throwable t) { 88 | throw new UndeclaredThrowableException(t); 89 | } 90 | } 91 | 92 | private static void toJSON(StringBuilder builder, Object o, String linePrefix, String lineIndent, String lineSeparator) { 93 | if (o instanceof Record record) { 94 | toJSONRecord(builder, record, linePrefix, lineIndent, lineSeparator); 95 | return; 96 | } 97 | if (o instanceof Collection> collection) { 98 | toJSONArray(builder, collection, linePrefix, lineIndent, lineSeparator); 99 | return; 100 | } 101 | toJSONPrimitive(builder, o); 102 | } 103 | 104 | private static void toJSONRecord(StringBuilder builder, Object record, String linePrefix, String lineIndent, String lineSeparator) { 105 | var shape = TraitImpl.mapShape(record.getClass()); 106 | builder.append('{'); 107 | var separator = ""; 108 | var innerLinePrefix = linePrefix + lineIndent; 109 | for(var i = 0; i < shape.size(); i++) { 110 | var key = shape.getKey(i); 111 | var value = shape.getValue(i); 112 | builder.append(separator) 113 | .append(lineSeparator).append(innerLinePrefix) 114 | .append('"').append(key).append("\": "); 115 | toJSON(builder, invokeValue(record, value), innerLinePrefix, lineIndent, lineSeparator); 116 | separator = lineSeparator.isEmpty()? ", ": ","; 117 | } 118 | builder.append(lineSeparator).append(linePrefix).append('}'); 119 | } 120 | 121 | private static void toJSONArray(StringBuilder builder, Collection> collection, String linePrefix, String lineIndent, String lineSeparator) { 122 | builder.append('['); 123 | var separator = ""; 124 | var innerLinePrefix = linePrefix + lineIndent; 125 | for(var value: collection) { 126 | builder.append(separator).append(lineSeparator).append(innerLinePrefix); 127 | toJSON(builder, value, innerLinePrefix, lineIndent, lineSeparator); 128 | separator = lineSeparator.isEmpty()? ", ": ","; 129 | } 130 | builder.append(lineSeparator).append(linePrefix).append(']'); 131 | } 132 | 133 | private static void toJSONPrimitive(StringBuilder builder, Object o) { 134 | if (o == null || o instanceof Number || o instanceof Boolean) { 135 | builder.append(o); 136 | return; 137 | } 138 | builder.append('"') 139 | .append(o.toString().replace("\"", "\\\"")) 140 | .append('"'); 141 | } 142 | 143 | /** 144 | * User defined callback to convert a JSON value typed as a {@code String} to a Java value 145 | * of a specific {@code Class}. 146 | * 147 | * By example, to parse dates using {@link java.time.LocalDate}, one can write 148 | * 149 | * Converter converter = (valueAsString, type, downstreamConverter) -> { 150 | * if (type == LocalDate.class) { 151 | * return LocalDate.parse(valueAsString); 152 | * } 153 | * return downstreamConverter.convert(valueAsString, type); 154 | * }; 155 | * 156 | * 157 | * @see #parse(Reader, Class, Converter) 158 | * @see #stream(Reader, Class, Converter) 159 | */ 160 | @FunctionalInterface 161 | interface Converter { 162 | /** 163 | * Default implementation of a converter that knows how to convert Java primitive types. 164 | */ 165 | interface DownStream { 166 | /** 167 | * Convert a JSON value encoded as a String to an object of peculiar Java class. 168 | * 169 | * @param valueAsString JSON value 170 | * @param type a Java class 171 | * @return a value of class {@code Class} 172 | * @throws IOException if the conversion is not possible 173 | */ 174 | Object convert(String valueAsString, Class> type) throws IOException; 175 | } 176 | 177 | /** 178 | * Convert a JSON value encoded as a String to an object of peculiar Java class. 179 | * 180 | * @param valueAsString a JSON value 181 | * @param type a Java class 182 | * @param downstreamConverter an already defined converter that implement the default conversions, 183 | * for primitive values, String, BigInteger and BigDecimal. 184 | * @return a value of class {@code Class} 185 | * @throws IOException if the conversion is not possible 186 | * 187 | * @see Converter.DownStream 188 | */ 189 | Object convert(String valueAsString, Class> type, DownStream downstreamConverter) throws IOException; 190 | } 191 | 192 | private static Converter defaultConverter() { 193 | return (valueAsString, type, downstreamConverter) -> downstreamConverter.convert(valueAsString, type); 194 | } 195 | 196 | /** 197 | * Parse a JSON Object using a record class to guide the decoding. 198 | * 199 | * @param reader the reader containing the JSON 200 | * @param recordType the type of the record to decode 201 | * @param the type of the record 202 | * @return a newly allocated record 203 | * @throws IOException if either an i/o error or a parsing error occur 204 | * 205 | * @see #parse(Reader, Class, Converter) 206 | */ 207 | static R parse(Reader reader, Class extends R> recordType) throws IOException { 208 | return parse(reader, recordType, defaultConverter()); 209 | } 210 | 211 | /** 212 | * Parse a JSON Object using a record class to guide the decoding. 213 | * 214 | * Primitive types, String, BigInteger, BigDecimal, records, List and Set are handled automatically, 215 | * for other types, you have to provide a {@link Converter} that recognize those types. 216 | * 217 | * @param reader the reader containing the JSON 218 | * @param recordType the type of the record to decode 219 | * @param converter a user defined converter to handle user specific conversions 220 | * @param the type of the record 221 | * @return a newly allocated record 222 | * @throws IOException if either an i/o error or a parsing error occur 223 | * 224 | * @see #parse(Reader, Class) 225 | * @see Converter 226 | */ 227 | static R parse(Reader reader, Class extends R> recordType, Converter converter) throws IOException { 228 | requireNonNull(reader, "reader is null"); 229 | requireNonNull(recordType, "recordType is null"); 230 | requireNonNull(converter, "converter is null"); 231 | return recordType.cast(JSONParsing.parse(reader, recordType, converter)); 232 | } 233 | 234 | /** 235 | * Returns a Stream from a JSON Array of Objects using a record class to guide the decoding. 236 | * 237 | * @param reader the reader containing the JSON 238 | * @param recordType the type of the record to decode 239 | * @param the type of the record 240 | * @return a Stream of records 241 | * @throws java.io.UncheckedIOException if either an i/o error or a parsing error occur 242 | * 243 | * @see #stream(Reader, Class, Converter) 244 | */ 245 | static Stream stream(Reader reader, Class extends R> recordType) { 246 | return stream(reader, recordType, defaultConverter()); 247 | } 248 | 249 | /** 250 | * Returns a Stream from a JSON Array of Objects using a record class to guide the decoding. 251 | * 252 | * Primitive types, String, BigInteger, BigDecimal, records, List and Set are handled automatically, 253 | * for other types, you have to provide a {@link Converter} that recognize those types. 254 | * 255 | * @param reader the reader containing the JSON 256 | * @param recordType the type of the record to decode 257 | * @param converter a user defined converter to handle user specific conversions 258 | * @param the type of the record 259 | * @return a Stream of records 260 | * @throws java.io.UncheckedIOException if either an i/o error or a parsing error occur 261 | * 262 | * @see #stream(Reader, Class) 263 | * @see Converter 264 | */ 265 | static Stream stream(Reader reader, Class extends R> recordType, Converter converter) { 266 | requireNonNull(reader, "reader is null"); 267 | requireNonNull(recordType, "recordType is null"); 268 | requireNonNull(converter, "converter is null"); 269 | return JSONParsing.stream(reader, recordType, converter); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/TraitImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodHandles.Lookup; 6 | import java.lang.invoke.MethodType; 7 | import java.lang.reflect.RecordComponent; 8 | import java.util.Arrays; 9 | 10 | import static java.lang.invoke.MethodType.methodType; 11 | 12 | class TraitImpl { 13 | 14 | /** 15 | * Combine a hash table ({@code table}) that stores a pair String/MethodHandle with 16 | * a list {@code vec} that stores the same pair. 17 | * 18 | * 19 | * to insert a pair uses {@link #put(int, String, MethodHandle)} 20 | * to know the number of pairs uses {@link #size()} 21 | * to get the value from a key uses {@link #getValue(String)} 22 | * to get the key from an index uses {@link #getKey(int)} 23 | * to get the value from an index uses {@link #getValue(int)} 24 | * 25 | */ 26 | record MapShape(Object[] table, Object[] vec) { 27 | MapShape(int capacity) { 28 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new Object[capacity << 1]); 29 | } 30 | 31 | void put(int index, String key, MethodHandle getter) { 32 | var slot = -probe(key) - 1; 33 | table[slot] = key; 34 | table[slot + 1] = getter; 35 | vec[index << 1] = key; 36 | vec[(index << 1) + 1] = getter; 37 | } 38 | 39 | MethodHandle getValue(String key) { 40 | var slot = probe(key); 41 | if (slot < 0) { 42 | return null; 43 | } 44 | return (MethodHandle) table[slot + 1]; 45 | } 46 | 47 | boolean containsKey(String key) { 48 | return probe(key) >= 0; 49 | } 50 | 51 | int size() { 52 | return vec.length >> 1; 53 | } 54 | boolean isEmpty() { 55 | return vec.length == 0; 56 | } 57 | 58 | String getKey(int index) { 59 | return (String) vec[index << 1]; 60 | } 61 | MethodHandle getValue(int index) { 62 | return (MethodHandle) vec[(index << 1) + 1]; 63 | } 64 | 65 | private int probe(String key) { 66 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 67 | var k = table[slot]; 68 | if (k == null) { 69 | return -slot - 1; 70 | } 71 | if (key.equals(k)) { 72 | return slot; 73 | } 74 | return probe2(key, slot); 75 | } 76 | 77 | private int probe2(String key, int slot) { 78 | for(;;) { 79 | slot = (slot + 2) & (table.length - 1); 80 | var k = table[slot]; 81 | if (k == null) { 82 | return -slot - 1; 83 | } 84 | if (key.equals(k)) { 85 | return slot; 86 | } 87 | } 88 | } 89 | } 90 | 91 | private static final ClassValue SHAPE_MAP = new ClassValue<>() { 92 | @Override 93 | protected MapShape computeValue(Class> type) { 94 | var components = type.getRecordComponents(); 95 | if (components == null) { 96 | throw new IllegalStateException(type.getName() + " is not a record"); 97 | } 98 | var lookup = teleport(type, MethodHandles.lookup()); 99 | 100 | var shape = new MapShape(components.length); 101 | for(var i = 0; i < components.length; i++) { 102 | var component = components[i]; 103 | var getter = asMH(lookup, component).asType(methodType(Object.class, Object.class)); 104 | shape.put(i, component.getName(), getter); 105 | } 106 | return shape; 107 | } 108 | }; 109 | 110 | private static Lookup teleport(Class> type, Lookup localLookup) { 111 | // add read access to the type module 112 | localLookup.lookupClass().getModule().addReads(type.getModule()); 113 | 114 | // then teleport 115 | try { 116 | return MethodHandles.privateLookupIn(type, localLookup); 117 | } catch (IllegalAccessException e) { 118 | throw (IllegalAccessError) new IllegalAccessError(""" 119 | the module %s does not open the package %s to the module com.github.forax.recordutil 120 | you can add this incantation to the module-info 121 | module %s { 122 | ... 123 | opens %s to com.github.forax.recordutil; 124 | } 125 | """.formatted(type.getModule(), type.getPackageName(), type.getModule(), type.getPackageName()) 126 | ).initCause(e); 127 | } 128 | } 129 | 130 | private static MethodHandle asMH(Lookup lookup, RecordComponent component) { 131 | try { 132 | return lookup.unreflect(component.getAccessor()); 133 | } catch (IllegalAccessException e) { 134 | throw new AssertionError(e); 135 | } 136 | } 137 | 138 | /** 139 | * Returns a hash/list describing the associating between a record component 140 | * name and its corresponding getter as a method handle. 141 | * 142 | * @param type the class of the record 143 | * @return the hash/list describing the record 144 | */ 145 | static MapShape mapShape(Class> type) { 146 | return SHAPE_MAP.get(type); 147 | } 148 | 149 | 150 | /** 151 | * Combine a hash table ({@code table}) that stores an index ({@code slot}) for 152 | * a String and a list that stores at the index ({@code slot}) 153 | * the corresponding MethodHandle. 154 | * It also stores the constructor as a method handle. 155 | * 156 | * 157 | * to insert a pair uses {@link #put(int, String, MethodHandle)} 158 | * to know the number of key/value uses {@link #size()} 159 | * to get the slot (index) from a key uses {@link #getSlot(String)} 160 | * to get the value from a slot (index) uses {@link #getValue(int)} 161 | * 162 | */ 163 | record WithShape(Object[] table, MethodHandle[] vec, MethodHandle constructor) { 164 | WithShape(int capacity, MethodHandle constructor) { 165 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new MethodHandle[capacity], constructor); 166 | } 167 | 168 | void put(int index, String key, MethodHandle getter) { 169 | var slot = -probe(key) - 1; 170 | table[slot] = key; 171 | table[slot + 1] = index; 172 | vec[index] = getter; 173 | } 174 | 175 | int getSlot(String key) { 176 | var slot = probe(key); 177 | if (slot < 0) { 178 | return -1; 179 | } 180 | return (int) table[slot + 1]; 181 | } 182 | 183 | int size() { 184 | return vec.length; 185 | } 186 | 187 | MethodHandle getValue(int index) { 188 | return vec[index]; 189 | } 190 | 191 | private int probe(String key) { 192 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 193 | var k = table[slot]; 194 | if (k == null) { 195 | return -slot - 1; 196 | } 197 | if (key.equals(k)) { 198 | return slot; 199 | } 200 | return probe2(key, slot); 201 | } 202 | 203 | private int probe2(String key, int slot) { 204 | for(;;) { 205 | slot = (slot + 2) & (table.length - 1); 206 | var k = table[slot]; 207 | if (k == null) { 208 | return -slot - 1; 209 | } 210 | if (key.equals(k)) { 211 | return slot; 212 | } 213 | } 214 | } 215 | } 216 | 217 | private static final ClassValue WITH_SHAPE_MAP = new ClassValue<>() { 218 | @Override 219 | protected WithShape computeValue(Class> type) { 220 | var components = type.getRecordComponents(); 221 | if (components == null) { 222 | throw new IllegalStateException(type.getName() + " is not a record"); 223 | } 224 | var lookup = teleport(type, MethodHandles.lookup()); 225 | var constructor = asConstructor(lookup, type, components) 226 | .asType(MethodType.genericMethodType(components.length)) 227 | .asSpreader(Object[].class, components.length); 228 | 229 | var shape = new WithShape(components.length, constructor); 230 | for(var i = 0; i < components.length; i++) { 231 | var component = components[i]; 232 | var getter = asMH(lookup, component).asType(methodType(Object.class, Object.class)); 233 | shape.put(i, component.getName(), getter); 234 | } 235 | return shape; 236 | } 237 | }; 238 | 239 | private static MethodHandle asConstructor(Lookup lookup, Class> type, RecordComponent[] components) { 240 | try { 241 | return lookup.findConstructor(type, methodType(void.class, Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new))); 242 | } catch (IllegalAccessException | NoSuchMethodException e) { 243 | throw new AssertionError(e); 244 | } 245 | } 246 | 247 | /** 248 | * Returns a hash/list describing the associating between a record component 249 | * name and its corresponding getter as a method handle and 250 | * the constructor. 251 | * 252 | * @param type the class of the record 253 | * @return the hash/list describing the record 254 | */ 255 | static WithShape withShape(Class> type) { 256 | return WITH_SHAPE_MAP.get(type); 257 | } 258 | 259 | 260 | /** 261 | * Combine a hash table ({@code table}) that stores an index ({@code slot}) for 262 | * a String and a list that stores at the index ({@code slot}) 263 | * the corresponding type ({@code Class}). 264 | * It also stores the constructor as a method handle. 265 | * 266 | * 267 | * to insert a pair uses {@link #put(int, String, Class)} 268 | * to know the number of key/value uses {@link #size()} 269 | * to get the slot (index) from a key uses {@link #getSlot(String)} 270 | * to get the type from a slot (index) uses {@link #getType(int)} 271 | * 272 | */ 273 | record JSONShape(Object[] table, Class>[] vec, MethodHandle constructor) { 274 | JSONShape(int capacity, MethodHandle constructor) { 275 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new Class>[capacity], constructor); 276 | } 277 | 278 | void put(int index, String key, Class> type) { 279 | var slot = -probe(key) - 1; 280 | table[slot] = key; 281 | table[slot + 1] = index; 282 | vec[index] = type; 283 | } 284 | 285 | int getSlot(String key) { 286 | var slot = probe(key); 287 | if (slot < 0) { 288 | return -1; 289 | } 290 | return (int) table[slot + 1]; 291 | } 292 | 293 | int size() { 294 | return vec.length; 295 | } 296 | 297 | Class> getType(int index) { 298 | return vec[index]; 299 | } 300 | 301 | private int probe(String key) { 302 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 303 | var k = table[slot]; 304 | if (k == null) { 305 | return -slot - 1; 306 | } 307 | if (key.equals(k)) { 308 | return slot; 309 | } 310 | return probe2(key, slot); 311 | } 312 | 313 | private int probe2(String key, int slot) { 314 | for(;;) { 315 | slot = (slot + 2) & (table.length - 1); 316 | var k = table[slot]; 317 | if (k == null) { 318 | return -slot - 1; 319 | } 320 | if (key.equals(k)) { 321 | return slot; 322 | } 323 | } 324 | } 325 | } 326 | 327 | private static final ClassValue JSON_SHAPE_MAP = new ClassValue<>() { 328 | @Override 329 | protected JSONShape computeValue(Class> type) { 330 | var components = type.getRecordComponents(); 331 | if (components == null) { 332 | throw new IllegalStateException(type.getName() + " is not a record"); 333 | } 334 | var lookup = teleport(type, MethodHandles.lookup()); 335 | var constructor = asConstructor(lookup, type, components) 336 | .asType(MethodType.genericMethodType(components.length)) 337 | .asSpreader(Object[].class, components.length); 338 | 339 | var shape = new JSONShape(components.length, constructor); 340 | for(var i = 0; i < components.length; i++) { 341 | var component = components[i]; 342 | shape.put(i, component.getName(), component.getType()); 343 | } 344 | return shape; 345 | } 346 | }; 347 | 348 | /** 349 | * Returns a hash/list describing the associating between a record component 350 | * name and its corresponding type (@code Class) and the constructor. 351 | * 352 | * @param type the class of the record 353 | * @return the hash/list describing the record 354 | */ 355 | static JSONShape jsonShape(Class> type) { 356 | return JSON_SHAPE_MAP.get(type); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/MapTrait.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.UndeclaredThrowableException; 5 | import java.util.AbstractList; 6 | import java.util.AbstractSet; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | import java.util.Set; 13 | import java.util.StringJoiner; 14 | import java.util.function.BiConsumer; 15 | import java.util.function.BiFunction; 16 | import java.util.function.Function; 17 | 18 | /** 19 | * An interface that provides an implementation for all methods of an unmodifiable {@link Map} 20 | * if the class that implements that interface is a {@link Record record}. 21 | * 22 | * Adding this interface transforms any records to a {@link Map}. 23 | * 24 | * record Person(String name, int age) implements MapTrait { } 25 | * ... 26 | * Map<String, Object> map = new Person("Bob", 42); 27 | * 28 | * 29 | * Sadly, an interface can not override the method {@code equals], {@code hashCode} 30 | * or {@code toString} using a default method so to get an implementation 31 | * of {@link Map} compatible with {@link java.util.AbstractMap}, those methods has 32 | * to be overridden by hand. 33 | * 34 | * record Person(String name, int age) implements MapTrait { 35 | * @Override 36 | * public boolean equals(Object o) { 37 | * return MapTrait.super.equalsOfMap(o); 38 | * } 39 | * 40 | * @Override 41 | * public int hashCode() { 42 | * return MapTrait.super.hashCodeOfMap(); 43 | * } 44 | * 45 | * @Override 46 | * public String toString() { 47 | * return MapTrait.super.toStringOfMap(); 48 | * } 49 | * } 50 | * 51 | * 52 | * 53 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 54 | * in a package in a module which does not open the package to the module 55 | * {@code com.github.forax.recordutil}. 56 | * By example, if the record is declared in a module mymodule in a package mypackage, 57 | * the module-info of this module should contains the following declaration 58 | * 59 | * module mymodule { 60 | * ... 61 | * open mypackage to com.github.forax.recordutil; 62 | * } 63 | * 64 | * 65 | * 66 | * This implementation guarantee that the methods {@link #entrySet()}, {@link #keySet()}, {@link #keys()} 67 | * and {@link #values()} provide the keys and values in the record components order. 68 | * 69 | * That's why the method {@link #keys()} and {@link #values()} returns a {@link List} instead of returning 70 | * a {@link java.util.Collection}. 71 | */ 72 | public interface MapTrait extends java.util.Map { 73 | @Override 74 | default int size() { 75 | return TraitImpl.mapShape(getClass()).size(); 76 | } 77 | @Override 78 | default boolean isEmpty() { 79 | return TraitImpl.mapShape(getClass()).isEmpty(); 80 | } 81 | 82 | @Override 83 | default Object get(Object key) { 84 | return getOrDefault(key, null); 85 | } 86 | @Override 87 | default Object getOrDefault(Object key, Object defaultValue) { 88 | if (!(key instanceof String s)) { 89 | return defaultValue; 90 | } 91 | var shape = TraitImpl.mapShape(getClass()); 92 | var getter = shape.getValue(s); 93 | if (getter == null) { 94 | return defaultValue; 95 | } 96 | return invokeValue(getter); 97 | } 98 | 99 | private Object invokeValue(MethodHandle getter) { 100 | try { 101 | return getter.invokeExact((Object) this); 102 | } catch(RuntimeException | Error e) { 103 | throw e; 104 | } catch (Throwable t) { 105 | throw new UndeclaredThrowableException(t); 106 | } 107 | } 108 | 109 | @Override 110 | default boolean containsKey(Object key) { 111 | if (!(key instanceof String s)) { 112 | return false; 113 | } 114 | var shape = TraitImpl.mapShape(getClass()); 115 | return shape.containsKey(s); 116 | } 117 | 118 | @Override 119 | default boolean containsValue(Object value) { 120 | var shape = TraitImpl.mapShape(getClass()); 121 | for(var i = 0; i < shape.size(); i++) { 122 | var getter = shape.getValue(i); 123 | if (Objects.equals(invokeValue(getter), value)) { 124 | return true; 125 | } 126 | } 127 | return false; 128 | } 129 | 130 | /** 131 | * The default implementation of this method comes from {@code java.lang.Record} 132 | * thus does not obey to the general contract of {@link Map#equals(Object)}. 133 | * 134 | * In your record, you can change the implementation by adding 135 | * 136 | * public boolean equals(Object o) { 137 | * return MapTrait.super.equalsOfMap(o); 138 | * } 139 | * 140 | * to get an implementation that behave like a {@link Map}. 141 | * 142 | * {@inheritDoc} 143 | * 144 | * @see #equalsOfMap(Object) 145 | */ 146 | @Override 147 | boolean equals(Object o); 148 | 149 | /** 150 | * The default implementation of this method comes from {@code java.lang.Record} 151 | * thus does not obey to the general contract of {@link Map#hashCode()}. 152 | * 153 | * In your record, you can change the implementation by adding 154 | * 155 | * public int hashCode() { 156 | * return MapTrait.super.hashCodeOfMap(); 157 | * } 158 | * 159 | * to get an implementation that behave like a {@link Map}. 160 | * 161 | * {@inheritDoc} 162 | * 163 | * @see #hashCodeOfMap() 164 | */ 165 | @Override 166 | int hashCode(); 167 | 168 | /** 169 | * The default implementation of this method comes from {@code java.lang.Record} 170 | * thus does not obey to the general contract of {@link java.util.AbstractMap#toString()}. 171 | * 172 | * In your record, you can change the implementation by adding 173 | * 174 | * public String toString() { 175 | * return MapTrait.super.toStringOfMap(); 176 | * } 177 | * 178 | * to get an implementation that behave like a {@link Map}. 179 | * 180 | * {@inheritDoc} 181 | * 182 | * @see #toStringOfMap() 183 | */ 184 | @Override 185 | String toString(); 186 | 187 | /** 188 | * Compares the specified object with this map for equality. Returns 189 | * {@code true} if the given object is also a map and the two maps 190 | * represent the same mappings. More formally, two maps {@code m1} and 191 | * {@code m2} represent the same mappings if 192 | * {@code m1.entrySet().equals(m2.entrySet())}. This ensures that the 193 | * {@code equals} method works properly across different implementations 194 | * of the {@code Map} interface. 195 | * 196 | * @param o object to be compared for equality with this map 197 | * @return {@code true} if the specified object is equal to this map 198 | * 199 | * @see #equals(Object) 200 | * @see Map#equals(Object) 201 | */ 202 | default boolean equalsOfMap(Object o) { 203 | return o instanceof Map,?> map && equalsOfMap(map); 204 | } 205 | 206 | private boolean equalsOfMap(Map,?> map) { 207 | var shape = TraitImpl.mapShape(getClass()); 208 | for(var i = 0; i < shape.size(); i++) { 209 | var value = map.get(shape.getKey(i)); 210 | if (!Objects.equals(invokeValue(shape.getValue(i)), value)) { 211 | return false; 212 | } 213 | } 214 | return true; 215 | } 216 | 217 | /** 218 | * Returns the hash code value for this map. The hash code of a map is 219 | * defined to be the sum of the hash codes of each entry in the map's 220 | * {@code entrySet()} view. This ensures that {@code m1.equals(m2)} 221 | * implies that {@code m1.hashCode()==m2.hashCode()} for any two maps 222 | * {@code m1} and {@code m2}, as required by the general contract of 223 | * {@link Object#hashCode}. 224 | * 225 | * @return the hash code value for this map 226 | * 227 | * @see #hashCode() 228 | * @see Map#hashCode() 229 | */ 230 | default int hashCodeOfMap() { 231 | var shape = TraitImpl.mapShape(getClass()); 232 | var h = 0; 233 | for (var i = 0; i < shape.size(); i++) { 234 | var value = invokeValue(shape.getValue(i)); 235 | h += shape.getKey(i).hashCode() ^ Objects.hashCode(value); 236 | } 237 | return h; 238 | } 239 | 240 | /** 241 | * Returns a string representation of this map. The string representation 242 | * consists of a list of key-value mappings in the order returned by the 243 | * map's {@code entrySet} view's iterator, enclosed in braces 244 | * ({@code "{}"}). Adjacent mappings are separated by the characters 245 | * {@code ", "} (comma and space). Each key-value mapping is rendered as 246 | * the key followed by an equals sign ({@code "="}) followed by the 247 | * associated value. Keys and values are converted to strings as by 248 | * {@link String#valueOf(Object)}. 249 | * 250 | * @return a string representation of this map 251 | * 252 | * @see #toString() 253 | * @see Map#toString() 254 | */ 255 | default String toStringOfMap() { 256 | var shape = TraitImpl.mapShape(getClass()); 257 | var joiner = new StringJoiner(", ", "{", "}"); 258 | for (var i = 0; i < shape.size(); i++) { 259 | joiner.add(shape.getKey(i) + "=" + invokeValue(shape.getValue(i))); 260 | } 261 | return joiner.toString(); 262 | } 263 | 264 | @Override 265 | default void forEach(BiConsumer super String, ? super Object> action) { 266 | var shape = TraitImpl.mapShape(getClass()); 267 | for (var i = 0; i < shape.size(); i++) { 268 | action.accept(shape.getKey(i), invokeValue(shape.getValue(i))); 269 | } 270 | } 271 | 272 | /** 273 | * Returns an unmodifiable {@link Set} view of the mappings contained in this map. 274 | * 275 | * @return a set view of the mappings contained in this map 276 | */ 277 | @Override 278 | default Set> entrySet() { 279 | var shape = TraitImpl.mapShape(getClass()); 280 | return new AbstractSet<>() { 281 | @Override 282 | public int size() { 283 | return shape.size(); 284 | } 285 | 286 | @Override 287 | public Iterator> iterator() { 288 | return new Iterator<>() { 289 | private int index; 290 | 291 | @Override 292 | public boolean hasNext() { 293 | return index < shape.size(); 294 | } 295 | 296 | @Override 297 | public Entry next() { 298 | if (!hasNext()) { 299 | throw new NoSuchElementException(); 300 | } 301 | return Map.entry(shape.getKey(index), invokeValue(shape.getValue(index++))); 302 | } 303 | }; 304 | } 305 | 306 | @Override 307 | public boolean contains(Object o) { 308 | if (!(o instanceof Map.Entry,?> e)) { 309 | return false; 310 | } 311 | return Objects.equals(get(e.getKey()), e.getValue()); 312 | } 313 | }; 314 | } 315 | 316 | /** 317 | * Returns an unmodifiable {@link Set} view of the keys contained in this map. 318 | * 319 | * @return a set view of the keys contained in this map 320 | */ 321 | @Override 322 | default Set keySet() { 323 | var shape = TraitImpl.mapShape(getClass()); 324 | return new AbstractSet<>() { 325 | @Override 326 | public int size() { 327 | return shape.size(); 328 | } 329 | 330 | @Override 331 | public Iterator iterator() { 332 | return new Iterator<>() { 333 | private int index; 334 | 335 | @Override 336 | public boolean hasNext() { 337 | return index < shape.size(); 338 | } 339 | 340 | @Override 341 | public String next() { 342 | if (!hasNext()) { 343 | throw new NoSuchElementException(); 344 | } 345 | return shape.getKey(index++); 346 | } 347 | }; 348 | } 349 | 350 | @Override 351 | public boolean contains(Object o) { 352 | return containsKey(o); 353 | } 354 | }; 355 | } 356 | 357 | /** 358 | * Returns an unmodifiable {@link List} view of the values contained in this map. 359 | * 360 | * @return an unmodifiable list of the values contained in this map 361 | */ 362 | @Override 363 | default List values() { 364 | var shape = TraitImpl.mapShape(getClass()); 365 | return new AbstractList<>() { 366 | @Override 367 | public int size() { 368 | return shape.size(); 369 | } 370 | 371 | @Override 372 | public Object get(int index) { 373 | Objects.checkIndex(index, shape.size()); 374 | return invokeValue(shape.getValue(index)); 375 | } 376 | }; 377 | } 378 | 379 | /** 380 | * Returns an unmodifiable {@link List} view of the keys contained in this map. 381 | * 382 | * @return an unmodifiable list of the keys contained in this map 383 | */ 384 | default List keys() { 385 | var shape = TraitImpl.mapShape(getClass()); 386 | return new AbstractList<>() { 387 | @Override 388 | public int size() { 389 | return shape.size(); 390 | } 391 | 392 | @Override 393 | public String get(int index) { 394 | Objects.checkIndex(index, shape.size()); 395 | return shape.getKey(index); 396 | } 397 | 398 | @Override 399 | public boolean contains(Object o) { 400 | return containsKey(o); 401 | } 402 | }; 403 | } 404 | 405 | @Override 406 | default void clear() { 407 | throw new UnsupportedOperationException(); 408 | } 409 | @Override 410 | default Object put(String key, Object value) { 411 | throw new UnsupportedOperationException(); 412 | } 413 | @Override 414 | default void putAll(Map extends String, ?> map) { 415 | throw new UnsupportedOperationException(); 416 | } 417 | 418 | @Override 419 | default Object remove(Object key) { 420 | throw new UnsupportedOperationException(); 421 | } 422 | @Override 423 | default boolean remove(Object key, Object value) { 424 | throw new UnsupportedOperationException(); 425 | } 426 | 427 | @Override 428 | default Object replace(String key, Object value) { 429 | throw new UnsupportedOperationException(); 430 | } 431 | @Override 432 | default boolean replace(String key, Object oldValue, Object newValue) { 433 | throw new UnsupportedOperationException(); 434 | } 435 | @Override 436 | default void replaceAll(BiFunction super String, ? super Object, ?> function) { 437 | throw new UnsupportedOperationException(); 438 | } 439 | 440 | @Override 441 | default Object compute(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 442 | throw new UnsupportedOperationException(); 443 | } 444 | @Override 445 | default Object computeIfAbsent(String key, Function super String, ?> mappingFunction) { 446 | throw new UnsupportedOperationException(); 447 | } 448 | @Override 449 | default Object computeIfPresent(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 450 | throw new UnsupportedOperationException(); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/Wither.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandles.Lookup; 4 | 5 | import static java.util.Objects.requireNonNull; 6 | 7 | /** 8 | * An efficient mechanism to create a record from an existing record object by updating several components. 9 | * 10 | * To be efficient, a {@code Wither} should be stored as a constant, by example as a static final field. 11 | * 12 | * record Person(String name, int age) {} 13 | * ... 14 | * private static final Wither<Person> wither = Wither.of(MethodHandles.lookup(), Person.class); 15 | * ... 16 | * var bob = new Person("Bob", 42); 17 | * var ana = wither.with(bob, "name", "Ana"); // create a Person with the name "Ana" and the same age as bob 18 | * 19 | * 20 | * 21 | * The implementation first extract the "shape" of the call to {@code with} (the number of components 22 | * and the name of each component to update) and generates a specialized code that calls all the getters 23 | * of the components that are not updated in the order of the record declaration then 24 | * re-create a new record by calling the constructor with all the values. 25 | * 26 | * Two calls with the same "shape" will reuse the same code, if there are different shapes, 27 | * multiple specialized codes are generated and the JIT will only keep the right specialized code for a call. 28 | * 29 | * Given the fact that the implementation keeps all specialized codes that have been seen at least once, 30 | * one {code Wither} may store a lot of metadata to the point the JIT will give up to try to optimize the code. 31 | * This should not be the code with handwritten code but can be problematic with generated Java codes. 32 | * 33 | * @param the type of the record to update. 34 | * 35 | * @see WithTrait 36 | */ 37 | @FunctionalInterface 38 | public interface Wither { 39 | /** 40 | * Returns a new record instance using the {@code nameCount} names and values to update the record instance 41 | * taken as first parameter. 42 | * This method should not be called directly, it's better to use one of the method {@code with} variants. 43 | * 44 | * @param record a record instance 45 | * @param nameCount the number of names that will used 46 | * @param name0 a name of a record component 47 | * @param value0 the value of the record component {@code name0} 48 | * @param name1 a name of a record component 49 | * @param value1 the value of the record component {@code name1} 50 | * @param name2 a name of a record component 51 | * @param value2 the value of the record component {@code name2} 52 | * @param name3 a name of a record component 53 | * @param value3 the value of the record component {@code name3} 54 | * @param name4 a name of a record component 55 | * @param value4 the value of the record component {@code name4} 56 | * @param name5 a name of a record component 57 | * @param value5 the value of the record component {@code name5} 58 | * @param name6 a name of a record component 59 | * @param value6 the value of the record component {@code name6} 60 | * @param name7 a name of a record component 61 | * @param value7 the value of the record component {@code name7} 62 | * @param name8 a name of a record component 63 | * @param value8 the value of the record component {@code name8} 64 | * 65 | * @throws NullPointerException if the record is null or one of the names is null 66 | * @throws IllegalArgumentException if {@code nameCount} is not between 1 and 9, if a name is not the name of one 67 | * of the record component of the record, if a name is not a constant string or if two names are the same. 68 | * @throws ClassCastException if a value class is not compatible with the record component type 69 | * 70 | * @return a new record instance with the updated values 71 | * 72 | * @see #with(Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object) 73 | */ 74 | R invoke(R record, int nameCount, 75 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 76 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 77 | String name6, Object value6, String name7, Object value7, String name8, Object value8); 78 | 79 | /** 80 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 81 | * {@code name3}, {@code name4}, {@code name5}, {@code name6}, {@code name7} and {@code name8} being updated to 82 | * the value {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, 83 | * {@code value6}, {@code value7} and {@code value8} respectively. 84 | * 85 | * @param record a record instance 86 | * @param name0 a name of a record component 87 | * @param value0 the value of the record component {@code name0} 88 | * @param name1 a name of a record component 89 | * @param value1 the value of the record component {@code name1} 90 | * @param name2 a name of a record component 91 | * @param value2 the value of the record component {@code name2} 92 | * @param name3 a name of a record component 93 | * @param value3 the value of the record component {@code name3} 94 | * @param name4 a name of a record component 95 | * @param value4 the value of the record component {@code name4} 96 | * @param name5 a name of a record component 97 | * @param value5 the value of the record component {@code name5} 98 | * @param name6 a name of a record component 99 | * @param value6 the value of the record component {@code name6} 100 | * @param name7 a name of a record component 101 | * @param value7 the value of the record component {@code name7} 102 | * @param name8 a name of a record component 103 | * @param value8 the value of the record component {@code name8} 104 | * 105 | * @throws NullPointerException if the record is null or one of the names is null 106 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 107 | * if a name is not a constant string or if two names are the same. 108 | * @throws ClassCastException if a value class is not compatible with the record component type 109 | * 110 | * @return a new record instance with the updated values 111 | */ 112 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 113 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 114 | String name6, Object value6, String name7, Object value7, String name8, Object value8) { 115 | return invoke(record, 9, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 116 | } 117 | 118 | /** 119 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 120 | * {@code name3}, {@code name4}, {@code name5}, {@code name6} and {@code name7} being updated to the value 121 | * {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, {@code value6} 122 | * and {@code value7} respectively. 123 | * 124 | * @param record a record instance 125 | * @param name0 a name of a record component 126 | * @param value0 the value of the record component {@code name0} 127 | * @param name1 a name of a record component 128 | * @param value1 the value of the record component {@code name1} 129 | * @param name2 a name of a record component 130 | * @param value2 the value of the record component {@code name2} 131 | * @param name3 a name of a record component 132 | * @param value3 the value of the record component {@code name3} 133 | * @param name4 a name of a record component 134 | * @param value4 the value of the record component {@code name4} 135 | * @param name5 a name of a record component 136 | * @param value5 the value of the record component {@code name5} 137 | * @param name6 a name of a record component 138 | * @param value6 the value of the record component {@code name6} 139 | * @param name7 a name of a record component 140 | * @param value7 the value of the record component {@code name7} 141 | * 142 | * @throws NullPointerException if the record is null or one of the names is null 143 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 144 | * if a name is not a constant string or if two names are the same. 145 | * @throws ClassCastException if a value class is not compatible with the record component type 146 | * 147 | * @return a new record instance with the updated values 148 | */ 149 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 150 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 151 | String name6, Object value6, String name7, Object value7) { 152 | return invoke(record, 8, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, null, null); 153 | } 154 | 155 | /** 156 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 157 | * {@code name3}, {@code name4}, {@code name5} and {@code name6} being updated to the value {@code value0}, 158 | * {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5} and {@code value6} respectively. 159 | * 160 | * @param record a record instance 161 | * @param name0 a name of a record component 162 | * @param value0 the value of the record component {@code name0} 163 | * @param name1 a name of a record component 164 | * @param value1 the value of the record component {@code name1} 165 | * @param name2 a name of a record component 166 | * @param value2 the value of the record component {@code name2} 167 | * @param name3 a name of a record component 168 | * @param value3 the value of the record component {@code name3} 169 | * @param name4 a name of a record component 170 | * @param value4 the value of the record component {@code name4} 171 | * @param name5 a name of a record component 172 | * @param value5 the value of the record component {@code name5} 173 | * @param name6 a name of a record component 174 | * @param value6 the value of the record component {@code name6} 175 | * 176 | * @throws NullPointerException if the record is null or one of the names is null 177 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 178 | * if a name is not a constant string or if two names are the same. 179 | * @throws ClassCastException if a value class is not compatible with the record component type 180 | * 181 | * @return a new record instance with the updated values 182 | */ 183 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 184 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 185 | String name6, Object value6) { 186 | return invoke(record, 7, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, null, null, null, null); 187 | } 188 | 189 | /** 190 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 191 | * {@code name3}, {@code name4} and {@code name5} being updated to the value {@code value0}, {@code value1}, 192 | * {@code value2}, {@code value3}, {@code value4} and {@code value5} respectively. 193 | * 194 | * @param record a record instance 195 | * @param name0 a name of a record component 196 | * @param value0 the value of the record component {@code name0} 197 | * @param name1 a name of a record component 198 | * @param value1 the value of the record component {@code name1} 199 | * @param name2 a name of a record component 200 | * @param value2 the value of the record component {@code name2} 201 | * @param name3 a name of a record component 202 | * @param value3 the value of the record component {@code name3} 203 | * @param name4 a name of a record component 204 | * @param value4 the value of the record component {@code name4} 205 | * @param name5 a name of a record component 206 | * @param value5 the value of the record component {@code name5} 207 | * 208 | * @throws NullPointerException if the record is null or one of the names is null 209 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 210 | * if a name is not a constant string or if two names are the same. 211 | * @throws ClassCastException if a value class is not compatible with the record component type 212 | * 213 | * @return a new record instance with the updated values 214 | */ 215 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 216 | String name3, Object value3, String name4, Object value4, String name5, Object value5) { 217 | return invoke(record, 6, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, null, null, null, null, null, null); 218 | } 219 | 220 | /** 221 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 222 | * {@code name3} and {@code name4} being updated to the value {@code value0}, {@code value1}, {@code value2}, 223 | * {@code value3} and {@code value4} respectively. 224 | * 225 | * @param record a record instance 226 | * @param name0 a name of a record component 227 | * @param value0 the value of the record component {@code name0} 228 | * @param name1 a name of a record component 229 | * @param value1 the value of the record component {@code name1} 230 | * @param name2 a name of a record component 231 | * @param value2 the value of the record component {@code name2} 232 | * @param name3 a name of a record component 233 | * @param value3 the value of the record component {@code name3} 234 | * @param name4 a name of a record component 235 | * @param value4 the value of the record component {@code name4} 236 | * 237 | * @throws NullPointerException if the record is null or one of the names is null 238 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 239 | * if a name is not a constant string or if two names are the same. 240 | * @throws ClassCastException if a value class is not compatible with the record component type 241 | * 242 | * @return a new record instance with the updated values 243 | */ 244 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 245 | String name3, Object value3, String name4, Object value4) { 246 | return invoke(record, 5, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, null, null, null, null, null, null, null, null); 247 | } 248 | 249 | /** 250 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2} and 251 | * {@code name3} being updated to the value {@code value0}, {@code value1}, {@code value2} and 252 | * {@code value3} respectively. 253 | * 254 | * @param record a record instance 255 | * @param name0 a name of a record component 256 | * @param value0 the value of the record component {@code name0} 257 | * @param name1 a name of a record component 258 | * @param value1 the value of the record component {@code name1} 259 | * @param name2 a name of a record component 260 | * @param value2 the value of the record component {@code name2} 261 | * @param name3 a name of a record component 262 | * @param value3 the value of the record component {@code name3} 263 | * 264 | * @throws NullPointerException if the record is null or one of the names is null 265 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 266 | * if a name is not a constant string or if two names are the same. 267 | * @throws ClassCastException if a value class is not compatible with the record component type 268 | * 269 | * @return a new record instance with the updated values 270 | */ 271 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 272 | String name3, Object value3) { 273 | return invoke(record, 4, name0, value0, name1, value1, name2, value2, name3, value3, null, null, null, null, null, null, null, null, null, null); 274 | } 275 | 276 | /** 277 | * Returns a new record instance with the record components named {@code name0}, {@code name1} and {@code name2} 278 | * being updated to the value {@code value0}, {@code value1} and {@code value2} respectively. 279 | * 280 | * @param record a record instance 281 | * @param name0 a name of a record component 282 | * @param value0 the value of the record component {@code name0} 283 | * @param name1 a name of a record component 284 | * @param value1 the value of the record component {@code name1} 285 | * @param name2 a name of a record component 286 | * @param value2 the value of the record component {@code name2} 287 | * 288 | * @throws NullPointerException if the record is null or one of the names is null 289 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 290 | * if a name is not a constant string or if two names are the same. 291 | * @throws ClassCastException if a value class is not compatible with the record component type 292 | * 293 | * @return a new record instance with the updated values 294 | */ 295 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2) { 296 | return invoke(record, 3, name0, value0, name1, value1, name2, value2, null, null, null, null, null, null, null, null, null, null, null, null); 297 | } 298 | 299 | /** 300 | * Returns a new record instance with the record components named {@code name0} and {@code name1} 301 | * being updated to the value {@code value0} and {@code value1} respectively. 302 | * 303 | * @param record a record instance 304 | * @param name0 a name of a record component 305 | * @param value0 the value of the record component {@code name0} 306 | * @param name1 a name of a record component 307 | * @param value1 the value of the record component {@code name1} 308 | * 309 | * @throws NullPointerException if the record is null or one of the names is null 310 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 311 | * if a name is not a constant string or if two names are the same. 312 | * @throws ClassCastException if a value class is not compatible with the record component type 313 | * 314 | * @return a new record instance with the updated values 315 | */ 316 | default R with(R record, String name0, Object value0, String name1, Object value1) { 317 | return invoke(record, 2, name0, value0, name1, value1, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 318 | } 319 | 320 | /** 321 | * Returns a new record instance with the record component named {@code name0} being updated to the value 322 | * {@code value0}. 323 | * 324 | * @param record a record instance 325 | * @param name0 a name of a record component 326 | * @param value0 the value of the record component {@code name0} 327 | * 328 | * @throws NullPointerException if the record is null or the name is null 329 | * @throws IllegalArgumentException if the {@code name0} is not the name of one of the record component of the record 330 | * or if the name is not a constant string. 331 | * @throws ClassCastException if the value class is not compatible with the record component type 332 | * 333 | * @return a new record instance with the updated values 334 | */ 335 | default R with(R record, String name0, Object value0) { 336 | return invoke(record, 1, name0, value0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 337 | } 338 | 339 | /** 340 | * Create a {@code Wither} from a lookup and a record class. 341 | * 342 | * @param lookup a lookup that should see the record constructor and accessors 343 | * @param recordType the class of the record instances that will be updated 344 | * @param the type of the record 345 | * @return a new {@code Wither} 346 | * 347 | * @throws NullPointerException if {@code lookup} or {@code recordType} is null 348 | * @throws IllegalAccessError if the record constructor is not accessible from the {@code lookup} 349 | */ 350 | static Wither of(Lookup lookup, Class extends R> recordType) { 351 | requireNonNull(lookup); 352 | requireNonNull(recordType); 353 | var target = WitherImpl.createMH(lookup, recordType); 354 | return (record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8) -> { 355 | Object newRecord; 356 | try { 357 | newRecord = target.invokeExact((Object) record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 358 | } catch(RuntimeException | Error e) { 359 | throw e; 360 | } catch(Throwable t) { 361 | throw new AssertionError(t); 362 | } 363 | return recordType.cast(newRecord); 364 | }; 365 | } 366 | } 367 | --------------------------------------------------------------------------------
17 | * record Person(String name, int age) implements WithTrait<Person> {} 18 | * ... 19 | * Person bob = new Person("Bob", 42); 20 | * Person ana = bob.with("name", "Ana"); 21 | *
24 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 25 | * in a package in a module which does not open the package to the module 26 | * {@code com.github.forax.recordutil}. 27 | * By example, if the record is declared in a module mymodule in a package mypackage, 28 | * the module-info of this module should contains the following declaration 29 | *
30 | * module mymodule { 31 | * ... 32 | * open mypackage to com.github.forax.recordutil; 33 | * } 34 | *
37 | * This implementation is not very efficient, use {@link Wither} for a more cumbersome 38 | * but more performant implementation. 39 | * 40 | * @param type of the record 41 | * 42 | * @see Wither 43 | */ 44 | public interface WithTrait { 45 | private Object[] initArray(WithShape shape) { 46 | var array = new Object[shape.size()]; 47 | try { 48 | for (var i = 0; i < shape.size(); i++) { 49 | array[i] = shape.getValue(i).invokeExact((Object) this); 50 | } 51 | return array; 52 | } catch (RuntimeException | Error e) { 53 | throw e; 54 | } catch (Throwable throwable) { 55 | throw new UndeclaredThrowableException(throwable); 56 | } 57 | } 58 | 59 | private static Object invokeArray(WithShape shape, Object[] array) { 60 | try { 61 | return shape.constructor().invokeExact(array); 62 | } catch (RuntimeException | Error e) { 63 | throw e; 64 | } catch (Throwable throwable) { 65 | throw new UndeclaredThrowableException(throwable); 66 | } 67 | } 68 | 69 | private static int slot(WithShape shape, String name) { 70 | var slot = shape.getSlot(name); 71 | if (slot == -1) { 72 | throw new IllegalStateException("record component " + name + "not found"); 73 | } 74 | return slot; 75 | } 76 | 77 | /** 78 | * Returns a new record instance with the record component named {@code name} updated 79 | * to the value {@code value}. 80 | * 81 | * @param name a record component name 82 | * @param value the new value of the record component {@code name} 83 | * @return a new record instance with the record component value updated 84 | * 85 | * @throws NullPointerException if {@code name} is null 86 | * @throws ClassCastException if the value has not a class compatible with the record component type 87 | */ 88 | @SuppressWarnings("unchecked") 89 | default R with(String name, Object value) { 90 | requireNonNull(name, "name is null"); 91 | var shape = TraitImpl.withShape(getClass()); 92 | var array = initArray(shape); 93 | array[slot(shape, name)] = value; 94 | return (R) invokeArray(shape, array); 95 | } 96 | 97 | /** 98 | * Returns a new record instance with the record components named {@code name1} 99 | * and {@code name2} respectively updated to the value {@code value1} and {@code value2}. 100 | * 101 | * @param name1 a record component name 102 | * @param value1 the new value of the record component {@code name1} 103 | * @param name2 a record component name 104 | * @param value2 the new value of the record component {@code name2} 105 | * @return a new record instance with the record component values updated 106 | * 107 | * @throws NullPointerException if {@code name1} or {@code name2} is null 108 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type 109 | */ 110 | @SuppressWarnings("unchecked") 111 | default R with(String name1, Object value1, String name2, Object value2) { 112 | requireNonNull(name1, "name1 is null"); 113 | requireNonNull(name2, "name2 is null"); 114 | var shape = TraitImpl.withShape(getClass()); 115 | var array = initArray(shape); 116 | array[slot(shape, name1)] = value1; 117 | array[slot(shape, name2)] = value2; 118 | return (R) invokeArray(shape, array); 119 | } 120 | 121 | /** 122 | * Returns a new record instance with the record components named {@code name1} 123 | * {@code name2} and {@code name3} respectively updated to the value {@code value1}, {@code value2} 124 | * and {@code value3}. 125 | * 126 | * @param name1 a record component name 127 | * @param value1 the new value of the record component {@code name1} 128 | * @param name2 a record component name 129 | * @param value2 the new value of the record component {@code name2} 130 | * @param name3 a record component name 131 | * @param value3 the new value of the record component {@code name3} 132 | * @return a new record instance with the record component values updated 133 | * 134 | * @throws NullPointerException if {@code name1}, {@code name2} or {@code name3} is null 135 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type 136 | */ 137 | @SuppressWarnings("unchecked") 138 | default R with(String name1, Object value1, String name2, Object value2, String name3, Object value3) { 139 | requireNonNull(name1, "name1 is null"); 140 | requireNonNull(name2, "name2 is null"); 141 | requireNonNull(name3, "name3 is null"); 142 | var shape = TraitImpl.withShape(getClass()); 143 | var array = initArray(shape); 144 | array[slot(shape, name1)] = value1; 145 | array[slot(shape, name2)] = value2; 146 | array[slot(shape, name3)] = value3; 147 | return (R) invokeArray(shape, array); 148 | } 149 | 150 | /** 151 | * Returns a new record instance with the record components named {@code name1} 152 | * {@code name2}, {@code name3} and {@code name4} respectively updated to the value {@code value1}, 153 | * {@code value2}, {@code value3} and {@code value4}. 154 | * 155 | * @param name1 a record component name 156 | * @param value1 the new value of the record component {@code name1} 157 | * @param name2 a record component name 158 | * @param value2 the new value of the record component {@code name2} 159 | * @param name3 a record component name 160 | * @param value3 the new value of the record component {@code name3} 161 | * @param name4 a record component name 162 | * @param value4 the new value of the record component {@code name4} 163 | * @return a new record instance with the record component values updated 164 | * 165 | * @throws NullPointerException if {@code name1}, {@code name2}, {@code name3} or {@code name4} is null 166 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type. 167 | */ 168 | @SuppressWarnings("unchecked") 169 | default R with(String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) { 170 | requireNonNull(name1, "name1 is null"); 171 | requireNonNull(name2, "name2 is null"); 172 | requireNonNull(name3, "name3 is null"); 173 | requireNonNull(name4, "name4 is null"); 174 | var shape = TraitImpl.withShape(getClass()); 175 | var array = initArray(shape); 176 | array[slot(shape, name1)] = value1; 177 | array[slot(shape, name2)] = value2; 178 | array[slot(shape, name3)] = value3; 179 | array[slot(shape, name4)] = value4; 180 | return (R) invokeArray(shape, array); 181 | } 182 | 183 | /** 184 | * Returns a new record instance with the record components whose names are . 185 | * 186 | * @param pairs an array of pair of record name/new value 187 | * @return a new record instance with the record component values updated 188 | * 189 | * @throws NullPointerException if one of the name of the pairs is null 190 | * @throws IllegalArgumentException is the pairs array length is odd or one name of the pairs is not a String 191 | * @throws ClassCastException if one value has not a class compatible with its corresponding record component type. 192 | */ 193 | @SuppressWarnings("unchecked") 194 | default R with(Object... pairs) { 195 | if ((pairs.length & 1) != 0) { 196 | throw new IllegalArgumentException("invalid arguments, it should be pairs of name, value"); 197 | } 198 | var shape = TraitImpl.withShape(getClass()); 199 | var array = initArray(shape); 200 | for(var i = 0; i < pairs.length; i += 2) { 201 | var name = Objects.requireNonNull(pairs[i], "name " + i + " is null"); 202 | if (!(name instanceof String key)) { 203 | throw new IllegalArgumentException("name " + i + " is not a String: " + name); 204 | } 205 | var value = pairs[i + 1]; 206 | array[slot(shape, key)] = value; 207 | } 208 | return (R) invokeArray(shape, array); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/WitherImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodHandles.Lookup; 6 | import java.lang.invoke.MutableCallSite; 7 | import java.lang.reflect.RecordComponent; 8 | import java.util.Arrays; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.stream.Stream; 13 | 14 | import static java.lang.invoke.MethodHandles.dropArguments; 15 | import static java.lang.invoke.MethodHandles.filterArguments; 16 | import static java.lang.invoke.MethodHandles.guardWithTest; 17 | import static java.lang.invoke.MethodHandles.insertArguments; 18 | import static java.lang.invoke.MethodHandles.permuteArguments; 19 | import static java.lang.invoke.MethodType.methodType; 20 | import static java.util.stream.Collectors.toMap; 21 | import static java.util.stream.IntStream.range; 22 | 23 | class WitherImpl { 24 | public static MethodHandle createMH(Lookup lookup, Class> recordType) { 25 | var components = recordType.getRecordComponents(); 26 | if (components == null) { 27 | throw new LinkageError("the record class " + recordType.getName() + " is not a record "); 28 | } 29 | Map nameToIndexMap = range(0, components.length).boxed().collect(toMap(i -> components[i].getName(), i -> i)); 30 | 31 | MethodHandle constructor; 32 | try { 33 | constructor = lookup.findConstructor(recordType, methodType(void.class, Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new))); 34 | } catch (NoSuchMethodException e) { 35 | throw (NoSuchMethodError) new NoSuchMethodError().initCause(e); 36 | } catch (IllegalAccessException e) { 37 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 38 | } 39 | 40 | return new InliningCache(lookup, recordType, components, nameToIndexMap, constructor).dynamicInvoker().asType( 41 | methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 42 | } 43 | 44 | private static class InliningCache extends MutableCallSite { 45 | private static final MethodHandle NAME_CHECK, NAME_COUNT_CHECK, FALLBACK; 46 | static { 47 | var lookup = MethodHandles.lookup(); 48 | try { 49 | NAME_CHECK = lookup.findStatic(InliningCache.class, "nameCheck", 50 | methodType(boolean.class, String.class, String.class)); 51 | NAME_COUNT_CHECK = lookup.findStatic(InliningCache.class, "nameCountCheck", 52 | methodType(boolean.class, int.class, int.class)); 53 | FALLBACK = lookup.findVirtual(InliningCache.class, "fallback", 54 | methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 55 | } catch (NoSuchMethodException | IllegalAccessException e) { 56 | throw new AssertionError(e); 57 | } 58 | } 59 | 60 | private final Lookup lookup; 61 | private final Class> recordType; 62 | private final RecordComponent[] components; 63 | private final Map nameToIndexMap; 64 | private final MethodHandle constructor; 65 | 66 | private InliningCache(Lookup lookup, Class> recordType, RecordComponent[] components, Map nameToIndexMap, MethodHandle constructor) { 67 | super(methodType(Object.class, Object.class, int.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class, String.class, Object.class)); 68 | this.lookup = lookup; 69 | this.recordType = recordType; 70 | this.components = components; 71 | this.nameToIndexMap = nameToIndexMap; 72 | this.constructor = constructor; 73 | setTarget(FALLBACK.bindTo(this).asType(type())); 74 | } 75 | 76 | private static boolean nameCheck(String name, String expected) { 77 | //noinspection StringEquality 78 | return name == expected; 79 | } 80 | 81 | private static boolean nameCountCheck(int nameCount, int expected) { 82 | return nameCount == expected; 83 | } 84 | 85 | private Object fallback(Object record, int nameCount, 86 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 87 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 88 | String name6, Object value6, String name7, Object value7, String name8, Object value8) throws Throwable { 89 | 90 | Objects.requireNonNull(record, "record is null"); 91 | var names = gatherKeys(nameCount, name0, name1, name2, name3, name4, name5, name6, name7, name8); 92 | 93 | // check null names, non interned names or duplicate names 94 | var duplicates = new HashSet(); 95 | for(var i = 0; i < names.length; i++) { 96 | var name = names[i]; 97 | Objects.requireNonNull(name, "name " + i + " is null"); 98 | //noinspection StringEquality 99 | if (name != name.intern()) { 100 | throw new IllegalArgumentException("name " + name + " should be a constant name"); 101 | } 102 | if (!duplicates.add(name)) { 103 | throw new IllegalArgumentException("duplicate name " + i); 104 | } 105 | } 106 | 107 | int[] reorder = new int[components.length]; 108 | Class>[] newTypes = new Class>[1 + names.length]; 109 | newTypes[0] = recordType; 110 | for(var i = 0; i < names.length; i++) { 111 | var name = names[i]; 112 | var componentIndex = nameToIndexMap.get(name); 113 | if (componentIndex == null) { 114 | throw new IllegalArgumentException("unknown record component " + name + " for record " + recordType.getName()); 115 | } 116 | 117 | reorder[componentIndex] = i + 1; 118 | newTypes[i + 1] = components[componentIndex].getType(); 119 | } 120 | 121 | // use getters 122 | var filters = range(0, components.length) 123 | .mapToObj(i -> (reorder[i] != 0)? null: asGetter(lookup, components[i])) 124 | .toArray(MethodHandle[]::new); 125 | var mh = filterArguments(constructor, 0, filters); 126 | 127 | // re-organise, duplicate the record if there is a getter 128 | mh = permuteArguments(mh, methodType(recordType, newTypes), reorder); 129 | 130 | // drop the names 131 | for(var i = names.length; --i >= 0;) { 132 | mh = dropArguments(mh, 1 + i, String.class); 133 | } 134 | 135 | // drop null name/value pair up to 9 136 | if (names.length != 9) { 137 | mh = dropArguments(mh, mh.type().parameterCount(), range(names.length, 9).boxed().flatMap(__ -> Stream.of(String.class, Object.class)).toArray(Class[]::new)); 138 | } 139 | 140 | var result = mh.invoke(record, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 141 | 142 | // drop nameCount 143 | mh = dropArguments(mh, 1, int.class); 144 | 145 | // mask all values as Object 146 | mh = mh.asType(type()); 147 | 148 | // install constant name guards 149 | var newTypes2 = mh.type().parameterArray(); 150 | var other = new InliningCache(lookup, recordType, components, nameToIndexMap, constructor).dynamicInvoker(); 151 | for(var i = 0; i < names.length; i++) { 152 | var name = names[i]; 153 | var check = insertArguments(NAME_CHECK, 1, name); 154 | var test = dropArguments(check, 0, Arrays.stream(newTypes2, 0, 2 + 2 * i).toArray(Class[]::new)); 155 | mh = guardWithTest(test, mh, other); 156 | } 157 | 158 | // install nameCount guard 159 | var check = insertArguments(NAME_COUNT_CHECK, 1, nameCount); 160 | var test = dropArguments(check, 0, Object.class); 161 | mh = guardWithTest(test, mh, other); 162 | 163 | // install the target 164 | setTarget(mh); 165 | 166 | return result; 167 | } 168 | 169 | private static MethodHandle asGetter(Lookup lookup, RecordComponent component) { 170 | try { 171 | return lookup.unreflect(component.getAccessor()); 172 | } catch (IllegalAccessException e) { 173 | throw (LinkageError) new LinkageError().initCause(e); 174 | } 175 | } 176 | 177 | @SuppressWarnings({"fallthrough", "DefaultNotLastCaseInSwitch"}) 178 | private static String[] gatherKeys(int nameCount, String name0, String name1, String name2, String name3, String name4, String name5, String name6, String name7, String name8) { 179 | var names = new String[nameCount]; 180 | switch (nameCount) { 181 | default: 182 | throw new IllegalArgumentException("invalid nameCount " + nameCount); 183 | case 9: 184 | names[8] = name8; // fallthrough 185 | case 8: 186 | names[7] = name7; // fallthrough 187 | case 7: 188 | names[6] = name6; // fallthrough 189 | case 6: 190 | names[5] = name5; // fallthrough 191 | case 5: 192 | names[4] = name4; // fallthrough 193 | case 4: 194 | names[3] = name3; // fallthrough 195 | case 3: 196 | names[2] = name2; // fallthrough 197 | case 2: 198 | names[1] = name1; // fallthrough 199 | case 1: 200 | names[0] = name0; // fallthrough 201 | } 202 | return names; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/JSONTrait.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.io.IOException; 4 | import java.io.Reader; 5 | import java.lang.invoke.MethodHandle; 6 | import java.lang.reflect.UndeclaredThrowableException; 7 | import java.util.Collection; 8 | import java.util.stream.Stream; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | /** 13 | * An interface that provides a method {@link #toJSON()} for any {@link Record record} that 14 | * implements this interface. 15 | * 16 | * Adding this interface add two methods {@link #toJSON()} and {@link #toHumanReadableJSON(String, String)} 17 | * 18 | * record Person(String name, int age) implements JSONTrait { } 19 | * ... 20 | * var person = new Person("Bob", 42); 21 | * System.out.println(person.toJSON()); 22 | * 23 | * 24 | * Moreover, JSONTrait defines two methods to parse a JSON file, {@link #parse(Reader, Class)} to decode 25 | * a JSON Object to a record instance and {@link #stream(Reader, Class)} to decode a JSON Array 26 | * to a {@link Stream} of record instances. 27 | * Both methods are implemented using Jackson but in order to avoid an unnecessary dependency 28 | * if those methods are not used , the dependency to Jackson is declared as optional. 29 | * So to use these methods, you have to add a `requires com.fasterxml.jackson.core;` into your module-info 30 | * and also adds the dependency to `com.fasterxml.jackson.core:jackson-core` in your POM file 31 | * 32 | * 33 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 34 | * in a package in a module which does not open the package to the module 35 | * {@code com.github.forax.recordutil}. 36 | * By example, if the record is declared in a module mymodule in a package mypackage, 37 | * the module-info of this module should contains the following declaration 38 | * 39 | * module mymodule { 40 | * ... 41 | * open mypackage to com.github.forax.recordutil; 42 | * } 43 | * 44 | */ 45 | public interface JSONTrait { 46 | /** 47 | * Returns the current record instance formatted using the JSON format 48 | * @return the current record instance formatted using the JSON format 49 | */ 50 | default String toJSON() { 51 | var builder = new StringBuilder(); 52 | toJSONRecord(builder, this, "", "", ""); 53 | return builder.toString(); 54 | } 55 | 56 | /** 57 | * Returns a human readable text using the JSON format of the current record, 58 | * using \n as line separator and " " as line indent. 59 | * 60 | * This is semantically equivalent to call 61 | * {@code toHumanReadableJSON(" ", "\n")} 62 | * 63 | * @return a human readable text using the JSON format of the current record 64 | * @see #toHumanReadableJSON(String, String) 65 | */ 66 | default String toHumanReadableJSON() { 67 | return toHumanReadableJSON(" ", "\n"); 68 | } 69 | 70 | /** 71 | * Returns a human readable text using the JSON format of the current record 72 | * @param lineIndent number of spaces to increment when entering a JSON Object or a JSON Array 73 | * @param lineSeparator the line separator (e.g. "\n" or "\r\n") 74 | * @return a human readable text using the JSON format of the current record 75 | */ 76 | default String toHumanReadableJSON(String lineIndent, String lineSeparator) { 77 | var builder = new StringBuilder(); 78 | toJSONRecord(builder, this, "", lineIndent, lineSeparator); 79 | return builder.toString(); 80 | } 81 | 82 | private static Object invokeValue(Object object, MethodHandle getter) { 83 | try { 84 | return getter.invokeExact(object); 85 | } catch(RuntimeException | Error e) { 86 | throw e; 87 | } catch (Throwable t) { 88 | throw new UndeclaredThrowableException(t); 89 | } 90 | } 91 | 92 | private static void toJSON(StringBuilder builder, Object o, String linePrefix, String lineIndent, String lineSeparator) { 93 | if (o instanceof Record record) { 94 | toJSONRecord(builder, record, linePrefix, lineIndent, lineSeparator); 95 | return; 96 | } 97 | if (o instanceof Collection> collection) { 98 | toJSONArray(builder, collection, linePrefix, lineIndent, lineSeparator); 99 | return; 100 | } 101 | toJSONPrimitive(builder, o); 102 | } 103 | 104 | private static void toJSONRecord(StringBuilder builder, Object record, String linePrefix, String lineIndent, String lineSeparator) { 105 | var shape = TraitImpl.mapShape(record.getClass()); 106 | builder.append('{'); 107 | var separator = ""; 108 | var innerLinePrefix = linePrefix + lineIndent; 109 | for(var i = 0; i < shape.size(); i++) { 110 | var key = shape.getKey(i); 111 | var value = shape.getValue(i); 112 | builder.append(separator) 113 | .append(lineSeparator).append(innerLinePrefix) 114 | .append('"').append(key).append("\": "); 115 | toJSON(builder, invokeValue(record, value), innerLinePrefix, lineIndent, lineSeparator); 116 | separator = lineSeparator.isEmpty()? ", ": ","; 117 | } 118 | builder.append(lineSeparator).append(linePrefix).append('}'); 119 | } 120 | 121 | private static void toJSONArray(StringBuilder builder, Collection> collection, String linePrefix, String lineIndent, String lineSeparator) { 122 | builder.append('['); 123 | var separator = ""; 124 | var innerLinePrefix = linePrefix + lineIndent; 125 | for(var value: collection) { 126 | builder.append(separator).append(lineSeparator).append(innerLinePrefix); 127 | toJSON(builder, value, innerLinePrefix, lineIndent, lineSeparator); 128 | separator = lineSeparator.isEmpty()? ", ": ","; 129 | } 130 | builder.append(lineSeparator).append(linePrefix).append(']'); 131 | } 132 | 133 | private static void toJSONPrimitive(StringBuilder builder, Object o) { 134 | if (o == null || o instanceof Number || o instanceof Boolean) { 135 | builder.append(o); 136 | return; 137 | } 138 | builder.append('"') 139 | .append(o.toString().replace("\"", "\\\"")) 140 | .append('"'); 141 | } 142 | 143 | /** 144 | * User defined callback to convert a JSON value typed as a {@code String} to a Java value 145 | * of a specific {@code Class}. 146 | * 147 | * By example, to parse dates using {@link java.time.LocalDate}, one can write 148 | * 149 | * Converter converter = (valueAsString, type, downstreamConverter) -> { 150 | * if (type == LocalDate.class) { 151 | * return LocalDate.parse(valueAsString); 152 | * } 153 | * return downstreamConverter.convert(valueAsString, type); 154 | * }; 155 | * 156 | * 157 | * @see #parse(Reader, Class, Converter) 158 | * @see #stream(Reader, Class, Converter) 159 | */ 160 | @FunctionalInterface 161 | interface Converter { 162 | /** 163 | * Default implementation of a converter that knows how to convert Java primitive types. 164 | */ 165 | interface DownStream { 166 | /** 167 | * Convert a JSON value encoded as a String to an object of peculiar Java class. 168 | * 169 | * @param valueAsString JSON value 170 | * @param type a Java class 171 | * @return a value of class {@code Class} 172 | * @throws IOException if the conversion is not possible 173 | */ 174 | Object convert(String valueAsString, Class> type) throws IOException; 175 | } 176 | 177 | /** 178 | * Convert a JSON value encoded as a String to an object of peculiar Java class. 179 | * 180 | * @param valueAsString a JSON value 181 | * @param type a Java class 182 | * @param downstreamConverter an already defined converter that implement the default conversions, 183 | * for primitive values, String, BigInteger and BigDecimal. 184 | * @return a value of class {@code Class} 185 | * @throws IOException if the conversion is not possible 186 | * 187 | * @see Converter.DownStream 188 | */ 189 | Object convert(String valueAsString, Class> type, DownStream downstreamConverter) throws IOException; 190 | } 191 | 192 | private static Converter defaultConverter() { 193 | return (valueAsString, type, downstreamConverter) -> downstreamConverter.convert(valueAsString, type); 194 | } 195 | 196 | /** 197 | * Parse a JSON Object using a record class to guide the decoding. 198 | * 199 | * @param reader the reader containing the JSON 200 | * @param recordType the type of the record to decode 201 | * @param the type of the record 202 | * @return a newly allocated record 203 | * @throws IOException if either an i/o error or a parsing error occur 204 | * 205 | * @see #parse(Reader, Class, Converter) 206 | */ 207 | static R parse(Reader reader, Class extends R> recordType) throws IOException { 208 | return parse(reader, recordType, defaultConverter()); 209 | } 210 | 211 | /** 212 | * Parse a JSON Object using a record class to guide the decoding. 213 | * 214 | * Primitive types, String, BigInteger, BigDecimal, records, List and Set are handled automatically, 215 | * for other types, you have to provide a {@link Converter} that recognize those types. 216 | * 217 | * @param reader the reader containing the JSON 218 | * @param recordType the type of the record to decode 219 | * @param converter a user defined converter to handle user specific conversions 220 | * @param the type of the record 221 | * @return a newly allocated record 222 | * @throws IOException if either an i/o error or a parsing error occur 223 | * 224 | * @see #parse(Reader, Class) 225 | * @see Converter 226 | */ 227 | static R parse(Reader reader, Class extends R> recordType, Converter converter) throws IOException { 228 | requireNonNull(reader, "reader is null"); 229 | requireNonNull(recordType, "recordType is null"); 230 | requireNonNull(converter, "converter is null"); 231 | return recordType.cast(JSONParsing.parse(reader, recordType, converter)); 232 | } 233 | 234 | /** 235 | * Returns a Stream from a JSON Array of Objects using a record class to guide the decoding. 236 | * 237 | * @param reader the reader containing the JSON 238 | * @param recordType the type of the record to decode 239 | * @param the type of the record 240 | * @return a Stream of records 241 | * @throws java.io.UncheckedIOException if either an i/o error or a parsing error occur 242 | * 243 | * @see #stream(Reader, Class, Converter) 244 | */ 245 | static Stream stream(Reader reader, Class extends R> recordType) { 246 | return stream(reader, recordType, defaultConverter()); 247 | } 248 | 249 | /** 250 | * Returns a Stream from a JSON Array of Objects using a record class to guide the decoding. 251 | * 252 | * Primitive types, String, BigInteger, BigDecimal, records, List and Set are handled automatically, 253 | * for other types, you have to provide a {@link Converter} that recognize those types. 254 | * 255 | * @param reader the reader containing the JSON 256 | * @param recordType the type of the record to decode 257 | * @param converter a user defined converter to handle user specific conversions 258 | * @param the type of the record 259 | * @return a Stream of records 260 | * @throws java.io.UncheckedIOException if either an i/o error or a parsing error occur 261 | * 262 | * @see #stream(Reader, Class) 263 | * @see Converter 264 | */ 265 | static Stream stream(Reader reader, Class extends R> recordType, Converter converter) { 266 | requireNonNull(reader, "reader is null"); 267 | requireNonNull(recordType, "recordType is null"); 268 | requireNonNull(converter, "converter is null"); 269 | return JSONParsing.stream(reader, recordType, converter); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/TraitImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodHandles.Lookup; 6 | import java.lang.invoke.MethodType; 7 | import java.lang.reflect.RecordComponent; 8 | import java.util.Arrays; 9 | 10 | import static java.lang.invoke.MethodType.methodType; 11 | 12 | class TraitImpl { 13 | 14 | /** 15 | * Combine a hash table ({@code table}) that stores a pair String/MethodHandle with 16 | * a list {@code vec} that stores the same pair. 17 | * 18 | * 19 | * to insert a pair uses {@link #put(int, String, MethodHandle)} 20 | * to know the number of pairs uses {@link #size()} 21 | * to get the value from a key uses {@link #getValue(String)} 22 | * to get the key from an index uses {@link #getKey(int)} 23 | * to get the value from an index uses {@link #getValue(int)} 24 | * 25 | */ 26 | record MapShape(Object[] table, Object[] vec) { 27 | MapShape(int capacity) { 28 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new Object[capacity << 1]); 29 | } 30 | 31 | void put(int index, String key, MethodHandle getter) { 32 | var slot = -probe(key) - 1; 33 | table[slot] = key; 34 | table[slot + 1] = getter; 35 | vec[index << 1] = key; 36 | vec[(index << 1) + 1] = getter; 37 | } 38 | 39 | MethodHandle getValue(String key) { 40 | var slot = probe(key); 41 | if (slot < 0) { 42 | return null; 43 | } 44 | return (MethodHandle) table[slot + 1]; 45 | } 46 | 47 | boolean containsKey(String key) { 48 | return probe(key) >= 0; 49 | } 50 | 51 | int size() { 52 | return vec.length >> 1; 53 | } 54 | boolean isEmpty() { 55 | return vec.length == 0; 56 | } 57 | 58 | String getKey(int index) { 59 | return (String) vec[index << 1]; 60 | } 61 | MethodHandle getValue(int index) { 62 | return (MethodHandle) vec[(index << 1) + 1]; 63 | } 64 | 65 | private int probe(String key) { 66 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 67 | var k = table[slot]; 68 | if (k == null) { 69 | return -slot - 1; 70 | } 71 | if (key.equals(k)) { 72 | return slot; 73 | } 74 | return probe2(key, slot); 75 | } 76 | 77 | private int probe2(String key, int slot) { 78 | for(;;) { 79 | slot = (slot + 2) & (table.length - 1); 80 | var k = table[slot]; 81 | if (k == null) { 82 | return -slot - 1; 83 | } 84 | if (key.equals(k)) { 85 | return slot; 86 | } 87 | } 88 | } 89 | } 90 | 91 | private static final ClassValue SHAPE_MAP = new ClassValue<>() { 92 | @Override 93 | protected MapShape computeValue(Class> type) { 94 | var components = type.getRecordComponents(); 95 | if (components == null) { 96 | throw new IllegalStateException(type.getName() + " is not a record"); 97 | } 98 | var lookup = teleport(type, MethodHandles.lookup()); 99 | 100 | var shape = new MapShape(components.length); 101 | for(var i = 0; i < components.length; i++) { 102 | var component = components[i]; 103 | var getter = asMH(lookup, component).asType(methodType(Object.class, Object.class)); 104 | shape.put(i, component.getName(), getter); 105 | } 106 | return shape; 107 | } 108 | }; 109 | 110 | private static Lookup teleport(Class> type, Lookup localLookup) { 111 | // add read access to the type module 112 | localLookup.lookupClass().getModule().addReads(type.getModule()); 113 | 114 | // then teleport 115 | try { 116 | return MethodHandles.privateLookupIn(type, localLookup); 117 | } catch (IllegalAccessException e) { 118 | throw (IllegalAccessError) new IllegalAccessError(""" 119 | the module %s does not open the package %s to the module com.github.forax.recordutil 120 | you can add this incantation to the module-info 121 | module %s { 122 | ... 123 | opens %s to com.github.forax.recordutil; 124 | } 125 | """.formatted(type.getModule(), type.getPackageName(), type.getModule(), type.getPackageName()) 126 | ).initCause(e); 127 | } 128 | } 129 | 130 | private static MethodHandle asMH(Lookup lookup, RecordComponent component) { 131 | try { 132 | return lookup.unreflect(component.getAccessor()); 133 | } catch (IllegalAccessException e) { 134 | throw new AssertionError(e); 135 | } 136 | } 137 | 138 | /** 139 | * Returns a hash/list describing the associating between a record component 140 | * name and its corresponding getter as a method handle. 141 | * 142 | * @param type the class of the record 143 | * @return the hash/list describing the record 144 | */ 145 | static MapShape mapShape(Class> type) { 146 | return SHAPE_MAP.get(type); 147 | } 148 | 149 | 150 | /** 151 | * Combine a hash table ({@code table}) that stores an index ({@code slot}) for 152 | * a String and a list that stores at the index ({@code slot}) 153 | * the corresponding MethodHandle. 154 | * It also stores the constructor as a method handle. 155 | * 156 | * 157 | * to insert a pair uses {@link #put(int, String, MethodHandle)} 158 | * to know the number of key/value uses {@link #size()} 159 | * to get the slot (index) from a key uses {@link #getSlot(String)} 160 | * to get the value from a slot (index) uses {@link #getValue(int)} 161 | * 162 | */ 163 | record WithShape(Object[] table, MethodHandle[] vec, MethodHandle constructor) { 164 | WithShape(int capacity, MethodHandle constructor) { 165 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new MethodHandle[capacity], constructor); 166 | } 167 | 168 | void put(int index, String key, MethodHandle getter) { 169 | var slot = -probe(key) - 1; 170 | table[slot] = key; 171 | table[slot + 1] = index; 172 | vec[index] = getter; 173 | } 174 | 175 | int getSlot(String key) { 176 | var slot = probe(key); 177 | if (slot < 0) { 178 | return -1; 179 | } 180 | return (int) table[slot + 1]; 181 | } 182 | 183 | int size() { 184 | return vec.length; 185 | } 186 | 187 | MethodHandle getValue(int index) { 188 | return vec[index]; 189 | } 190 | 191 | private int probe(String key) { 192 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 193 | var k = table[slot]; 194 | if (k == null) { 195 | return -slot - 1; 196 | } 197 | if (key.equals(k)) { 198 | return slot; 199 | } 200 | return probe2(key, slot); 201 | } 202 | 203 | private int probe2(String key, int slot) { 204 | for(;;) { 205 | slot = (slot + 2) & (table.length - 1); 206 | var k = table[slot]; 207 | if (k == null) { 208 | return -slot - 1; 209 | } 210 | if (key.equals(k)) { 211 | return slot; 212 | } 213 | } 214 | } 215 | } 216 | 217 | private static final ClassValue WITH_SHAPE_MAP = new ClassValue<>() { 218 | @Override 219 | protected WithShape computeValue(Class> type) { 220 | var components = type.getRecordComponents(); 221 | if (components == null) { 222 | throw new IllegalStateException(type.getName() + " is not a record"); 223 | } 224 | var lookup = teleport(type, MethodHandles.lookup()); 225 | var constructor = asConstructor(lookup, type, components) 226 | .asType(MethodType.genericMethodType(components.length)) 227 | .asSpreader(Object[].class, components.length); 228 | 229 | var shape = new WithShape(components.length, constructor); 230 | for(var i = 0; i < components.length; i++) { 231 | var component = components[i]; 232 | var getter = asMH(lookup, component).asType(methodType(Object.class, Object.class)); 233 | shape.put(i, component.getName(), getter); 234 | } 235 | return shape; 236 | } 237 | }; 238 | 239 | private static MethodHandle asConstructor(Lookup lookup, Class> type, RecordComponent[] components) { 240 | try { 241 | return lookup.findConstructor(type, methodType(void.class, Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new))); 242 | } catch (IllegalAccessException | NoSuchMethodException e) { 243 | throw new AssertionError(e); 244 | } 245 | } 246 | 247 | /** 248 | * Returns a hash/list describing the associating between a record component 249 | * name and its corresponding getter as a method handle and 250 | * the constructor. 251 | * 252 | * @param type the class of the record 253 | * @return the hash/list describing the record 254 | */ 255 | static WithShape withShape(Class> type) { 256 | return WITH_SHAPE_MAP.get(type); 257 | } 258 | 259 | 260 | /** 261 | * Combine a hash table ({@code table}) that stores an index ({@code slot}) for 262 | * a String and a list that stores at the index ({@code slot}) 263 | * the corresponding type ({@code Class}). 264 | * It also stores the constructor as a method handle. 265 | * 266 | * 267 | * to insert a pair uses {@link #put(int, String, Class)} 268 | * to know the number of key/value uses {@link #size()} 269 | * to get the slot (index) from a key uses {@link #getSlot(String)} 270 | * to get the type from a slot (index) uses {@link #getType(int)} 271 | * 272 | */ 273 | record JSONShape(Object[] table, Class>[] vec, MethodHandle constructor) { 274 | JSONShape(int capacity, MethodHandle constructor) { 275 | this(new Object[capacity == 0? 2: Integer.highestOneBit(capacity) << 2], new Class>[capacity], constructor); 276 | } 277 | 278 | void put(int index, String key, Class> type) { 279 | var slot = -probe(key) - 1; 280 | table[slot] = key; 281 | table[slot + 1] = index; 282 | vec[index] = type; 283 | } 284 | 285 | int getSlot(String key) { 286 | var slot = probe(key); 287 | if (slot < 0) { 288 | return -1; 289 | } 290 | return (int) table[slot + 1]; 291 | } 292 | 293 | int size() { 294 | return vec.length; 295 | } 296 | 297 | Class> getType(int index) { 298 | return vec[index]; 299 | } 300 | 301 | private int probe(String key) { 302 | var slot = (key.hashCode() & ((table.length >> 1) - 1)) << 1; 303 | var k = table[slot]; 304 | if (k == null) { 305 | return -slot - 1; 306 | } 307 | if (key.equals(k)) { 308 | return slot; 309 | } 310 | return probe2(key, slot); 311 | } 312 | 313 | private int probe2(String key, int slot) { 314 | for(;;) { 315 | slot = (slot + 2) & (table.length - 1); 316 | var k = table[slot]; 317 | if (k == null) { 318 | return -slot - 1; 319 | } 320 | if (key.equals(k)) { 321 | return slot; 322 | } 323 | } 324 | } 325 | } 326 | 327 | private static final ClassValue JSON_SHAPE_MAP = new ClassValue<>() { 328 | @Override 329 | protected JSONShape computeValue(Class> type) { 330 | var components = type.getRecordComponents(); 331 | if (components == null) { 332 | throw new IllegalStateException(type.getName() + " is not a record"); 333 | } 334 | var lookup = teleport(type, MethodHandles.lookup()); 335 | var constructor = asConstructor(lookup, type, components) 336 | .asType(MethodType.genericMethodType(components.length)) 337 | .asSpreader(Object[].class, components.length); 338 | 339 | var shape = new JSONShape(components.length, constructor); 340 | for(var i = 0; i < components.length; i++) { 341 | var component = components[i]; 342 | shape.put(i, component.getName(), component.getType()); 343 | } 344 | return shape; 345 | } 346 | }; 347 | 348 | /** 349 | * Returns a hash/list describing the associating between a record component 350 | * name and its corresponding type (@code Class) and the constructor. 351 | * 352 | * @param type the class of the record 353 | * @return the hash/list describing the record 354 | */ 355 | static JSONShape jsonShape(Class> type) { 356 | return JSON_SHAPE_MAP.get(type); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/MapTrait.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.reflect.UndeclaredThrowableException; 5 | import java.util.AbstractList; 6 | import java.util.AbstractSet; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | import java.util.Set; 13 | import java.util.StringJoiner; 14 | import java.util.function.BiConsumer; 15 | import java.util.function.BiFunction; 16 | import java.util.function.Function; 17 | 18 | /** 19 | * An interface that provides an implementation for all methods of an unmodifiable {@link Map} 20 | * if the class that implements that interface is a {@link Record record}. 21 | * 22 | * Adding this interface transforms any records to a {@link Map}. 23 | * 24 | * record Person(String name, int age) implements MapTrait { } 25 | * ... 26 | * Map<String, Object> map = new Person("Bob", 42); 27 | * 28 | * 29 | * Sadly, an interface can not override the method {@code equals], {@code hashCode} 30 | * or {@code toString} using a default method so to get an implementation 31 | * of {@link Map} compatible with {@link java.util.AbstractMap}, those methods has 32 | * to be overridden by hand. 33 | * 34 | * record Person(String name, int age) implements MapTrait { 35 | * @Override 36 | * public boolean equals(Object o) { 37 | * return MapTrait.super.equalsOfMap(o); 38 | * } 39 | * 40 | * @Override 41 | * public int hashCode() { 42 | * return MapTrait.super.hashCodeOfMap(); 43 | * } 44 | * 45 | * @Override 46 | * public String toString() { 47 | * return MapTrait.super.toStringOfMap(); 48 | * } 49 | * } 50 | * 51 | * 52 | * 53 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 54 | * in a package in a module which does not open the package to the module 55 | * {@code com.github.forax.recordutil}. 56 | * By example, if the record is declared in a module mymodule in a package mypackage, 57 | * the module-info of this module should contains the following declaration 58 | * 59 | * module mymodule { 60 | * ... 61 | * open mypackage to com.github.forax.recordutil; 62 | * } 63 | * 64 | * 65 | * 66 | * This implementation guarantee that the methods {@link #entrySet()}, {@link #keySet()}, {@link #keys()} 67 | * and {@link #values()} provide the keys and values in the record components order. 68 | * 69 | * That's why the method {@link #keys()} and {@link #values()} returns a {@link List} instead of returning 70 | * a {@link java.util.Collection}. 71 | */ 72 | public interface MapTrait extends java.util.Map { 73 | @Override 74 | default int size() { 75 | return TraitImpl.mapShape(getClass()).size(); 76 | } 77 | @Override 78 | default boolean isEmpty() { 79 | return TraitImpl.mapShape(getClass()).isEmpty(); 80 | } 81 | 82 | @Override 83 | default Object get(Object key) { 84 | return getOrDefault(key, null); 85 | } 86 | @Override 87 | default Object getOrDefault(Object key, Object defaultValue) { 88 | if (!(key instanceof String s)) { 89 | return defaultValue; 90 | } 91 | var shape = TraitImpl.mapShape(getClass()); 92 | var getter = shape.getValue(s); 93 | if (getter == null) { 94 | return defaultValue; 95 | } 96 | return invokeValue(getter); 97 | } 98 | 99 | private Object invokeValue(MethodHandle getter) { 100 | try { 101 | return getter.invokeExact((Object) this); 102 | } catch(RuntimeException | Error e) { 103 | throw e; 104 | } catch (Throwable t) { 105 | throw new UndeclaredThrowableException(t); 106 | } 107 | } 108 | 109 | @Override 110 | default boolean containsKey(Object key) { 111 | if (!(key instanceof String s)) { 112 | return false; 113 | } 114 | var shape = TraitImpl.mapShape(getClass()); 115 | return shape.containsKey(s); 116 | } 117 | 118 | @Override 119 | default boolean containsValue(Object value) { 120 | var shape = TraitImpl.mapShape(getClass()); 121 | for(var i = 0; i < shape.size(); i++) { 122 | var getter = shape.getValue(i); 123 | if (Objects.equals(invokeValue(getter), value)) { 124 | return true; 125 | } 126 | } 127 | return false; 128 | } 129 | 130 | /** 131 | * The default implementation of this method comes from {@code java.lang.Record} 132 | * thus does not obey to the general contract of {@link Map#equals(Object)}. 133 | * 134 | * In your record, you can change the implementation by adding 135 | * 136 | * public boolean equals(Object o) { 137 | * return MapTrait.super.equalsOfMap(o); 138 | * } 139 | * 140 | * to get an implementation that behave like a {@link Map}. 141 | * 142 | * {@inheritDoc} 143 | * 144 | * @see #equalsOfMap(Object) 145 | */ 146 | @Override 147 | boolean equals(Object o); 148 | 149 | /** 150 | * The default implementation of this method comes from {@code java.lang.Record} 151 | * thus does not obey to the general contract of {@link Map#hashCode()}. 152 | * 153 | * In your record, you can change the implementation by adding 154 | * 155 | * public int hashCode() { 156 | * return MapTrait.super.hashCodeOfMap(); 157 | * } 158 | * 159 | * to get an implementation that behave like a {@link Map}. 160 | * 161 | * {@inheritDoc} 162 | * 163 | * @see #hashCodeOfMap() 164 | */ 165 | @Override 166 | int hashCode(); 167 | 168 | /** 169 | * The default implementation of this method comes from {@code java.lang.Record} 170 | * thus does not obey to the general contract of {@link java.util.AbstractMap#toString()}. 171 | * 172 | * In your record, you can change the implementation by adding 173 | * 174 | * public String toString() { 175 | * return MapTrait.super.toStringOfMap(); 176 | * } 177 | * 178 | * to get an implementation that behave like a {@link Map}. 179 | * 180 | * {@inheritDoc} 181 | * 182 | * @see #toStringOfMap() 183 | */ 184 | @Override 185 | String toString(); 186 | 187 | /** 188 | * Compares the specified object with this map for equality. Returns 189 | * {@code true} if the given object is also a map and the two maps 190 | * represent the same mappings. More formally, two maps {@code m1} and 191 | * {@code m2} represent the same mappings if 192 | * {@code m1.entrySet().equals(m2.entrySet())}. This ensures that the 193 | * {@code equals} method works properly across different implementations 194 | * of the {@code Map} interface. 195 | * 196 | * @param o object to be compared for equality with this map 197 | * @return {@code true} if the specified object is equal to this map 198 | * 199 | * @see #equals(Object) 200 | * @see Map#equals(Object) 201 | */ 202 | default boolean equalsOfMap(Object o) { 203 | return o instanceof Map,?> map && equalsOfMap(map); 204 | } 205 | 206 | private boolean equalsOfMap(Map,?> map) { 207 | var shape = TraitImpl.mapShape(getClass()); 208 | for(var i = 0; i < shape.size(); i++) { 209 | var value = map.get(shape.getKey(i)); 210 | if (!Objects.equals(invokeValue(shape.getValue(i)), value)) { 211 | return false; 212 | } 213 | } 214 | return true; 215 | } 216 | 217 | /** 218 | * Returns the hash code value for this map. The hash code of a map is 219 | * defined to be the sum of the hash codes of each entry in the map's 220 | * {@code entrySet()} view. This ensures that {@code m1.equals(m2)} 221 | * implies that {@code m1.hashCode()==m2.hashCode()} for any two maps 222 | * {@code m1} and {@code m2}, as required by the general contract of 223 | * {@link Object#hashCode}. 224 | * 225 | * @return the hash code value for this map 226 | * 227 | * @see #hashCode() 228 | * @see Map#hashCode() 229 | */ 230 | default int hashCodeOfMap() { 231 | var shape = TraitImpl.mapShape(getClass()); 232 | var h = 0; 233 | for (var i = 0; i < shape.size(); i++) { 234 | var value = invokeValue(shape.getValue(i)); 235 | h += shape.getKey(i).hashCode() ^ Objects.hashCode(value); 236 | } 237 | return h; 238 | } 239 | 240 | /** 241 | * Returns a string representation of this map. The string representation 242 | * consists of a list of key-value mappings in the order returned by the 243 | * map's {@code entrySet} view's iterator, enclosed in braces 244 | * ({@code "{}"}). Adjacent mappings are separated by the characters 245 | * {@code ", "} (comma and space). Each key-value mapping is rendered as 246 | * the key followed by an equals sign ({@code "="}) followed by the 247 | * associated value. Keys and values are converted to strings as by 248 | * {@link String#valueOf(Object)}. 249 | * 250 | * @return a string representation of this map 251 | * 252 | * @see #toString() 253 | * @see Map#toString() 254 | */ 255 | default String toStringOfMap() { 256 | var shape = TraitImpl.mapShape(getClass()); 257 | var joiner = new StringJoiner(", ", "{", "}"); 258 | for (var i = 0; i < shape.size(); i++) { 259 | joiner.add(shape.getKey(i) + "=" + invokeValue(shape.getValue(i))); 260 | } 261 | return joiner.toString(); 262 | } 263 | 264 | @Override 265 | default void forEach(BiConsumer super String, ? super Object> action) { 266 | var shape = TraitImpl.mapShape(getClass()); 267 | for (var i = 0; i < shape.size(); i++) { 268 | action.accept(shape.getKey(i), invokeValue(shape.getValue(i))); 269 | } 270 | } 271 | 272 | /** 273 | * Returns an unmodifiable {@link Set} view of the mappings contained in this map. 274 | * 275 | * @return a set view of the mappings contained in this map 276 | */ 277 | @Override 278 | default Set> entrySet() { 279 | var shape = TraitImpl.mapShape(getClass()); 280 | return new AbstractSet<>() { 281 | @Override 282 | public int size() { 283 | return shape.size(); 284 | } 285 | 286 | @Override 287 | public Iterator> iterator() { 288 | return new Iterator<>() { 289 | private int index; 290 | 291 | @Override 292 | public boolean hasNext() { 293 | return index < shape.size(); 294 | } 295 | 296 | @Override 297 | public Entry next() { 298 | if (!hasNext()) { 299 | throw new NoSuchElementException(); 300 | } 301 | return Map.entry(shape.getKey(index), invokeValue(shape.getValue(index++))); 302 | } 303 | }; 304 | } 305 | 306 | @Override 307 | public boolean contains(Object o) { 308 | if (!(o instanceof Map.Entry,?> e)) { 309 | return false; 310 | } 311 | return Objects.equals(get(e.getKey()), e.getValue()); 312 | } 313 | }; 314 | } 315 | 316 | /** 317 | * Returns an unmodifiable {@link Set} view of the keys contained in this map. 318 | * 319 | * @return a set view of the keys contained in this map 320 | */ 321 | @Override 322 | default Set keySet() { 323 | var shape = TraitImpl.mapShape(getClass()); 324 | return new AbstractSet<>() { 325 | @Override 326 | public int size() { 327 | return shape.size(); 328 | } 329 | 330 | @Override 331 | public Iterator iterator() { 332 | return new Iterator<>() { 333 | private int index; 334 | 335 | @Override 336 | public boolean hasNext() { 337 | return index < shape.size(); 338 | } 339 | 340 | @Override 341 | public String next() { 342 | if (!hasNext()) { 343 | throw new NoSuchElementException(); 344 | } 345 | return shape.getKey(index++); 346 | } 347 | }; 348 | } 349 | 350 | @Override 351 | public boolean contains(Object o) { 352 | return containsKey(o); 353 | } 354 | }; 355 | } 356 | 357 | /** 358 | * Returns an unmodifiable {@link List} view of the values contained in this map. 359 | * 360 | * @return an unmodifiable list of the values contained in this map 361 | */ 362 | @Override 363 | default List values() { 364 | var shape = TraitImpl.mapShape(getClass()); 365 | return new AbstractList<>() { 366 | @Override 367 | public int size() { 368 | return shape.size(); 369 | } 370 | 371 | @Override 372 | public Object get(int index) { 373 | Objects.checkIndex(index, shape.size()); 374 | return invokeValue(shape.getValue(index)); 375 | } 376 | }; 377 | } 378 | 379 | /** 380 | * Returns an unmodifiable {@link List} view of the keys contained in this map. 381 | * 382 | * @return an unmodifiable list of the keys contained in this map 383 | */ 384 | default List keys() { 385 | var shape = TraitImpl.mapShape(getClass()); 386 | return new AbstractList<>() { 387 | @Override 388 | public int size() { 389 | return shape.size(); 390 | } 391 | 392 | @Override 393 | public String get(int index) { 394 | Objects.checkIndex(index, shape.size()); 395 | return shape.getKey(index); 396 | } 397 | 398 | @Override 399 | public boolean contains(Object o) { 400 | return containsKey(o); 401 | } 402 | }; 403 | } 404 | 405 | @Override 406 | default void clear() { 407 | throw new UnsupportedOperationException(); 408 | } 409 | @Override 410 | default Object put(String key, Object value) { 411 | throw new UnsupportedOperationException(); 412 | } 413 | @Override 414 | default void putAll(Map extends String, ?> map) { 415 | throw new UnsupportedOperationException(); 416 | } 417 | 418 | @Override 419 | default Object remove(Object key) { 420 | throw new UnsupportedOperationException(); 421 | } 422 | @Override 423 | default boolean remove(Object key, Object value) { 424 | throw new UnsupportedOperationException(); 425 | } 426 | 427 | @Override 428 | default Object replace(String key, Object value) { 429 | throw new UnsupportedOperationException(); 430 | } 431 | @Override 432 | default boolean replace(String key, Object oldValue, Object newValue) { 433 | throw new UnsupportedOperationException(); 434 | } 435 | @Override 436 | default void replaceAll(BiFunction super String, ? super Object, ?> function) { 437 | throw new UnsupportedOperationException(); 438 | } 439 | 440 | @Override 441 | default Object compute(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 442 | throw new UnsupportedOperationException(); 443 | } 444 | @Override 445 | default Object computeIfAbsent(String key, Function super String, ?> mappingFunction) { 446 | throw new UnsupportedOperationException(); 447 | } 448 | @Override 449 | default Object computeIfPresent(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 450 | throw new UnsupportedOperationException(); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/Wither.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandles.Lookup; 4 | 5 | import static java.util.Objects.requireNonNull; 6 | 7 | /** 8 | * An efficient mechanism to create a record from an existing record object by updating several components. 9 | * 10 | * To be efficient, a {@code Wither} should be stored as a constant, by example as a static final field. 11 | * 12 | * record Person(String name, int age) {} 13 | * ... 14 | * private static final Wither<Person> wither = Wither.of(MethodHandles.lookup(), Person.class); 15 | * ... 16 | * var bob = new Person("Bob", 42); 17 | * var ana = wither.with(bob, "name", "Ana"); // create a Person with the name "Ana" and the same age as bob 18 | * 19 | * 20 | * 21 | * The implementation first extract the "shape" of the call to {@code with} (the number of components 22 | * and the name of each component to update) and generates a specialized code that calls all the getters 23 | * of the components that are not updated in the order of the record declaration then 24 | * re-create a new record by calling the constructor with all the values. 25 | * 26 | * Two calls with the same "shape" will reuse the same code, if there are different shapes, 27 | * multiple specialized codes are generated and the JIT will only keep the right specialized code for a call. 28 | * 29 | * Given the fact that the implementation keeps all specialized codes that have been seen at least once, 30 | * one {code Wither} may store a lot of metadata to the point the JIT will give up to try to optimize the code. 31 | * This should not be the code with handwritten code but can be problematic with generated Java codes. 32 | * 33 | * @param the type of the record to update. 34 | * 35 | * @see WithTrait 36 | */ 37 | @FunctionalInterface 38 | public interface Wither { 39 | /** 40 | * Returns a new record instance using the {@code nameCount} names and values to update the record instance 41 | * taken as first parameter. 42 | * This method should not be called directly, it's better to use one of the method {@code with} variants. 43 | * 44 | * @param record a record instance 45 | * @param nameCount the number of names that will used 46 | * @param name0 a name of a record component 47 | * @param value0 the value of the record component {@code name0} 48 | * @param name1 a name of a record component 49 | * @param value1 the value of the record component {@code name1} 50 | * @param name2 a name of a record component 51 | * @param value2 the value of the record component {@code name2} 52 | * @param name3 a name of a record component 53 | * @param value3 the value of the record component {@code name3} 54 | * @param name4 a name of a record component 55 | * @param value4 the value of the record component {@code name4} 56 | * @param name5 a name of a record component 57 | * @param value5 the value of the record component {@code name5} 58 | * @param name6 a name of a record component 59 | * @param value6 the value of the record component {@code name6} 60 | * @param name7 a name of a record component 61 | * @param value7 the value of the record component {@code name7} 62 | * @param name8 a name of a record component 63 | * @param value8 the value of the record component {@code name8} 64 | * 65 | * @throws NullPointerException if the record is null or one of the names is null 66 | * @throws IllegalArgumentException if {@code nameCount} is not between 1 and 9, if a name is not the name of one 67 | * of the record component of the record, if a name is not a constant string or if two names are the same. 68 | * @throws ClassCastException if a value class is not compatible with the record component type 69 | * 70 | * @return a new record instance with the updated values 71 | * 72 | * @see #with(Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object) 73 | */ 74 | R invoke(R record, int nameCount, 75 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 76 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 77 | String name6, Object value6, String name7, Object value7, String name8, Object value8); 78 | 79 | /** 80 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 81 | * {@code name3}, {@code name4}, {@code name5}, {@code name6}, {@code name7} and {@code name8} being updated to 82 | * the value {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, 83 | * {@code value6}, {@code value7} and {@code value8} respectively. 84 | * 85 | * @param record a record instance 86 | * @param name0 a name of a record component 87 | * @param value0 the value of the record component {@code name0} 88 | * @param name1 a name of a record component 89 | * @param value1 the value of the record component {@code name1} 90 | * @param name2 a name of a record component 91 | * @param value2 the value of the record component {@code name2} 92 | * @param name3 a name of a record component 93 | * @param value3 the value of the record component {@code name3} 94 | * @param name4 a name of a record component 95 | * @param value4 the value of the record component {@code name4} 96 | * @param name5 a name of a record component 97 | * @param value5 the value of the record component {@code name5} 98 | * @param name6 a name of a record component 99 | * @param value6 the value of the record component {@code name6} 100 | * @param name7 a name of a record component 101 | * @param value7 the value of the record component {@code name7} 102 | * @param name8 a name of a record component 103 | * @param value8 the value of the record component {@code name8} 104 | * 105 | * @throws NullPointerException if the record is null or one of the names is null 106 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 107 | * if a name is not a constant string or if two names are the same. 108 | * @throws ClassCastException if a value class is not compatible with the record component type 109 | * 110 | * @return a new record instance with the updated values 111 | */ 112 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 113 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 114 | String name6, Object value6, String name7, Object value7, String name8, Object value8) { 115 | return invoke(record, 9, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 116 | } 117 | 118 | /** 119 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 120 | * {@code name3}, {@code name4}, {@code name5}, {@code name6} and {@code name7} being updated to the value 121 | * {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, {@code value6} 122 | * and {@code value7} respectively. 123 | * 124 | * @param record a record instance 125 | * @param name0 a name of a record component 126 | * @param value0 the value of the record component {@code name0} 127 | * @param name1 a name of a record component 128 | * @param value1 the value of the record component {@code name1} 129 | * @param name2 a name of a record component 130 | * @param value2 the value of the record component {@code name2} 131 | * @param name3 a name of a record component 132 | * @param value3 the value of the record component {@code name3} 133 | * @param name4 a name of a record component 134 | * @param value4 the value of the record component {@code name4} 135 | * @param name5 a name of a record component 136 | * @param value5 the value of the record component {@code name5} 137 | * @param name6 a name of a record component 138 | * @param value6 the value of the record component {@code name6} 139 | * @param name7 a name of a record component 140 | * @param value7 the value of the record component {@code name7} 141 | * 142 | * @throws NullPointerException if the record is null or one of the names is null 143 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 144 | * if a name is not a constant string or if two names are the same. 145 | * @throws ClassCastException if a value class is not compatible with the record component type 146 | * 147 | * @return a new record instance with the updated values 148 | */ 149 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 150 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 151 | String name6, Object value6, String name7, Object value7) { 152 | return invoke(record, 8, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, null, null); 153 | } 154 | 155 | /** 156 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 157 | * {@code name3}, {@code name4}, {@code name5} and {@code name6} being updated to the value {@code value0}, 158 | * {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5} and {@code value6} respectively. 159 | * 160 | * @param record a record instance 161 | * @param name0 a name of a record component 162 | * @param value0 the value of the record component {@code name0} 163 | * @param name1 a name of a record component 164 | * @param value1 the value of the record component {@code name1} 165 | * @param name2 a name of a record component 166 | * @param value2 the value of the record component {@code name2} 167 | * @param name3 a name of a record component 168 | * @param value3 the value of the record component {@code name3} 169 | * @param name4 a name of a record component 170 | * @param value4 the value of the record component {@code name4} 171 | * @param name5 a name of a record component 172 | * @param value5 the value of the record component {@code name5} 173 | * @param name6 a name of a record component 174 | * @param value6 the value of the record component {@code name6} 175 | * 176 | * @throws NullPointerException if the record is null or one of the names is null 177 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 178 | * if a name is not a constant string or if two names are the same. 179 | * @throws ClassCastException if a value class is not compatible with the record component type 180 | * 181 | * @return a new record instance with the updated values 182 | */ 183 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 184 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 185 | String name6, Object value6) { 186 | return invoke(record, 7, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, null, null, null, null); 187 | } 188 | 189 | /** 190 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 191 | * {@code name3}, {@code name4} and {@code name5} being updated to the value {@code value0}, {@code value1}, 192 | * {@code value2}, {@code value3}, {@code value4} and {@code value5} respectively. 193 | * 194 | * @param record a record instance 195 | * @param name0 a name of a record component 196 | * @param value0 the value of the record component {@code name0} 197 | * @param name1 a name of a record component 198 | * @param value1 the value of the record component {@code name1} 199 | * @param name2 a name of a record component 200 | * @param value2 the value of the record component {@code name2} 201 | * @param name3 a name of a record component 202 | * @param value3 the value of the record component {@code name3} 203 | * @param name4 a name of a record component 204 | * @param value4 the value of the record component {@code name4} 205 | * @param name5 a name of a record component 206 | * @param value5 the value of the record component {@code name5} 207 | * 208 | * @throws NullPointerException if the record is null or one of the names is null 209 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 210 | * if a name is not a constant string or if two names are the same. 211 | * @throws ClassCastException if a value class is not compatible with the record component type 212 | * 213 | * @return a new record instance with the updated values 214 | */ 215 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 216 | String name3, Object value3, String name4, Object value4, String name5, Object value5) { 217 | return invoke(record, 6, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, null, null, null, null, null, null); 218 | } 219 | 220 | /** 221 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 222 | * {@code name3} and {@code name4} being updated to the value {@code value0}, {@code value1}, {@code value2}, 223 | * {@code value3} and {@code value4} respectively. 224 | * 225 | * @param record a record instance 226 | * @param name0 a name of a record component 227 | * @param value0 the value of the record component {@code name0} 228 | * @param name1 a name of a record component 229 | * @param value1 the value of the record component {@code name1} 230 | * @param name2 a name of a record component 231 | * @param value2 the value of the record component {@code name2} 232 | * @param name3 a name of a record component 233 | * @param value3 the value of the record component {@code name3} 234 | * @param name4 a name of a record component 235 | * @param value4 the value of the record component {@code name4} 236 | * 237 | * @throws NullPointerException if the record is null or one of the names is null 238 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 239 | * if a name is not a constant string or if two names are the same. 240 | * @throws ClassCastException if a value class is not compatible with the record component type 241 | * 242 | * @return a new record instance with the updated values 243 | */ 244 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 245 | String name3, Object value3, String name4, Object value4) { 246 | return invoke(record, 5, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, null, null, null, null, null, null, null, null); 247 | } 248 | 249 | /** 250 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2} and 251 | * {@code name3} being updated to the value {@code value0}, {@code value1}, {@code value2} and 252 | * {@code value3} respectively. 253 | * 254 | * @param record a record instance 255 | * @param name0 a name of a record component 256 | * @param value0 the value of the record component {@code name0} 257 | * @param name1 a name of a record component 258 | * @param value1 the value of the record component {@code name1} 259 | * @param name2 a name of a record component 260 | * @param value2 the value of the record component {@code name2} 261 | * @param name3 a name of a record component 262 | * @param value3 the value of the record component {@code name3} 263 | * 264 | * @throws NullPointerException if the record is null or one of the names is null 265 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 266 | * if a name is not a constant string or if two names are the same. 267 | * @throws ClassCastException if a value class is not compatible with the record component type 268 | * 269 | * @return a new record instance with the updated values 270 | */ 271 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 272 | String name3, Object value3) { 273 | return invoke(record, 4, name0, value0, name1, value1, name2, value2, name3, value3, null, null, null, null, null, null, null, null, null, null); 274 | } 275 | 276 | /** 277 | * Returns a new record instance with the record components named {@code name0}, {@code name1} and {@code name2} 278 | * being updated to the value {@code value0}, {@code value1} and {@code value2} respectively. 279 | * 280 | * @param record a record instance 281 | * @param name0 a name of a record component 282 | * @param value0 the value of the record component {@code name0} 283 | * @param name1 a name of a record component 284 | * @param value1 the value of the record component {@code name1} 285 | * @param name2 a name of a record component 286 | * @param value2 the value of the record component {@code name2} 287 | * 288 | * @throws NullPointerException if the record is null or one of the names is null 289 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 290 | * if a name is not a constant string or if two names are the same. 291 | * @throws ClassCastException if a value class is not compatible with the record component type 292 | * 293 | * @return a new record instance with the updated values 294 | */ 295 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2) { 296 | return invoke(record, 3, name0, value0, name1, value1, name2, value2, null, null, null, null, null, null, null, null, null, null, null, null); 297 | } 298 | 299 | /** 300 | * Returns a new record instance with the record components named {@code name0} and {@code name1} 301 | * being updated to the value {@code value0} and {@code value1} respectively. 302 | * 303 | * @param record a record instance 304 | * @param name0 a name of a record component 305 | * @param value0 the value of the record component {@code name0} 306 | * @param name1 a name of a record component 307 | * @param value1 the value of the record component {@code name1} 308 | * 309 | * @throws NullPointerException if the record is null or one of the names is null 310 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 311 | * if a name is not a constant string or if two names are the same. 312 | * @throws ClassCastException if a value class is not compatible with the record component type 313 | * 314 | * @return a new record instance with the updated values 315 | */ 316 | default R with(R record, String name0, Object value0, String name1, Object value1) { 317 | return invoke(record, 2, name0, value0, name1, value1, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 318 | } 319 | 320 | /** 321 | * Returns a new record instance with the record component named {@code name0} being updated to the value 322 | * {@code value0}. 323 | * 324 | * @param record a record instance 325 | * @param name0 a name of a record component 326 | * @param value0 the value of the record component {@code name0} 327 | * 328 | * @throws NullPointerException if the record is null or the name is null 329 | * @throws IllegalArgumentException if the {@code name0} is not the name of one of the record component of the record 330 | * or if the name is not a constant string. 331 | * @throws ClassCastException if the value class is not compatible with the record component type 332 | * 333 | * @return a new record instance with the updated values 334 | */ 335 | default R with(R record, String name0, Object value0) { 336 | return invoke(record, 1, name0, value0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 337 | } 338 | 339 | /** 340 | * Create a {@code Wither} from a lookup and a record class. 341 | * 342 | * @param lookup a lookup that should see the record constructor and accessors 343 | * @param recordType the class of the record instances that will be updated 344 | * @param the type of the record 345 | * @return a new {@code Wither} 346 | * 347 | * @throws NullPointerException if {@code lookup} or {@code recordType} is null 348 | * @throws IllegalAccessError if the record constructor is not accessible from the {@code lookup} 349 | */ 350 | static Wither of(Lookup lookup, Class extends R> recordType) { 351 | requireNonNull(lookup); 352 | requireNonNull(recordType); 353 | var target = WitherImpl.createMH(lookup, recordType); 354 | return (record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8) -> { 355 | Object newRecord; 356 | try { 357 | newRecord = target.invokeExact((Object) record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 358 | } catch(RuntimeException | Error e) { 359 | throw e; 360 | } catch(Throwable t) { 361 | throw new AssertionError(t); 362 | } 363 | return recordType.cast(newRecord); 364 | }; 365 | } 366 | } 367 | --------------------------------------------------------------------------------
18 | * record Person(String name, int age) implements JSONTrait { } 19 | * ... 20 | * var person = new Person("Bob", 42); 21 | * System.out.println(person.toJSON()); 22 | *
33 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 34 | * in a package in a module which does not open the package to the module 35 | * {@code com.github.forax.recordutil}. 36 | * By example, if the record is declared in a module mymodule in a package mypackage, 37 | * the module-info of this module should contains the following declaration 38 | *
39 | * module mymodule { 40 | * ... 41 | * open mypackage to com.github.forax.recordutil; 42 | * } 43 | *
149 | * Converter converter = (valueAsString, type, downstreamConverter) -> { 150 | * if (type == LocalDate.class) { 151 | * return LocalDate.parse(valueAsString); 152 | * } 153 | * return downstreamConverter.convert(valueAsString, type); 154 | * }; 155 | *
24 | * record Person(String name, int age) implements MapTrait { } 25 | * ... 26 | * Map<String, Object> map = new Person("Bob", 42); 27 | *
34 | * record Person(String name, int age) implements MapTrait { 35 | * @Override 36 | * public boolean equals(Object o) { 37 | * return MapTrait.super.equalsOfMap(o); 38 | * } 39 | * 40 | * @Override 41 | * public int hashCode() { 42 | * return MapTrait.super.hashCodeOfMap(); 43 | * } 44 | * 45 | * @Override 46 | * public String toString() { 47 | * return MapTrait.super.toStringOfMap(); 48 | * } 49 | * } 50 | *
53 | * All methods may throw an error {@link IllegalAccessError} if the record is declared 54 | * in a package in a module which does not open the package to the module 55 | * {@code com.github.forax.recordutil}. 56 | * By example, if the record is declared in a module mymodule in a package mypackage, 57 | * the module-info of this module should contains the following declaration 58 | *
59 | * module mymodule { 60 | * ... 61 | * open mypackage to com.github.forax.recordutil; 62 | * } 63 | *
66 | * This implementation guarantee that the methods {@link #entrySet()}, {@link #keySet()}, {@link #keys()} 67 | * and {@link #values()} provide the keys and values in the record components order. 68 | * 69 | * That's why the method {@link #keys()} and {@link #values()} returns a {@link List} instead of returning 70 | * a {@link java.util.Collection}. 71 | */ 72 | public interface MapTrait extends java.util.Map { 73 | @Override 74 | default int size() { 75 | return TraitImpl.mapShape(getClass()).size(); 76 | } 77 | @Override 78 | default boolean isEmpty() { 79 | return TraitImpl.mapShape(getClass()).isEmpty(); 80 | } 81 | 82 | @Override 83 | default Object get(Object key) { 84 | return getOrDefault(key, null); 85 | } 86 | @Override 87 | default Object getOrDefault(Object key, Object defaultValue) { 88 | if (!(key instanceof String s)) { 89 | return defaultValue; 90 | } 91 | var shape = TraitImpl.mapShape(getClass()); 92 | var getter = shape.getValue(s); 93 | if (getter == null) { 94 | return defaultValue; 95 | } 96 | return invokeValue(getter); 97 | } 98 | 99 | private Object invokeValue(MethodHandle getter) { 100 | try { 101 | return getter.invokeExact((Object) this); 102 | } catch(RuntimeException | Error e) { 103 | throw e; 104 | } catch (Throwable t) { 105 | throw new UndeclaredThrowableException(t); 106 | } 107 | } 108 | 109 | @Override 110 | default boolean containsKey(Object key) { 111 | if (!(key instanceof String s)) { 112 | return false; 113 | } 114 | var shape = TraitImpl.mapShape(getClass()); 115 | return shape.containsKey(s); 116 | } 117 | 118 | @Override 119 | default boolean containsValue(Object value) { 120 | var shape = TraitImpl.mapShape(getClass()); 121 | for(var i = 0; i < shape.size(); i++) { 122 | var getter = shape.getValue(i); 123 | if (Objects.equals(invokeValue(getter), value)) { 124 | return true; 125 | } 126 | } 127 | return false; 128 | } 129 | 130 | /** 131 | * The default implementation of this method comes from {@code java.lang.Record} 132 | * thus does not obey to the general contract of {@link Map#equals(Object)}. 133 | * 134 | * In your record, you can change the implementation by adding 135 | * 136 | * public boolean equals(Object o) { 137 | * return MapTrait.super.equalsOfMap(o); 138 | * } 139 | * 140 | * to get an implementation that behave like a {@link Map}. 141 | * 142 | * {@inheritDoc} 143 | * 144 | * @see #equalsOfMap(Object) 145 | */ 146 | @Override 147 | boolean equals(Object o); 148 | 149 | /** 150 | * The default implementation of this method comes from {@code java.lang.Record} 151 | * thus does not obey to the general contract of {@link Map#hashCode()}. 152 | * 153 | * In your record, you can change the implementation by adding 154 | * 155 | * public int hashCode() { 156 | * return MapTrait.super.hashCodeOfMap(); 157 | * } 158 | * 159 | * to get an implementation that behave like a {@link Map}. 160 | * 161 | * {@inheritDoc} 162 | * 163 | * @see #hashCodeOfMap() 164 | */ 165 | @Override 166 | int hashCode(); 167 | 168 | /** 169 | * The default implementation of this method comes from {@code java.lang.Record} 170 | * thus does not obey to the general contract of {@link java.util.AbstractMap#toString()}. 171 | * 172 | * In your record, you can change the implementation by adding 173 | * 174 | * public String toString() { 175 | * return MapTrait.super.toStringOfMap(); 176 | * } 177 | * 178 | * to get an implementation that behave like a {@link Map}. 179 | * 180 | * {@inheritDoc} 181 | * 182 | * @see #toStringOfMap() 183 | */ 184 | @Override 185 | String toString(); 186 | 187 | /** 188 | * Compares the specified object with this map for equality. Returns 189 | * {@code true} if the given object is also a map and the two maps 190 | * represent the same mappings. More formally, two maps {@code m1} and 191 | * {@code m2} represent the same mappings if 192 | * {@code m1.entrySet().equals(m2.entrySet())}. This ensures that the 193 | * {@code equals} method works properly across different implementations 194 | * of the {@code Map} interface. 195 | * 196 | * @param o object to be compared for equality with this map 197 | * @return {@code true} if the specified object is equal to this map 198 | * 199 | * @see #equals(Object) 200 | * @see Map#equals(Object) 201 | */ 202 | default boolean equalsOfMap(Object o) { 203 | return o instanceof Map,?> map && equalsOfMap(map); 204 | } 205 | 206 | private boolean equalsOfMap(Map,?> map) { 207 | var shape = TraitImpl.mapShape(getClass()); 208 | for(var i = 0; i < shape.size(); i++) { 209 | var value = map.get(shape.getKey(i)); 210 | if (!Objects.equals(invokeValue(shape.getValue(i)), value)) { 211 | return false; 212 | } 213 | } 214 | return true; 215 | } 216 | 217 | /** 218 | * Returns the hash code value for this map. The hash code of a map is 219 | * defined to be the sum of the hash codes of each entry in the map's 220 | * {@code entrySet()} view. This ensures that {@code m1.equals(m2)} 221 | * implies that {@code m1.hashCode()==m2.hashCode()} for any two maps 222 | * {@code m1} and {@code m2}, as required by the general contract of 223 | * {@link Object#hashCode}. 224 | * 225 | * @return the hash code value for this map 226 | * 227 | * @see #hashCode() 228 | * @see Map#hashCode() 229 | */ 230 | default int hashCodeOfMap() { 231 | var shape = TraitImpl.mapShape(getClass()); 232 | var h = 0; 233 | for (var i = 0; i < shape.size(); i++) { 234 | var value = invokeValue(shape.getValue(i)); 235 | h += shape.getKey(i).hashCode() ^ Objects.hashCode(value); 236 | } 237 | return h; 238 | } 239 | 240 | /** 241 | * Returns a string representation of this map. The string representation 242 | * consists of a list of key-value mappings in the order returned by the 243 | * map's {@code entrySet} view's iterator, enclosed in braces 244 | * ({@code "{}"}). Adjacent mappings are separated by the characters 245 | * {@code ", "} (comma and space). Each key-value mapping is rendered as 246 | * the key followed by an equals sign ({@code "="}) followed by the 247 | * associated value. Keys and values are converted to strings as by 248 | * {@link String#valueOf(Object)}. 249 | * 250 | * @return a string representation of this map 251 | * 252 | * @see #toString() 253 | * @see Map#toString() 254 | */ 255 | default String toStringOfMap() { 256 | var shape = TraitImpl.mapShape(getClass()); 257 | var joiner = new StringJoiner(", ", "{", "}"); 258 | for (var i = 0; i < shape.size(); i++) { 259 | joiner.add(shape.getKey(i) + "=" + invokeValue(shape.getValue(i))); 260 | } 261 | return joiner.toString(); 262 | } 263 | 264 | @Override 265 | default void forEach(BiConsumer super String, ? super Object> action) { 266 | var shape = TraitImpl.mapShape(getClass()); 267 | for (var i = 0; i < shape.size(); i++) { 268 | action.accept(shape.getKey(i), invokeValue(shape.getValue(i))); 269 | } 270 | } 271 | 272 | /** 273 | * Returns an unmodifiable {@link Set} view of the mappings contained in this map. 274 | * 275 | * @return a set view of the mappings contained in this map 276 | */ 277 | @Override 278 | default Set> entrySet() { 279 | var shape = TraitImpl.mapShape(getClass()); 280 | return new AbstractSet<>() { 281 | @Override 282 | public int size() { 283 | return shape.size(); 284 | } 285 | 286 | @Override 287 | public Iterator> iterator() { 288 | return new Iterator<>() { 289 | private int index; 290 | 291 | @Override 292 | public boolean hasNext() { 293 | return index < shape.size(); 294 | } 295 | 296 | @Override 297 | public Entry next() { 298 | if (!hasNext()) { 299 | throw new NoSuchElementException(); 300 | } 301 | return Map.entry(shape.getKey(index), invokeValue(shape.getValue(index++))); 302 | } 303 | }; 304 | } 305 | 306 | @Override 307 | public boolean contains(Object o) { 308 | if (!(o instanceof Map.Entry,?> e)) { 309 | return false; 310 | } 311 | return Objects.equals(get(e.getKey()), e.getValue()); 312 | } 313 | }; 314 | } 315 | 316 | /** 317 | * Returns an unmodifiable {@link Set} view of the keys contained in this map. 318 | * 319 | * @return a set view of the keys contained in this map 320 | */ 321 | @Override 322 | default Set keySet() { 323 | var shape = TraitImpl.mapShape(getClass()); 324 | return new AbstractSet<>() { 325 | @Override 326 | public int size() { 327 | return shape.size(); 328 | } 329 | 330 | @Override 331 | public Iterator iterator() { 332 | return new Iterator<>() { 333 | private int index; 334 | 335 | @Override 336 | public boolean hasNext() { 337 | return index < shape.size(); 338 | } 339 | 340 | @Override 341 | public String next() { 342 | if (!hasNext()) { 343 | throw new NoSuchElementException(); 344 | } 345 | return shape.getKey(index++); 346 | } 347 | }; 348 | } 349 | 350 | @Override 351 | public boolean contains(Object o) { 352 | return containsKey(o); 353 | } 354 | }; 355 | } 356 | 357 | /** 358 | * Returns an unmodifiable {@link List} view of the values contained in this map. 359 | * 360 | * @return an unmodifiable list of the values contained in this map 361 | */ 362 | @Override 363 | default List values() { 364 | var shape = TraitImpl.mapShape(getClass()); 365 | return new AbstractList<>() { 366 | @Override 367 | public int size() { 368 | return shape.size(); 369 | } 370 | 371 | @Override 372 | public Object get(int index) { 373 | Objects.checkIndex(index, shape.size()); 374 | return invokeValue(shape.getValue(index)); 375 | } 376 | }; 377 | } 378 | 379 | /** 380 | * Returns an unmodifiable {@link List} view of the keys contained in this map. 381 | * 382 | * @return an unmodifiable list of the keys contained in this map 383 | */ 384 | default List keys() { 385 | var shape = TraitImpl.mapShape(getClass()); 386 | return new AbstractList<>() { 387 | @Override 388 | public int size() { 389 | return shape.size(); 390 | } 391 | 392 | @Override 393 | public String get(int index) { 394 | Objects.checkIndex(index, shape.size()); 395 | return shape.getKey(index); 396 | } 397 | 398 | @Override 399 | public boolean contains(Object o) { 400 | return containsKey(o); 401 | } 402 | }; 403 | } 404 | 405 | @Override 406 | default void clear() { 407 | throw new UnsupportedOperationException(); 408 | } 409 | @Override 410 | default Object put(String key, Object value) { 411 | throw new UnsupportedOperationException(); 412 | } 413 | @Override 414 | default void putAll(Map extends String, ?> map) { 415 | throw new UnsupportedOperationException(); 416 | } 417 | 418 | @Override 419 | default Object remove(Object key) { 420 | throw new UnsupportedOperationException(); 421 | } 422 | @Override 423 | default boolean remove(Object key, Object value) { 424 | throw new UnsupportedOperationException(); 425 | } 426 | 427 | @Override 428 | default Object replace(String key, Object value) { 429 | throw new UnsupportedOperationException(); 430 | } 431 | @Override 432 | default boolean replace(String key, Object oldValue, Object newValue) { 433 | throw new UnsupportedOperationException(); 434 | } 435 | @Override 436 | default void replaceAll(BiFunction super String, ? super Object, ?> function) { 437 | throw new UnsupportedOperationException(); 438 | } 439 | 440 | @Override 441 | default Object compute(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 442 | throw new UnsupportedOperationException(); 443 | } 444 | @Override 445 | default Object computeIfAbsent(String key, Function super String, ?> mappingFunction) { 446 | throw new UnsupportedOperationException(); 447 | } 448 | @Override 449 | default Object computeIfPresent(String key, BiFunction super String, ? super Object, ?> remappingFunction) { 450 | throw new UnsupportedOperationException(); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/main/java/com/github/forax/recordutil/Wither.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.recordutil; 2 | 3 | import java.lang.invoke.MethodHandles.Lookup; 4 | 5 | import static java.util.Objects.requireNonNull; 6 | 7 | /** 8 | * An efficient mechanism to create a record from an existing record object by updating several components. 9 | * 10 | * To be efficient, a {@code Wither} should be stored as a constant, by example as a static final field. 11 | * 12 | * record Person(String name, int age) {} 13 | * ... 14 | * private static final Wither<Person> wither = Wither.of(MethodHandles.lookup(), Person.class); 15 | * ... 16 | * var bob = new Person("Bob", 42); 17 | * var ana = wither.with(bob, "name", "Ana"); // create a Person with the name "Ana" and the same age as bob 18 | * 19 | * 20 | * 21 | * The implementation first extract the "shape" of the call to {@code with} (the number of components 22 | * and the name of each component to update) and generates a specialized code that calls all the getters 23 | * of the components that are not updated in the order of the record declaration then 24 | * re-create a new record by calling the constructor with all the values. 25 | * 26 | * Two calls with the same "shape" will reuse the same code, if there are different shapes, 27 | * multiple specialized codes are generated and the JIT will only keep the right specialized code for a call. 28 | * 29 | * Given the fact that the implementation keeps all specialized codes that have been seen at least once, 30 | * one {code Wither} may store a lot of metadata to the point the JIT will give up to try to optimize the code. 31 | * This should not be the code with handwritten code but can be problematic with generated Java codes. 32 | * 33 | * @param the type of the record to update. 34 | * 35 | * @see WithTrait 36 | */ 37 | @FunctionalInterface 38 | public interface Wither { 39 | /** 40 | * Returns a new record instance using the {@code nameCount} names and values to update the record instance 41 | * taken as first parameter. 42 | * This method should not be called directly, it's better to use one of the method {@code with} variants. 43 | * 44 | * @param record a record instance 45 | * @param nameCount the number of names that will used 46 | * @param name0 a name of a record component 47 | * @param value0 the value of the record component {@code name0} 48 | * @param name1 a name of a record component 49 | * @param value1 the value of the record component {@code name1} 50 | * @param name2 a name of a record component 51 | * @param value2 the value of the record component {@code name2} 52 | * @param name3 a name of a record component 53 | * @param value3 the value of the record component {@code name3} 54 | * @param name4 a name of a record component 55 | * @param value4 the value of the record component {@code name4} 56 | * @param name5 a name of a record component 57 | * @param value5 the value of the record component {@code name5} 58 | * @param name6 a name of a record component 59 | * @param value6 the value of the record component {@code name6} 60 | * @param name7 a name of a record component 61 | * @param value7 the value of the record component {@code name7} 62 | * @param name8 a name of a record component 63 | * @param value8 the value of the record component {@code name8} 64 | * 65 | * @throws NullPointerException if the record is null or one of the names is null 66 | * @throws IllegalArgumentException if {@code nameCount} is not between 1 and 9, if a name is not the name of one 67 | * of the record component of the record, if a name is not a constant string or if two names are the same. 68 | * @throws ClassCastException if a value class is not compatible with the record component type 69 | * 70 | * @return a new record instance with the updated values 71 | * 72 | * @see #with(Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object) 73 | */ 74 | R invoke(R record, int nameCount, 75 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 76 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 77 | String name6, Object value6, String name7, Object value7, String name8, Object value8); 78 | 79 | /** 80 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 81 | * {@code name3}, {@code name4}, {@code name5}, {@code name6}, {@code name7} and {@code name8} being updated to 82 | * the value {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, 83 | * {@code value6}, {@code value7} and {@code value8} respectively. 84 | * 85 | * @param record a record instance 86 | * @param name0 a name of a record component 87 | * @param value0 the value of the record component {@code name0} 88 | * @param name1 a name of a record component 89 | * @param value1 the value of the record component {@code name1} 90 | * @param name2 a name of a record component 91 | * @param value2 the value of the record component {@code name2} 92 | * @param name3 a name of a record component 93 | * @param value3 the value of the record component {@code name3} 94 | * @param name4 a name of a record component 95 | * @param value4 the value of the record component {@code name4} 96 | * @param name5 a name of a record component 97 | * @param value5 the value of the record component {@code name5} 98 | * @param name6 a name of a record component 99 | * @param value6 the value of the record component {@code name6} 100 | * @param name7 a name of a record component 101 | * @param value7 the value of the record component {@code name7} 102 | * @param name8 a name of a record component 103 | * @param value8 the value of the record component {@code name8} 104 | * 105 | * @throws NullPointerException if the record is null or one of the names is null 106 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 107 | * if a name is not a constant string or if two names are the same. 108 | * @throws ClassCastException if a value class is not compatible with the record component type 109 | * 110 | * @return a new record instance with the updated values 111 | */ 112 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 113 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 114 | String name6, Object value6, String name7, Object value7, String name8, Object value8) { 115 | return invoke(record, 9, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 116 | } 117 | 118 | /** 119 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 120 | * {@code name3}, {@code name4}, {@code name5}, {@code name6} and {@code name7} being updated to the value 121 | * {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, {@code value6} 122 | * and {@code value7} respectively. 123 | * 124 | * @param record a record instance 125 | * @param name0 a name of a record component 126 | * @param value0 the value of the record component {@code name0} 127 | * @param name1 a name of a record component 128 | * @param value1 the value of the record component {@code name1} 129 | * @param name2 a name of a record component 130 | * @param value2 the value of the record component {@code name2} 131 | * @param name3 a name of a record component 132 | * @param value3 the value of the record component {@code name3} 133 | * @param name4 a name of a record component 134 | * @param value4 the value of the record component {@code name4} 135 | * @param name5 a name of a record component 136 | * @param value5 the value of the record component {@code name5} 137 | * @param name6 a name of a record component 138 | * @param value6 the value of the record component {@code name6} 139 | * @param name7 a name of a record component 140 | * @param value7 the value of the record component {@code name7} 141 | * 142 | * @throws NullPointerException if the record is null or one of the names is null 143 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 144 | * if a name is not a constant string or if two names are the same. 145 | * @throws ClassCastException if a value class is not compatible with the record component type 146 | * 147 | * @return a new record instance with the updated values 148 | */ 149 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 150 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 151 | String name6, Object value6, String name7, Object value7) { 152 | return invoke(record, 8, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, null, null); 153 | } 154 | 155 | /** 156 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 157 | * {@code name3}, {@code name4}, {@code name5} and {@code name6} being updated to the value {@code value0}, 158 | * {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5} and {@code value6} respectively. 159 | * 160 | * @param record a record instance 161 | * @param name0 a name of a record component 162 | * @param value0 the value of the record component {@code name0} 163 | * @param name1 a name of a record component 164 | * @param value1 the value of the record component {@code name1} 165 | * @param name2 a name of a record component 166 | * @param value2 the value of the record component {@code name2} 167 | * @param name3 a name of a record component 168 | * @param value3 the value of the record component {@code name3} 169 | * @param name4 a name of a record component 170 | * @param value4 the value of the record component {@code name4} 171 | * @param name5 a name of a record component 172 | * @param value5 the value of the record component {@code name5} 173 | * @param name6 a name of a record component 174 | * @param value6 the value of the record component {@code name6} 175 | * 176 | * @throws NullPointerException if the record is null or one of the names is null 177 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 178 | * if a name is not a constant string or if two names are the same. 179 | * @throws ClassCastException if a value class is not compatible with the record component type 180 | * 181 | * @return a new record instance with the updated values 182 | */ 183 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 184 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 185 | String name6, Object value6) { 186 | return invoke(record, 7, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, null, null, null, null); 187 | } 188 | 189 | /** 190 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 191 | * {@code name3}, {@code name4} and {@code name5} being updated to the value {@code value0}, {@code value1}, 192 | * {@code value2}, {@code value3}, {@code value4} and {@code value5} respectively. 193 | * 194 | * @param record a record instance 195 | * @param name0 a name of a record component 196 | * @param value0 the value of the record component {@code name0} 197 | * @param name1 a name of a record component 198 | * @param value1 the value of the record component {@code name1} 199 | * @param name2 a name of a record component 200 | * @param value2 the value of the record component {@code name2} 201 | * @param name3 a name of a record component 202 | * @param value3 the value of the record component {@code name3} 203 | * @param name4 a name of a record component 204 | * @param value4 the value of the record component {@code name4} 205 | * @param name5 a name of a record component 206 | * @param value5 the value of the record component {@code name5} 207 | * 208 | * @throws NullPointerException if the record is null or one of the names is null 209 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 210 | * if a name is not a constant string or if two names are the same. 211 | * @throws ClassCastException if a value class is not compatible with the record component type 212 | * 213 | * @return a new record instance with the updated values 214 | */ 215 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 216 | String name3, Object value3, String name4, Object value4, String name5, Object value5) { 217 | return invoke(record, 6, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, null, null, null, null, null, null); 218 | } 219 | 220 | /** 221 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 222 | * {@code name3} and {@code name4} being updated to the value {@code value0}, {@code value1}, {@code value2}, 223 | * {@code value3} and {@code value4} respectively. 224 | * 225 | * @param record a record instance 226 | * @param name0 a name of a record component 227 | * @param value0 the value of the record component {@code name0} 228 | * @param name1 a name of a record component 229 | * @param value1 the value of the record component {@code name1} 230 | * @param name2 a name of a record component 231 | * @param value2 the value of the record component {@code name2} 232 | * @param name3 a name of a record component 233 | * @param value3 the value of the record component {@code name3} 234 | * @param name4 a name of a record component 235 | * @param value4 the value of the record component {@code name4} 236 | * 237 | * @throws NullPointerException if the record is null or one of the names is null 238 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 239 | * if a name is not a constant string or if two names are the same. 240 | * @throws ClassCastException if a value class is not compatible with the record component type 241 | * 242 | * @return a new record instance with the updated values 243 | */ 244 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 245 | String name3, Object value3, String name4, Object value4) { 246 | return invoke(record, 5, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, null, null, null, null, null, null, null, null); 247 | } 248 | 249 | /** 250 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2} and 251 | * {@code name3} being updated to the value {@code value0}, {@code value1}, {@code value2} and 252 | * {@code value3} respectively. 253 | * 254 | * @param record a record instance 255 | * @param name0 a name of a record component 256 | * @param value0 the value of the record component {@code name0} 257 | * @param name1 a name of a record component 258 | * @param value1 the value of the record component {@code name1} 259 | * @param name2 a name of a record component 260 | * @param value2 the value of the record component {@code name2} 261 | * @param name3 a name of a record component 262 | * @param value3 the value of the record component {@code name3} 263 | * 264 | * @throws NullPointerException if the record is null or one of the names is null 265 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 266 | * if a name is not a constant string or if two names are the same. 267 | * @throws ClassCastException if a value class is not compatible with the record component type 268 | * 269 | * @return a new record instance with the updated values 270 | */ 271 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 272 | String name3, Object value3) { 273 | return invoke(record, 4, name0, value0, name1, value1, name2, value2, name3, value3, null, null, null, null, null, null, null, null, null, null); 274 | } 275 | 276 | /** 277 | * Returns a new record instance with the record components named {@code name0}, {@code name1} and {@code name2} 278 | * being updated to the value {@code value0}, {@code value1} and {@code value2} respectively. 279 | * 280 | * @param record a record instance 281 | * @param name0 a name of a record component 282 | * @param value0 the value of the record component {@code name0} 283 | * @param name1 a name of a record component 284 | * @param value1 the value of the record component {@code name1} 285 | * @param name2 a name of a record component 286 | * @param value2 the value of the record component {@code name2} 287 | * 288 | * @throws NullPointerException if the record is null or one of the names is null 289 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 290 | * if a name is not a constant string or if two names are the same. 291 | * @throws ClassCastException if a value class is not compatible with the record component type 292 | * 293 | * @return a new record instance with the updated values 294 | */ 295 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2) { 296 | return invoke(record, 3, name0, value0, name1, value1, name2, value2, null, null, null, null, null, null, null, null, null, null, null, null); 297 | } 298 | 299 | /** 300 | * Returns a new record instance with the record components named {@code name0} and {@code name1} 301 | * being updated to the value {@code value0} and {@code value1} respectively. 302 | * 303 | * @param record a record instance 304 | * @param name0 a name of a record component 305 | * @param value0 the value of the record component {@code name0} 306 | * @param name1 a name of a record component 307 | * @param value1 the value of the record component {@code name1} 308 | * 309 | * @throws NullPointerException if the record is null or one of the names is null 310 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 311 | * if a name is not a constant string or if two names are the same. 312 | * @throws ClassCastException if a value class is not compatible with the record component type 313 | * 314 | * @return a new record instance with the updated values 315 | */ 316 | default R with(R record, String name0, Object value0, String name1, Object value1) { 317 | return invoke(record, 2, name0, value0, name1, value1, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 318 | } 319 | 320 | /** 321 | * Returns a new record instance with the record component named {@code name0} being updated to the value 322 | * {@code value0}. 323 | * 324 | * @param record a record instance 325 | * @param name0 a name of a record component 326 | * @param value0 the value of the record component {@code name0} 327 | * 328 | * @throws NullPointerException if the record is null or the name is null 329 | * @throws IllegalArgumentException if the {@code name0} is not the name of one of the record component of the record 330 | * or if the name is not a constant string. 331 | * @throws ClassCastException if the value class is not compatible with the record component type 332 | * 333 | * @return a new record instance with the updated values 334 | */ 335 | default R with(R record, String name0, Object value0) { 336 | return invoke(record, 1, name0, value0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 337 | } 338 | 339 | /** 340 | * Create a {@code Wither} from a lookup and a record class. 341 | * 342 | * @param lookup a lookup that should see the record constructor and accessors 343 | * @param recordType the class of the record instances that will be updated 344 | * @param the type of the record 345 | * @return a new {@code Wither} 346 | * 347 | * @throws NullPointerException if {@code lookup} or {@code recordType} is null 348 | * @throws IllegalAccessError if the record constructor is not accessible from the {@code lookup} 349 | */ 350 | static Wither of(Lookup lookup, Class extends R> recordType) { 351 | requireNonNull(lookup); 352 | requireNonNull(recordType); 353 | var target = WitherImpl.createMH(lookup, recordType); 354 | return (record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8) -> { 355 | Object newRecord; 356 | try { 357 | newRecord = target.invokeExact((Object) record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 358 | } catch(RuntimeException | Error e) { 359 | throw e; 360 | } catch(Throwable t) { 361 | throw new AssertionError(t); 362 | } 363 | return recordType.cast(newRecord); 364 | }; 365 | } 366 | } 367 | --------------------------------------------------------------------------------
136 | * public boolean equals(Object o) { 137 | * return MapTrait.super.equalsOfMap(o); 138 | * } 139 | *
155 | * public int hashCode() { 156 | * return MapTrait.super.hashCodeOfMap(); 157 | * } 158 | *
174 | * public String toString() { 175 | * return MapTrait.super.toStringOfMap(); 176 | * } 177 | *
12 | * record Person(String name, int age) {} 13 | * ... 14 | * private static final Wither<Person> wither = Wither.of(MethodHandles.lookup(), Person.class); 15 | * ... 16 | * var bob = new Person("Bob", 42); 17 | * var ana = wither.with(bob, "name", "Ana"); // create a Person with the name "Ana" and the same age as bob 18 | *
21 | * The implementation first extract the "shape" of the call to {@code with} (the number of components 22 | * and the name of each component to update) and generates a specialized code that calls all the getters 23 | * of the components that are not updated in the order of the record declaration then 24 | * re-create a new record by calling the constructor with all the values. 25 | * 26 | * Two calls with the same "shape" will reuse the same code, if there are different shapes, 27 | * multiple specialized codes are generated and the JIT will only keep the right specialized code for a call. 28 | * 29 | * Given the fact that the implementation keeps all specialized codes that have been seen at least once, 30 | * one {code Wither} may store a lot of metadata to the point the JIT will give up to try to optimize the code. 31 | * This should not be the code with handwritten code but can be problematic with generated Java codes. 32 | * 33 | * @param the type of the record to update. 34 | * 35 | * @see WithTrait 36 | */ 37 | @FunctionalInterface 38 | public interface Wither { 39 | /** 40 | * Returns a new record instance using the {@code nameCount} names and values to update the record instance 41 | * taken as first parameter. 42 | * This method should not be called directly, it's better to use one of the method {@code with} variants. 43 | * 44 | * @param record a record instance 45 | * @param nameCount the number of names that will used 46 | * @param name0 a name of a record component 47 | * @param value0 the value of the record component {@code name0} 48 | * @param name1 a name of a record component 49 | * @param value1 the value of the record component {@code name1} 50 | * @param name2 a name of a record component 51 | * @param value2 the value of the record component {@code name2} 52 | * @param name3 a name of a record component 53 | * @param value3 the value of the record component {@code name3} 54 | * @param name4 a name of a record component 55 | * @param value4 the value of the record component {@code name4} 56 | * @param name5 a name of a record component 57 | * @param value5 the value of the record component {@code name5} 58 | * @param name6 a name of a record component 59 | * @param value6 the value of the record component {@code name6} 60 | * @param name7 a name of a record component 61 | * @param value7 the value of the record component {@code name7} 62 | * @param name8 a name of a record component 63 | * @param value8 the value of the record component {@code name8} 64 | * 65 | * @throws NullPointerException if the record is null or one of the names is null 66 | * @throws IllegalArgumentException if {@code nameCount} is not between 1 and 9, if a name is not the name of one 67 | * of the record component of the record, if a name is not a constant string or if two names are the same. 68 | * @throws ClassCastException if a value class is not compatible with the record component type 69 | * 70 | * @return a new record instance with the updated values 71 | * 72 | * @see #with(Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object, String, Object) 73 | */ 74 | R invoke(R record, int nameCount, 75 | String name0, Object value0, String name1, Object value1, String name2, Object value2, 76 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 77 | String name6, Object value6, String name7, Object value7, String name8, Object value8); 78 | 79 | /** 80 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 81 | * {@code name3}, {@code name4}, {@code name5}, {@code name6}, {@code name7} and {@code name8} being updated to 82 | * the value {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, 83 | * {@code value6}, {@code value7} and {@code value8} respectively. 84 | * 85 | * @param record a record instance 86 | * @param name0 a name of a record component 87 | * @param value0 the value of the record component {@code name0} 88 | * @param name1 a name of a record component 89 | * @param value1 the value of the record component {@code name1} 90 | * @param name2 a name of a record component 91 | * @param value2 the value of the record component {@code name2} 92 | * @param name3 a name of a record component 93 | * @param value3 the value of the record component {@code name3} 94 | * @param name4 a name of a record component 95 | * @param value4 the value of the record component {@code name4} 96 | * @param name5 a name of a record component 97 | * @param value5 the value of the record component {@code name5} 98 | * @param name6 a name of a record component 99 | * @param value6 the value of the record component {@code name6} 100 | * @param name7 a name of a record component 101 | * @param value7 the value of the record component {@code name7} 102 | * @param name8 a name of a record component 103 | * @param value8 the value of the record component {@code name8} 104 | * 105 | * @throws NullPointerException if the record is null or one of the names is null 106 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 107 | * if a name is not a constant string or if two names are the same. 108 | * @throws ClassCastException if a value class is not compatible with the record component type 109 | * 110 | * @return a new record instance with the updated values 111 | */ 112 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 113 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 114 | String name6, Object value6, String name7, Object value7, String name8, Object value8) { 115 | return invoke(record, 9, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 116 | } 117 | 118 | /** 119 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 120 | * {@code name3}, {@code name4}, {@code name5}, {@code name6} and {@code name7} being updated to the value 121 | * {@code value0}, {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5}, {@code value6} 122 | * and {@code value7} respectively. 123 | * 124 | * @param record a record instance 125 | * @param name0 a name of a record component 126 | * @param value0 the value of the record component {@code name0} 127 | * @param name1 a name of a record component 128 | * @param value1 the value of the record component {@code name1} 129 | * @param name2 a name of a record component 130 | * @param value2 the value of the record component {@code name2} 131 | * @param name3 a name of a record component 132 | * @param value3 the value of the record component {@code name3} 133 | * @param name4 a name of a record component 134 | * @param value4 the value of the record component {@code name4} 135 | * @param name5 a name of a record component 136 | * @param value5 the value of the record component {@code name5} 137 | * @param name6 a name of a record component 138 | * @param value6 the value of the record component {@code name6} 139 | * @param name7 a name of a record component 140 | * @param value7 the value of the record component {@code name7} 141 | * 142 | * @throws NullPointerException if the record is null or one of the names is null 143 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 144 | * if a name is not a constant string or if two names are the same. 145 | * @throws ClassCastException if a value class is not compatible with the record component type 146 | * 147 | * @return a new record instance with the updated values 148 | */ 149 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 150 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 151 | String name6, Object value6, String name7, Object value7) { 152 | return invoke(record, 8, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, null, null); 153 | } 154 | 155 | /** 156 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 157 | * {@code name3}, {@code name4}, {@code name5} and {@code name6} being updated to the value {@code value0}, 158 | * {@code value1}, {@code value2}, {@code value3}, {@code value4}, {@code value5} and {@code value6} respectively. 159 | * 160 | * @param record a record instance 161 | * @param name0 a name of a record component 162 | * @param value0 the value of the record component {@code name0} 163 | * @param name1 a name of a record component 164 | * @param value1 the value of the record component {@code name1} 165 | * @param name2 a name of a record component 166 | * @param value2 the value of the record component {@code name2} 167 | * @param name3 a name of a record component 168 | * @param value3 the value of the record component {@code name3} 169 | * @param name4 a name of a record component 170 | * @param value4 the value of the record component {@code name4} 171 | * @param name5 a name of a record component 172 | * @param value5 the value of the record component {@code name5} 173 | * @param name6 a name of a record component 174 | * @param value6 the value of the record component {@code name6} 175 | * 176 | * @throws NullPointerException if the record is null or one of the names is null 177 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 178 | * if a name is not a constant string or if two names are the same. 179 | * @throws ClassCastException if a value class is not compatible with the record component type 180 | * 181 | * @return a new record instance with the updated values 182 | */ 183 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 184 | String name3, Object value3, String name4, Object value4, String name5, Object value5, 185 | String name6, Object value6) { 186 | return invoke(record, 7, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, null, null, null, null); 187 | } 188 | 189 | /** 190 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 191 | * {@code name3}, {@code name4} and {@code name5} being updated to the value {@code value0}, {@code value1}, 192 | * {@code value2}, {@code value3}, {@code value4} and {@code value5} respectively. 193 | * 194 | * @param record a record instance 195 | * @param name0 a name of a record component 196 | * @param value0 the value of the record component {@code name0} 197 | * @param name1 a name of a record component 198 | * @param value1 the value of the record component {@code name1} 199 | * @param name2 a name of a record component 200 | * @param value2 the value of the record component {@code name2} 201 | * @param name3 a name of a record component 202 | * @param value3 the value of the record component {@code name3} 203 | * @param name4 a name of a record component 204 | * @param value4 the value of the record component {@code name4} 205 | * @param name5 a name of a record component 206 | * @param value5 the value of the record component {@code name5} 207 | * 208 | * @throws NullPointerException if the record is null or one of the names is null 209 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 210 | * if a name is not a constant string or if two names are the same. 211 | * @throws ClassCastException if a value class is not compatible with the record component type 212 | * 213 | * @return a new record instance with the updated values 214 | */ 215 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 216 | String name3, Object value3, String name4, Object value4, String name5, Object value5) { 217 | return invoke(record, 6, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, null, null, null, null, null, null); 218 | } 219 | 220 | /** 221 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2}, 222 | * {@code name3} and {@code name4} being updated to the value {@code value0}, {@code value1}, {@code value2}, 223 | * {@code value3} and {@code value4} respectively. 224 | * 225 | * @param record a record instance 226 | * @param name0 a name of a record component 227 | * @param value0 the value of the record component {@code name0} 228 | * @param name1 a name of a record component 229 | * @param value1 the value of the record component {@code name1} 230 | * @param name2 a name of a record component 231 | * @param value2 the value of the record component {@code name2} 232 | * @param name3 a name of a record component 233 | * @param value3 the value of the record component {@code name3} 234 | * @param name4 a name of a record component 235 | * @param value4 the value of the record component {@code name4} 236 | * 237 | * @throws NullPointerException if the record is null or one of the names is null 238 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 239 | * if a name is not a constant string or if two names are the same. 240 | * @throws ClassCastException if a value class is not compatible with the record component type 241 | * 242 | * @return a new record instance with the updated values 243 | */ 244 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 245 | String name3, Object value3, String name4, Object value4) { 246 | return invoke(record, 5, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, null, null, null, null, null, null, null, null); 247 | } 248 | 249 | /** 250 | * Returns a new record instance with the record components named {@code name0}, {@code name1}, {@code name2} and 251 | * {@code name3} being updated to the value {@code value0}, {@code value1}, {@code value2} and 252 | * {@code value3} respectively. 253 | * 254 | * @param record a record instance 255 | * @param name0 a name of a record component 256 | * @param value0 the value of the record component {@code name0} 257 | * @param name1 a name of a record component 258 | * @param value1 the value of the record component {@code name1} 259 | * @param name2 a name of a record component 260 | * @param value2 the value of the record component {@code name2} 261 | * @param name3 a name of a record component 262 | * @param value3 the value of the record component {@code name3} 263 | * 264 | * @throws NullPointerException if the record is null or one of the names is null 265 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 266 | * if a name is not a constant string or if two names are the same. 267 | * @throws ClassCastException if a value class is not compatible with the record component type 268 | * 269 | * @return a new record instance with the updated values 270 | */ 271 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2, 272 | String name3, Object value3) { 273 | return invoke(record, 4, name0, value0, name1, value1, name2, value2, name3, value3, null, null, null, null, null, null, null, null, null, null); 274 | } 275 | 276 | /** 277 | * Returns a new record instance with the record components named {@code name0}, {@code name1} and {@code name2} 278 | * being updated to the value {@code value0}, {@code value1} and {@code value2} respectively. 279 | * 280 | * @param record a record instance 281 | * @param name0 a name of a record component 282 | * @param value0 the value of the record component {@code name0} 283 | * @param name1 a name of a record component 284 | * @param value1 the value of the record component {@code name1} 285 | * @param name2 a name of a record component 286 | * @param value2 the value of the record component {@code name2} 287 | * 288 | * @throws NullPointerException if the record is null or one of the names is null 289 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 290 | * if a name is not a constant string or if two names are the same. 291 | * @throws ClassCastException if a value class is not compatible with the record component type 292 | * 293 | * @return a new record instance with the updated values 294 | */ 295 | default R with(R record, String name0, Object value0, String name1, Object value1, String name2, Object value2) { 296 | return invoke(record, 3, name0, value0, name1, value1, name2, value2, null, null, null, null, null, null, null, null, null, null, null, null); 297 | } 298 | 299 | /** 300 | * Returns a new record instance with the record components named {@code name0} and {@code name1} 301 | * being updated to the value {@code value0} and {@code value1} respectively. 302 | * 303 | * @param record a record instance 304 | * @param name0 a name of a record component 305 | * @param value0 the value of the record component {@code name0} 306 | * @param name1 a name of a record component 307 | * @param value1 the value of the record component {@code name1} 308 | * 309 | * @throws NullPointerException if the record is null or one of the names is null 310 | * @throws IllegalArgumentException if a name is not the name of one of the record component of the record, 311 | * if a name is not a constant string or if two names are the same. 312 | * @throws ClassCastException if a value class is not compatible with the record component type 313 | * 314 | * @return a new record instance with the updated values 315 | */ 316 | default R with(R record, String name0, Object value0, String name1, Object value1) { 317 | return invoke(record, 2, name0, value0, name1, value1, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 318 | } 319 | 320 | /** 321 | * Returns a new record instance with the record component named {@code name0} being updated to the value 322 | * {@code value0}. 323 | * 324 | * @param record a record instance 325 | * @param name0 a name of a record component 326 | * @param value0 the value of the record component {@code name0} 327 | * 328 | * @throws NullPointerException if the record is null or the name is null 329 | * @throws IllegalArgumentException if the {@code name0} is not the name of one of the record component of the record 330 | * or if the name is not a constant string. 331 | * @throws ClassCastException if the value class is not compatible with the record component type 332 | * 333 | * @return a new record instance with the updated values 334 | */ 335 | default R with(R record, String name0, Object value0) { 336 | return invoke(record, 1, name0, value0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); 337 | } 338 | 339 | /** 340 | * Create a {@code Wither} from a lookup and a record class. 341 | * 342 | * @param lookup a lookup that should see the record constructor and accessors 343 | * @param recordType the class of the record instances that will be updated 344 | * @param the type of the record 345 | * @return a new {@code Wither} 346 | * 347 | * @throws NullPointerException if {@code lookup} or {@code recordType} is null 348 | * @throws IllegalAccessError if the record constructor is not accessible from the {@code lookup} 349 | */ 350 | static Wither of(Lookup lookup, Class extends R> recordType) { 351 | requireNonNull(lookup); 352 | requireNonNull(recordType); 353 | var target = WitherImpl.createMH(lookup, recordType); 354 | return (record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8) -> { 355 | Object newRecord; 356 | try { 357 | newRecord = target.invokeExact((Object) record, nameCount, name0, value0, name1, value1, name2, value2, name3, value3, name4, value4, name5, value5, name6, value6, name7, value7, name8, value8); 358 | } catch(RuntimeException | Error e) { 359 | throw e; 360 | } catch(Throwable t) { 361 | throw new AssertionError(t); 362 | } 363 | return recordType.cast(newRecord); 364 | }; 365 | } 366 | } 367 | --------------------------------------------------------------------------------