diff --git a/pom.xml b/pom.xml
index 73b4dea..993de2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
org.orcid
orcid-model
- 4.0.1
+ 4.0.2
ORCID - Model
Container for all classes that will be used to marshal and unmarshal XML/JSON from the API
diff --git a/src/main/java/org/orcid/jaxb/model/record_v2/Email.java b/src/main/java/org/orcid/jaxb/model/record_v2/Email.java
index 330de28..b13e85c 100644
--- a/src/main/java/org/orcid/jaxb/model/record_v2/Email.java
+++ b/src/main/java/org/orcid/jaxb/model/record_v2/Email.java
@@ -133,6 +133,21 @@ public void setPrimary(Boolean primary) {
this.primary = primary;
}
+ // java.beans.Introspector recognises an "is" read method only for primitive boolean, so these
+ // boxed flags need get-accessors to be readable by code that copies beans by introspection,
+ // such as ORCID-Source's v2.0/v2.1 version converter. The isX() methods stay for existing callers.
+ public Boolean getVerified() {
+ return verified;
+ }
+
+ public Boolean getCurrent() {
+ return current;
+ }
+
+ public Boolean getPrimary() {
+ return primary;
+ }
+
@Override
public int hashCode() {
final int prime = 31;
diff --git a/src/test/java/org/orcid/jaxb/model/record_v2/EmailBeanPropertiesTest.java b/src/test/java/org/orcid/jaxb/model/record_v2/EmailBeanPropertiesTest.java
new file mode 100644
index 0000000..6277e19
--- /dev/null
+++ b/src/test/java/org/orcid/jaxb/model/record_v2/EmailBeanPropertiesTest.java
@@ -0,0 +1,109 @@
+package org.orcid.jaxb.model.record_v2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.beans.BeanInfo;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.InputStream;
+import java.io.StringWriter;
+
+import jakarta.xml.bind.JAXBContext;
+import jakarta.xml.bind.Marshaller;
+
+import org.junit.Test;
+import org.orcid.jaxb.model.common_v2.Visibility;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * The flags are boxed Booleans. java.beans.Introspector only accepts an "is" read method for
+ * primitive boolean, so with isVerified() alone the property is write-only to anything that
+ * copies beans by introspection, and ORCID-Source's v2.0/v2.1 version converter returned every
+ * email with verified and primary set to null. These tests pin the read methods, and check that
+ * adding them leaves the XML and JSON output as it was.
+ */
+public class EmailBeanPropertiesTest {
+
+ @Test
+ public void testIntrospectorSeesAReadAndAWriteMethodForEachFlag() throws Exception {
+ BeanInfo beanInfo = Introspector.getBeanInfo(Email.class);
+ for (String flag : new String[] { "verified", "primary", "current" }) {
+ PropertyDescriptor pd = find(beanInfo, flag);
+ assertNotNull(flag + " has no read method, so bean copies drop it", pd.getReadMethod());
+ assertNotNull(flag + " has no write method", pd.getWriteMethod());
+ assertEquals(Boolean.class, pd.getPropertyType());
+ }
+ }
+
+ @Test
+ public void testMarshalledXmlKeepsTheFlagAttributes() throws Exception {
+ Email email = emailWithFlags(Boolean.TRUE, Boolean.FALSE);
+ email.setCurrent(Boolean.TRUE);
+
+ StringWriter xml = new StringWriter();
+ Marshaller marshaller = JAXBContext.newInstance(Email.class).createMarshaller();
+ marshaller.marshal(email, xml);
+
+ assertTrue(xml.toString(), xml.toString().contains("verified=\"true\""));
+ assertTrue(xml.toString(), xml.toString().contains("primary=\"false\""));
+ assertFalse("current is transient and must not be marshalled", xml.toString().contains("current"));
+ }
+
+ /**
+ * Reads the flags the way a bean copy does, through the introspected read method, so false
+ * has to come back as false rather than being skipped as unreadable.
+ */
+ @Test
+ public void testUnmarshalledSampleFlagsAreReadableByIntrospection() throws Exception {
+ try (InputStream in = getClass().getResourceAsStream("/record_2.0/samples/read_samples/email-2.0.xml")) {
+ Email email = (Email) JAXBContext.newInstance(Email.class).createUnmarshaller().unmarshal(in);
+ BeanInfo beanInfo = Introspector.getBeanInfo(Email.class);
+ assertEquals(Boolean.TRUE, read(beanInfo, "primary", email));
+ assertEquals(Boolean.FALSE, read(beanInfo, "verified", email));
+ }
+ }
+
+ /**
+ * Jackson accepts both isVerified() and getVerified() for a boxed Boolean. It must pick one
+ * without a "Conflicting getter definitions" error, and false must stay false.
+ */
+ @Test
+ public void testJacksonSerialisesEachFlagOnceWithoutAConflict() throws Exception {
+ Email email = emailWithFlags(Boolean.TRUE, Boolean.FALSE);
+ email.setCurrent(Boolean.TRUE);
+
+ String json = new ObjectMapper().writeValueAsString(email);
+
+ assertTrue(json, json.contains("\"verified\":true"));
+ assertTrue(json, json.contains("\"primary\":false"));
+ assertFalse("current is @JsonIgnore and must not be serialised", json.contains("current"));
+ }
+
+ private Email emailWithFlags(Boolean verified, Boolean primary) {
+ Email email = new Email();
+ email.setEmail("test@orcid.org");
+ email.setVisibility(Visibility.PUBLIC);
+ email.setVerified(verified);
+ email.setPrimary(primary);
+ return email;
+ }
+
+ private Object read(BeanInfo beanInfo, String name, Email email) throws Exception {
+ PropertyDescriptor pd = find(beanInfo, name);
+ assertNotNull(name + " has no read method, so bean copies drop it", pd.getReadMethod());
+ return pd.getReadMethod().invoke(email);
+ }
+
+ private PropertyDescriptor find(BeanInfo beanInfo, String name) {
+ for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
+ if (name.equals(pd.getName())) {
+ return pd;
+ }
+ }
+ throw new AssertionError("no property descriptor named " + name);
+ }
+}