diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7cfbbc30..e6ac5db0 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -14,6 +14,10 @@ Mostly compatible with v2.x. Deprecated methods have been removed. + + Allow Java records to become beans. + Add `RecordBean` interface that can be implemented by records. + Potentially incompatible change: Manual equals, hashCode and toString methods must now be located *before* the autogenerated block. diff --git a/src/main/java/org/joda/beans/JodaBeanUtils.java b/src/main/java/org/joda/beans/JodaBeanUtils.java index d17cf6ba..6cefada2 100644 --- a/src/main/java/org/joda/beans/JodaBeanUtils.java +++ b/src/main/java/org/joda/beans/JodaBeanUtils.java @@ -405,7 +405,7 @@ public static Map flatten(Bean bean) { for (var entry : propertyMap.entrySet()) { map.put(entry.getKey(), entry.getValue().get(bean)); } - return Map.copyOf(map); + return Collections.unmodifiableMap(map); } //----------------------------------------------------------------------- @@ -773,7 +773,7 @@ protected Map computeValue(Class contextClass) { entry.setValue(value); } } - return Map.copyOf(resolved); + return Collections.unmodifiableMap(resolved); } private void findTypeVars(Type type, HashMap resolved) { diff --git a/src/main/java/org/joda/beans/MetaBeans.java b/src/main/java/org/joda/beans/MetaBeans.java index c25b56d1..f4b6927b 100644 --- a/src/main/java/org/joda/beans/MetaBeans.java +++ b/src/main/java/org/joda/beans/MetaBeans.java @@ -15,11 +15,13 @@ */ package org.joda.beans; +import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.joda.beans.impl.RecordBean; import org.joda.beans.impl.flexi.FlexiBean; import org.joda.beans.impl.map.MapBean; @@ -32,11 +34,13 @@ final class MetaBeans { * The cache of meta-beans. */ private static final ConcurrentHashMap, MetaBean> META_BEANS = new ConcurrentHashMap<>(); + // not a ClassValue, as entries are registered manually /** * The cache of meta-bean providers; access is guarded by a lock on {@code MetaBeans.class}. */ private static final Map, MetaBeanProvider> META_BEAN_PROVIDERS = new HashMap<>(); + // not a ClassValue, as it is not on the fast path /** * Restricted constructor. @@ -93,15 +97,22 @@ private static MetaBean metaBeanLookup(Class cls) { if (meta != null) { return meta; } - var providerAnnotation = findProviderAnnotation(cls); - if (providerAnnotation != null) { - // Synchronization is necessary to prevent a race condition where the same meta-bean is registered twice - synchronized (MetaBeans.class) { - // Re-check in case the meta-bean has been added by another thread since we checked above - meta = META_BEANS.get(cls); - if (meta != null) { - return meta; - } + // Synchronization is necessary to prevent a race condition where the same meta-bean is registered twice + synchronized (MetaBeans.class) { + // Re-check in case the meta-bean has been added by another thread since we checked above + meta = META_BEANS.get(cls); + if (meta != null) { + return meta; + } + // handle records + if (cls.isRecord() && ImmutableBean.class.isAssignableFrom(cls)) { + @SuppressWarnings({"rawtypes", "unchecked"}) + var metaBean = RecordBean.register((Class) cls, MethodHandles.lookup()); + return metaBean; + } + // handle provider annotations + var providerAnnotation = findProviderAnnotation(cls); + if (providerAnnotation != null) { var providerClass = providerAnnotation.value(); try { var provider = META_BEAN_PROVIDERS.get(providerClass); diff --git a/src/main/java/org/joda/beans/impl/RecordBean.java b/src/main/java/org/joda/beans/impl/RecordBean.java new file mode 100644 index 00000000..667b9cfa --- /dev/null +++ b/src/main/java/org/joda/beans/impl/RecordBean.java @@ -0,0 +1,96 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans.impl; + +import java.lang.invoke.MethodHandles; + +import org.joda.beans.ImmutableBean; +import org.joda.beans.JodaBeanUtils; +import org.joda.beans.MetaBean; +import org.joda.beans.TypedMetaBean; + +/** + * A bean that is implemented using the record language feature. + *

+ * Simply add {@code implements RecordBean} to the record to turn it into a bean. + * There is no need to add annotations. Derived properties are not supported. + *

+ * For public records, this is the approach to use:. + * {@snippet lang="java": + * public static record StringIntPair(String first, int second) implements RecordBean { + * } + * } + *

+ * For non-public records, this is the approach to use: + * {@snippet lang="java": + * private static record StringLongPair(String first, long second) implements RecordBean { + * static { + * RecordBean.register(StringLongPair.class, MethodHandles.lookup()); + * } + * } + * } + *

+ * Note that a public record within a module that doesn't export the record will need to adopt the + * non-public approach. + * + * @param the record bean type + * @since 3.0.0 + */ +public interface RecordBean> extends ImmutableBean { + + /** + * Registers a meta-bean for the specified record. + *

+ * See the class-level Javadoc to understand when this method should be used. + *

+ * Note that this method must only be called once for each class, and never concurrently. + * If you follow one of the two patterns in the class-level Javadoc everything will be fine. + * + * @param the type of the record + * @param recordClass the record class, not null + * @param lookup the lookup object, granting permission to non-accessible methods + * @return the meta-bean + * @throws RuntimeException if unable to register the record + */ + public static MetaBean register(Class recordClass, MethodHandles.Lookup lookup) { + JodaBeanUtils.notNull(recordClass, "recordClass"); + JodaBeanUtils.notNull(lookup, "lookup"); + validateRecordClass(recordClass); + var metaBean = new RecordMetaBean<>(recordClass, lookup); + MetaBean.register(metaBean); + return metaBean; + } + + // Class could be erased, thus we double-check it + private static void validateRecordClass(Class recordClass) { + if (!recordClass.isRecord()) { + throw new IllegalArgumentException( + "RecordBean can only be used with records: " + recordClass.getName()); + } + if (!ImmutableBean.class.isAssignableFrom(recordClass)) { + throw new IllegalArgumentException( + "RecordBean can only be used with classes that implement ImmutableBean: " + recordClass.getName()); + } + } + + //------------------------------------------------------------------------- + @Override + @SuppressWarnings("unchecked") + public default TypedMetaBean metaBean() { + return (TypedMetaBean) MetaBean.of(getClass()); + } + +} diff --git a/src/main/java/org/joda/beans/impl/RecordBeanBuilder.java b/src/main/java/org/joda/beans/impl/RecordBeanBuilder.java new file mode 100644 index 00000000..60bfcabb --- /dev/null +++ b/src/main/java/org/joda/beans/impl/RecordBeanBuilder.java @@ -0,0 +1,73 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans.impl; + +import org.joda.beans.BeanBuilder; +import org.joda.beans.ImmutableBean; +import org.joda.beans.MetaProperty; + +/** + * The RecordBean bean builder. + * + * @param the record bean type + */ +final class RecordBeanBuilder implements BeanBuilder { + + private final RecordMetaBean metaBean; + private final Object[] data; + + RecordBeanBuilder(RecordMetaBean metaBean, Object[] data) { + this.metaBean = metaBean; + this.data = data; + } + + //----------------------------------------------------------------------- + @Override + public Object get(String propertyName) { + return data[metaBean.index(propertyName)]; + } + + @Override + public

P get(MetaProperty

metaProperty) { + return metaProperty.propertyType().cast(get(metaProperty.name())); + } + + @Override + public BeanBuilder set(String propertyName, Object value) { + data[metaBean.index(propertyName)] = value; + return this; + } + + @Override + public BeanBuilder set(MetaProperty metaProperty, Object value) { + return set(metaProperty.name(), value); + } + + @Override + public T build() { + return metaBean.build(data); + } + + /** + * Returns a string that summarises the builder. + * + * @return a summary string, not null + */ + @Override + public String toString() { + return "BeanBuilder: " + metaBean.beanType().getSimpleName(); + } +} diff --git a/src/main/java/org/joda/beans/impl/RecordMetaBean.java b/src/main/java/org/joda/beans/impl/RecordMetaBean.java new file mode 100644 index 00000000..8190f8d4 --- /dev/null +++ b/src/main/java/org/joda/beans/impl/RecordMetaBean.java @@ -0,0 +1,145 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans.impl; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.RecordComponent; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.joda.beans.Bean; +import org.joda.beans.BeanBuilder; +import org.joda.beans.ImmutableBean; +import org.joda.beans.JodaBeanUtils; +import org.joda.beans.MetaProperty; +import org.joda.beans.TypedMetaBean; + +/** + * A meta-bean for beans implemented using the record language feature. + * + * @param the record bean type + */ +final class RecordMetaBean extends BasicMetaBean implements TypedMetaBean { + + private final Class beanType; + private final Map> metaPropertyMap; + private final MethodHandle constructorHandle; + + RecordMetaBean(Class beanType, MethodHandles.Lookup lookup) { + JodaBeanUtils.notNull(beanType, "beanType"); + JodaBeanUtils.notNull(lookup, "lookup"); + this.beanType = beanType; + var recordComponents = beanType.getRecordComponents(); + var paramTypes = new Class[recordComponents.length]; + @SuppressWarnings("unchecked") + var properties = LinkedHashMap.>newLinkedHashMap(recordComponents.length); + for (int i = 0; i < recordComponents.length; i++) { + var name = recordComponents[i].getName(); + paramTypes[i] = recordComponents[i].getType(); + var getterHandle = findGetterHandle(recordComponents[i], lookup); + properties.put(name, new RecordMetaProperty(this, recordComponents[i], getterHandle, i)); + } + try { + var constructor = beanType.getDeclaredConstructor(paramTypes); + this.constructorHandle = findConstructorHandle(beanType, lookup, constructor); + } catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("Invalid record", ex); + } + this.metaPropertyMap = Collections.unmodifiableMap(properties); + } + + // finds the getter handle + private MethodHandle findGetterHandle(RecordComponent recordComponent, Lookup lookup) { + try { + var handle = lookup.unreflect(recordComponent.getAccessor()); + return handle.asType(MethodType.methodType(Object.class, Bean.class)); + } catch (IllegalAccessException ex) { + throw new IllegalArgumentException("Invalid record, method cannot be accessed: " + recordComponent.getName(), ex); + } + } + + // finds constructor which matches types exactly + private static MethodHandle findConstructorHandle( + Class beanType, + MethodHandles.Lookup lookup, + Constructor constructor) { + + try { + // spreader allows an Object[] to invoke the positional arguments + var constructorType = MethodType.methodType(Void.TYPE, constructor.getParameterTypes()); + var baseHandle = lookup.findConstructor(beanType, constructorType) + .asSpreader(Object[].class, constructor.getParameterTypes().length); + // change the return type so caller can use invokeExact() - this is the erased type of T + return baseHandle.asType(baseHandle.type().changeReturnType(ImmutableBean.class)); + } catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("Invalid record, constructor cannot be found: " + beanType.getSimpleName()); + } catch (IllegalAccessException ex) { + throw new IllegalArgumentException("Invalid record, constructor cannot be accessed: " + beanType.getSimpleName()); + } + } + + //------------------------------------------------------------------------- + // finds the index of a property + int index(String propertyName) { + var metaProperty = metaPropertyMap.get(propertyName); + if (metaProperty == null) { + throw new NoSuchElementException("Unknown property: " + propertyName); + } + return metaProperty.getConstructorIndex(); + } + + // builds a new instance + T build(Object[] data) { + try { + return (T) constructorHandle.invokeExact(data); + } catch (Error ex) { + throw ex; + } catch (Throwable ex) { + throw new IllegalArgumentException( + "Bean cannot be created: " + beanName() + " from " + Arrays.toString(data), ex); + } + } + + //------------------------------------------------------------------------- + @Override + public boolean isBuildable() { + return true; + } + + @Override + public BeanBuilder builder() { + var data = new Object[metaPropertyMap.size()]; + return new RecordBeanBuilder<>(this, data); + } + + @Override + public Class beanType() { + return beanType; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Map> metaPropertyMap() { + return (Map) metaPropertyMap; + } +} diff --git a/src/main/java/org/joda/beans/impl/RecordMetaProperty.java b/src/main/java/org/joda/beans/impl/RecordMetaProperty.java new file mode 100644 index 00000000..7776e86a --- /dev/null +++ b/src/main/java/org/joda/beans/impl/RecordMetaProperty.java @@ -0,0 +1,133 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans.impl; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.util.List; + +import org.joda.beans.Bean; +import org.joda.beans.MetaBean; +import org.joda.beans.MetaProperty; +import org.joda.beans.PropertyStyle; + +import com.google.common.collect.ImmutableList; + +/** + * The RecordBean meta-property. + * + * @param

the property type + */ +final class RecordMetaProperty

implements MetaProperty

{ + + private final MetaBean metaBean; + private final RecordComponent recordComponent; + private final MethodHandle getterHandle; + private final int constructorIndex; + + RecordMetaProperty( + MetaBean metaBean, + RecordComponent recordComponent, + MethodHandle getterHandle, + int constructorIndex) { + + this.metaBean = metaBean; + this.recordComponent = recordComponent; + this.getterHandle = getterHandle; + this.constructorIndex = constructorIndex; + } + + //------------------------------------------------------------------------- + @Override + public MetaBean metaBean() { + return metaBean; + } + + @Override + public String name() { + return recordComponent.getName(); + } + + @Override + public Class declaringType() { + return recordComponent.getDeclaringRecord(); + } + + @Override + @SuppressWarnings("unchecked") + public Class

propertyType() { + return (Class

) recordComponent.getType(); + } + + @Override + public Type propertyGenericType() { + return recordComponent.getGenericType(); + } + + @Override + public PropertyStyle style() { + return PropertyStyle.IMMUTABLE; + } + + @Override + public List annotations() { + return ImmutableList.copyOf(recordComponent.getAnnotations()); + } + + @Override + @SuppressWarnings("unchecked") + public P get(Bean bean) { + try { + return (P) getterHandle.invokeExact(bean); + } catch (Throwable ex) { + throw new RuntimeException("Property cannot be read: " + name(), ex); + } + } + + @Override + public void set(Bean bean, Object value) { + throw new UnsupportedOperationException("Property cannot be written: " + name()); + } + + //------------------------------------------------------------------------- + @Override + public boolean equals(Object obj) { + return obj instanceof MetaProperty other && + name().equals(other.name()) && + declaringType().equals(other.declaringType()); + } + + @Override + public int hashCode() { + return name().hashCode() ^ declaringType().hashCode(); + } + + /** + * Returns a string that summarises the meta-property. + * + * @return a summary string, not null + */ + @Override + public String toString() { + return declaringType().getSimpleName() + ":" + name(); + } + + int getConstructorIndex() { + return constructorIndex; + } +} diff --git a/src/main/java/org/joda/beans/impl/light/LightBeanBuilder.java b/src/main/java/org/joda/beans/impl/light/LightBeanBuilder.java index db26cc54..f554f516 100644 --- a/src/main/java/org/joda/beans/impl/light/LightBeanBuilder.java +++ b/src/main/java/org/joda/beans/impl/light/LightBeanBuilder.java @@ -39,6 +39,7 @@ class LightBeanBuilder * Constructs the builder wrapping the target bean. * * @param metaBean the target meta-bean, not null + * @param data the newly created data array */ LightBeanBuilder(LightMetaBean metaBean, Object[] data) { this.metaBean = metaBean; diff --git a/src/main/java/org/joda/beans/impl/light/LightMetaProperty.java b/src/main/java/org/joda/beans/impl/light/LightMetaProperty.java index cf4e64aa..658c9a63 100644 --- a/src/main/java/org/joda/beans/impl/light/LightMetaProperty.java +++ b/src/main/java/org/joda/beans/impl/light/LightMetaProperty.java @@ -126,8 +126,8 @@ static

LightMetaProperty

of( MethodHandle getter; try { - var methodTypeype = MethodType.methodType(getMethod.getReturnType(), getMethod.getParameterTypes()); - getter = lookup.findVirtual(field.getDeclaringClass(), getMethod.getName(), methodTypeype); + var methodType = MethodType.methodType(getMethod.getReturnType(), getMethod.getParameterTypes()); + getter = lookup.findVirtual(field.getDeclaringClass(), getMethod.getName(), methodType); } catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException ex) { throw new UnsupportedOperationException("Property cannot be read: " + propertyName, ex); } diff --git a/src/test/java/org/joda/beans/TestRecordBean.java b/src/test/java/org/joda/beans/TestRecordBean.java new file mode 100644 index 00000000..d4d45738 --- /dev/null +++ b/src/test/java/org/joda/beans/TestRecordBean.java @@ -0,0 +1,149 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.lang.invoke.MethodHandles; +import java.util.NoSuchElementException; + +import org.joda.beans.impl.RecordBean; +import org.joda.beans.sample.Pair; +import org.joda.beans.sample.RecordStrIntPair; +import org.joda.beans.ser.JodaBeanSer; +import org.junit.jupiter.api.Test; + +/** + * Test RecordBean. + */ +class TestRecordBean { + + private static record StringLongPair(String first, long second) implements RecordBean { + static { + RecordBean.register(StringLongPair.class, MethodHandles.lookup()); + } + } + + @Test + void test_metaBean_public() { + var test = new RecordStrIntPair("A", 1); + assertThat(test.first()).isEqualTo("A"); + assertThat(test.second()).isEqualTo(1); + + var meta = test.metaBean(); + assertThat(meta.isBuildable()).isTrue(); + assertThat(meta.beanType()).isEqualTo(RecordStrIntPair.class); + assertThat(meta.metaPropertyCount()).isEqualTo(2); + + var mp1 = meta.metaProperty("first"); + assertThat(mp1.name()).isEqualTo("first"); + assertThat(mp1.declaringType()).isEqualTo(RecordStrIntPair.class); + assertThat(mp1.metaBean()).isSameAs(meta); + assertThat(mp1.get(test)).isEqualTo("A"); + assertThat(mp1.propertyType()).isEqualTo(String.class); + assertThat(mp1.style()).isEqualTo(PropertyStyle.IMMUTABLE); + + var mp2 = meta.metaProperty("second"); + assertThat(mp2.name()).isEqualTo("second"); + assertThat(mp2.declaringType()).isEqualTo(RecordStrIntPair.class); + assertThat(mp2.metaBean()).isSameAs(meta); + assertThat(mp2.get(test)).isEqualTo(1); + assertThat(mp2.propertyType()).isEqualTo(int.class); + assertThat(mp2.style()).isEqualTo(PropertyStyle.IMMUTABLE); + + assertThat(mp1).isEqualTo(mp1) + .isNotEqualTo(mp2) + .isNotEqualTo("") + .isNotEqualTo(Pair.meta().first()) + .hasSameHashCodeAs(mp1) + .doesNotHaveSameHashCodeAs(mp2); + + var builder = meta.builder(); + builder.set("first", "B"); + builder.set("second", 2); + assertThat(builder.get("first")).isEqualTo("B"); + assertThat(builder.get(mp1)).isEqualTo("B"); + assertThat(builder.build()).isEqualTo(new RecordStrIntPair("B", 2)); + + builder.set(mp1, "A"); + assertThat(builder.build()).isEqualTo(new RecordStrIntPair("A", 2)); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> builder.get("foo")); + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> builder.set("foo", "")); + + var json = JodaBeanSer.PRETTY.jsonWriter().write(test); + var parsed = JodaBeanSer.COMPACT.jsonReader().read(json); + assertThat(parsed).isEqualTo(test); + } + + @Test + void test_metaBean_private() { + var test = new StringLongPair("A", 1L); + assertThat(test.first()).isEqualTo("A"); + assertThat(test.second()).isEqualTo(1L); + + var meta = test.metaBean(); + assertThat(meta.isBuildable()).isTrue(); + assertThat(meta.beanType()).isEqualTo(StringLongPair.class); + assertThat(meta.metaPropertyCount()).isEqualTo(2); + + var mp1 = meta.metaProperty("first"); + assertThat(mp1.name()).isEqualTo("first"); + assertThat(mp1.declaringType()).isEqualTo(StringLongPair.class); + assertThat(mp1.metaBean()).isSameAs(meta); + assertThat(mp1.get(test)).isEqualTo("A"); + assertThat(mp1.propertyType()).isEqualTo(String.class); + assertThat(mp1.style()).isEqualTo(PropertyStyle.IMMUTABLE); + + var mp2 = meta.metaProperty("second"); + assertThat(mp2.name()).isEqualTo("second"); + assertThat(mp2.declaringType()).isEqualTo(StringLongPair.class); + assertThat(mp2.metaBean()).isSameAs(meta); + assertThat(mp2.get(test)).isEqualTo(1L); + assertThat(mp2.propertyType()).isEqualTo(long.class); + assertThat(mp2.style()).isEqualTo(PropertyStyle.IMMUTABLE); + + assertThat(mp1).isEqualTo(mp1) + .isNotEqualTo(mp2) + .isNotEqualTo("") + .isNotEqualTo(Pair.meta().first()) + .hasSameHashCodeAs(mp1) + .doesNotHaveSameHashCodeAs(mp2); + + var builder = meta.builder(); + builder.set("first", "B"); + builder.set("second", 2L); + assertThat(builder.get("first")).isEqualTo("B"); + assertThat(builder.get(mp1)).isEqualTo("B"); + assertThat(builder.build()).isEqualTo(new StringLongPair("B", 2L)); + + builder.set(mp1, "A"); + assertThat(builder.build()).isEqualTo(new StringLongPair("A", 2L)); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> builder.get("foo")); + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> builder.set("foo", "")); + + var json = JodaBeanSer.PRETTY.jsonWriter().write(test); + var parsed = JodaBeanSer.COMPACT.jsonReader().read(json); + assertThat(parsed).isEqualTo(test); + } + +} diff --git a/src/test/java/org/joda/beans/sample/RecordStrIntPair.java b/src/test/java/org/joda/beans/sample/RecordStrIntPair.java new file mode 100644 index 00000000..b4e2cfd4 --- /dev/null +++ b/src/test/java/org/joda/beans/sample/RecordStrIntPair.java @@ -0,0 +1,28 @@ +/* + * Copyright 2001-present Stephen Colebourne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.joda.beans.sample; + +import org.joda.beans.impl.RecordBean; + +/** + * Mock, used for testing. + * + * @param first the first + * @param second the second + */ +public record RecordStrIntPair(String first, int second) implements RecordBean { + +}