This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new ce4e984838 Clarify the name of `Record` and `RecordType` objects created at XML unmarshalling time. The previous "CharacterSequence" name was misleading because it suggested a field name, while it was used for a record containing a single field of type text. The "Single text" name is a better description. ce4e984838 is described below commit ce4e9848385a3db75ca5fd0ae02aaf05cff8b88b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Nov 25 18:54:05 2022 +0100 Clarify the name of `Record` and `RecordType` objects created at XML unmarshalling time. The previous "CharacterSequence" name was misleading because it suggested a field name, while it was used for a record containing a single field of type text. The "Single text" name is a better description. https://issues.apache.org/jira/browse/SIS-419 --- .../sis/internal/metadata/RecordSchemaSIS.java | 36 +++++-- .../apache/sis/internal/metadata/Resources.java | 15 +++ .../sis/internal/metadata/Resources.properties | 3 + .../sis/internal/metadata/Resources_fr.properties | 3 + .../org/apache/sis/util/iso/DefaultMemberName.java | 2 +- .../apache/sis/util/iso/DefaultRecordSchema.java | 22 +++- .../org/apache/sis/util/iso/DefaultRecordType.java | 13 ++- .../main/java/org/apache/sis/util/iso/Names.java | 14 +++ .../iso/quality/DefaultQuantitativeResultTest.java | 116 +++++++++++++++++++++ 9 files changed, 205 insertions(+), 19 deletions(-) diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/RecordSchemaSIS.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/RecordSchemaSIS.java index 7526e4866b..edf4a8c5cc 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/RecordSchemaSIS.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/RecordSchemaSIS.java @@ -19,6 +19,7 @@ package org.apache.sis.internal.metadata; import java.io.Serializable; import java.io.ObjectStreamException; import java.util.Collections; +import org.opengis.util.TypeName; import org.opengis.util.InternationalString; import org.apache.sis.internal.util.Constants; import org.apache.sis.util.iso.DefaultRecordType; @@ -30,7 +31,7 @@ import org.apache.sis.util.resources.Vocabulary; * The system-wide schema in the "SIS" namespace. * * @author Martin Desruisseaux (Geomatys) - * @version 1.0 + * @version 1.3 * @since 0.7 * @module */ @@ -42,18 +43,28 @@ public final class RecordSchemaSIS extends DefaultRecordSchema implements Serial public static final DefaultRecordSchema INSTANCE = new RecordSchemaSIS(); /** - * The type of record instances for holding a {@link String} value. + * The type name for a record having an unknown number of fields. + * This is used at {@code <gco:RecordType>} unmarshalling time, + * where the type is not well defined, by assuming one field per line. + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-419">SIS-419</a> + */ + public static final TypeName MULTILINE = INSTANCE.createRecordTypeName( + Resources.formatInternational(Resources.Keys.MultilineRecord)); + + /** + * The type of record instances for holding a single {@link String} value. */ public static final DefaultRecordType STRING; /** - * The type of record instances for holding a {@link Double} value. + * The type of record instances for holding a single {@link Double} value. */ public static final DefaultRecordType REAL; static { - final InternationalString label = Vocabulary.formatInternational(Vocabulary.Keys.Value); - STRING = (DefaultRecordType) INSTANCE.createRecordType("CharacterSequence", Collections.singletonMap(label, String.class)); - REAL = (DefaultRecordType) INSTANCE.createRecordType("Real", Collections.singletonMap(label, Double.class)); + final InternationalString field = Vocabulary.formatInternational(Vocabulary.Keys.Value); + STRING = singleton(Resources.Keys.SingleText, field, String.class); + REAL = singleton(Resources.Keys.SingleNumber, field, Double.class); } /** @@ -63,6 +74,19 @@ public final class RecordSchemaSIS extends DefaultRecordSchema implements Serial super(null, null, Constants.SIS); } + /** + * Creates a new record type of the given name, which will contain the given field. + * + * @param typeName the record type name as a {@link Resources.Keys} code. + * @param field the name of the singleton record field. + * @param valueClass the expected value type for the singleton field. + * @return a record type of the given name and field. + */ + private static DefaultRecordType singleton(final short typeName, final InternationalString field, final Class<?> valueClass) { + return (DefaultRecordType) INSTANCE.createRecordType( + Resources.formatInternational(typeName), Collections.singletonMap(field, valueClass)); + } + /** * On serialization, returns a proxy which will be resolved as {@link #INSTANCE} on deserialization. * diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java index 017b2c491f..1ce3c5c303 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java @@ -83,6 +83,21 @@ public final class Resources extends IndexedResourceBundle { */ public static final short ExpectedInterface_2 = 5; + /** + * Enregistrement multilignes + */ + public static final short MultilineRecord = 9; + + /** + * Single number + */ + public static final short SingleNumber = 8; + + /** + * Single text + */ + public static final short SingleText = 7; + /** * This metadata is not modifiable. */ diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties index 8ab6976ce6..6eaaf71b73 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties +++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties @@ -24,4 +24,7 @@ ConnectionAlreadyInitialized_1 = Connection to \u201c{0}\u201d database is al ElementAlreadyInitialized_1 = This metadata element is already initialized with value \u201c{0}\u201d. ElementsOmitted_1 = \u2026 {0} elements omitted \u2026 ExpectedInterface_2 = Illegal class `{1}`. Specify the `{0}` interface instead. +MultilineRecord = Enregistrement multilignes +SingleNumber = Single number +SingleText = Single text UnmodifiableMetadata = This metadata is not modifiable. diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties index 8359aaa4d5..b1e7ea2494 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties +++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties @@ -29,4 +29,7 @@ ConnectionAlreadyInitialized_1 = La connexion \u00e0 la base de donn\u00e9es ElementAlreadyInitialized_1 = Cet \u00e9l\u00e9ment de m\u00e9ta-donn\u00e9e est d\u00e9j\u00e0 initialis\u00e9 avec la valeur \u00ab\u202f{0}\u202f\u00bb. ElementsOmitted_1 = \u2026 {0} \u00e9l\u00e9ments omis \u2026 ExpectedInterface_2 = La classe `{1}` est ill\u00e9gale. Sp\u00e9cifiez l\u2019interface `{0}` \u00e0 la place. +MultilineRecord = Multiline record +SingleNumber = Nombre seul +SingleText = Texte seul UnmodifiableMetadata = Cette m\u00e9ta-donn\u00e9e n\u2019est pas modifiable. diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultMemberName.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultMemberName.java index f647a4b7b7..978d96215e 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultMemberName.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultMemberName.java @@ -149,7 +149,7 @@ public class DefaultMemberName extends DefaultLocalName implements MemberName { ////////////////////////////////////////////////////////////////////////////////////////////////// /** - * Empty constructor to be used by JAXB only. Despite its `final` declaration, + * Empty constructor to be used by JAXB only. Despite its {@code final} declaration, * the {@link #attributeType} field will be set by JAXB during unmarshalling. */ private DefaultMemberName() { diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordSchema.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordSchema.java index 6fe3407151..fe5233b3b7 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordSchema.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordSchema.java @@ -71,7 +71,7 @@ import org.apache.sis.internal.util.Strings; * {@link java.io.Serializable} interface) returning a system-wide static constant for their schema. * * @author Martin Desruisseaux (Geomatys) - * @version 0.5 + * @version 1.3 * * @see DefaultRecordType * @see DefaultRecord @@ -148,11 +148,24 @@ public class DefaultRecordSchema implements RecordSchema { return namespace.name().tip(); } + /** + * Creates the name of a record. + * + * @param typeName name of the record type to create. + * @return name of a record type. + * + * @since 1.3 + */ + public TypeName createRecordTypeName(final CharSequence typeName) { + ArgumentChecks.ensureNonNull("typeName", typeName); + return nameFactory.createTypeName(namespace, typeName, null); + } + /** * Creates a new record type of the given name, which will contain the given fields. * Fields are declared in iteration order. * - * @param typeName the record type name. + * @param typeName name of the record type to create. * @param fields the name of each record field, together with the expected value types. * @return a record type of the given name and fields. * @throws IllegalArgumentException if a record already exists for the given name but with different fields. @@ -160,9 +173,8 @@ public class DefaultRecordSchema implements RecordSchema { public RecordType createRecordType(final CharSequence typeName, final Map<CharSequence,Class<?>> fields) throws IllegalArgumentException { - ArgumentChecks.ensureNonNull("typeName", typeName); - ArgumentChecks.ensureNonNull("fields", fields); - final TypeName name = nameFactory.createTypeName(namespace, typeName); + ArgumentChecks.ensureNonNull("fields", fields); + final TypeName name = createRecordTypeName(typeName); final Map<CharSequence,Type> fieldTypes = ObjectConverters.derivedValues(fields, CharSequence.class, toTypes); RecordType record; synchronized (description) { diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java index a519a44dac..087364b7a8 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java @@ -89,7 +89,7 @@ import org.apache.sis.internal.metadata.RecordSchemaSIS; * so users wanting serialization may need to provide their own schema. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.3 * * @see DefaultRecord * @see DefaultRecordSchema @@ -452,14 +452,13 @@ public class DefaultRecordType extends RecordDefinition implements RecordType, S ////////////////////////////////////////////////////////////////////////////////////////////////// /** - * Constructs an initially empty type describing exactly one value as a string. + * Constructs an initially empty type describing one field per line. * See {@link #setValue(String)} for a description of the supported XML content. */ @SuppressWarnings("unused") private DefaultRecordType() { - final DefaultRecordType type = RecordSchemaSIS.STRING; - typeName = type.typeName; - container = type.container; + typeName = RecordSchemaSIS.MULTILINE; + container = RecordSchemaSIS.STRING.container; } /** @@ -476,7 +475,7 @@ public class DefaultRecordType extends RecordDefinition implements RecordType, S } /** - * Sets the record type value as a string. Current implementation expect one field per line. + * Sets the record type value as a string. Current implementation expects one field per line. * A record can be anything, but usages that we have seen so far write a character sequence * of what seems <var>key</var>-<var>description</var> pairs. Examples: * @@ -498,7 +497,7 @@ public class DefaultRecordType extends RecordDefinition implements RecordType, S element = element.subSequence(0, CharSequences.skipTrailingWhitespaces(element, 0, s)); // TODO: the part after ":" is the description. For now, we have no room for storing it. } - final MemberName m = Names.createMemberName(null, null, element, String.class); + final MemberName m = Names.createMemberName(typeName, element, String.class); fields.put(m, RecordSchemaSIS.INSTANCE.toAttributeType(String.class)); } fieldTypes = computeTransientFields(fields); diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java index 342a5f7011..7429297cbe 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java @@ -270,6 +270,20 @@ public final class Names extends Static { return factory.toTypeName(valueClass); // SIS-specific method. } + /** + * Creates a member name for a record of the given name. + * The given namespace is usually an instance of {@link TypeName}. + * + * @param namespace the name of the record which will contain this member name. + * @param localPart the name which is locale in the given namespace. + * @param valueClass the type of values, used for inferring a {@link TypeName} instance. + * @return a member name in the given namespace for values of the given type. + */ + static MemberName createMemberName(final GenericName namespace, final CharSequence localPart, final Class<?> valueClass) { + final DefaultNameFactory factory = DefaultFactories.forBuildin(NameFactory.class, DefaultNameFactory.class); + return factory.createMemberName(factory.createNameSpace(namespace, null), localPart, factory.toTypeName(valueClass)); + } + /** * Creates a member name for values of the given class. A {@link TypeName} will be inferred * from the given {@code valueClass} as documented in the {@link DefaultTypeName} javadoc. diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/quality/DefaultQuantitativeResultTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/quality/DefaultQuantitativeResultTest.java index d24c5ebac8..78f21b9fbd 100644 --- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/quality/DefaultQuantitativeResultTest.java +++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/quality/DefaultQuantitativeResultTest.java @@ -16,8 +16,22 @@ */ package org.apache.sis.metadata.iso.quality; +import java.util.Map; +import java.util.Iterator; +import java.util.Collections; +import javax.xml.bind.JAXBException; +import org.opengis.util.Type; +import org.opengis.util.RecordType; +import org.opengis.util.MemberName; +import org.opengis.metadata.quality.Element; +import org.opengis.metadata.quality.QuantitativeResult; +import org.apache.sis.internal.metadata.RecordSchemaSIS; +import org.apache.sis.internal.xml.LegacyNamespaces; import org.apache.sis.util.SimpleInternationalString; +import org.apache.sis.util.iso.DefaultRecord; +import org.apache.sis.test.TestUtilities; import org.apache.sis.test.TestCase; +import org.apache.sis.xml.XML; import org.junit.Test; import static org.junit.Assert.*; @@ -27,6 +41,7 @@ import static org.junit.Assert.*; * Tests {@link DefaultQuantitativeResult}. * * @author Martin Desruisseaux (Geomatys) + * @author Guilhem Legal (Geomatys) * @version 1.3 * @since 1.3 * @module @@ -47,4 +62,105 @@ public final strictfp class DefaultQuantitativeResultTest extends TestCase { r.setErrorStatistic(new SimpleInternationalString("a description")); assertFalse(r.isEmpty()); } + + /** + * Creates a {@code DefaultQuantitativeResult} instance wrapped in an element. + * The returned element is as below: + * + * {@preformat text + * Quantitative attribute accuracy + * ├─Measure + * │ └─Name of measure…………………… Some quality flag + * └─Quantitative result + * ├─Value……………………………………………… The quality is okay + * └─Value record type……………… CharacterSequence + * } + */ + @SuppressWarnings("deprecation") + private static Element createResultInsideElement() { + /* + * The `RecordType` constructor invoked at unmarshalling time sets the name + * to the hard-coded "Multiline record" string. We need to use the same name. + */ + final RecordType recordType = RecordSchemaSIS.INSTANCE.createRecordType( + RecordSchemaSIS.MULTILINE.toInternationalString(), + Collections.singletonMap("Result of quality measurement", String.class)); + /* + * The `Record` constructor invoked at unmarshalling time sets the type + * to the hard-coded "Single text" value. We need to use the same type. + */ + final RecordType singleText = RecordSchemaSIS.STRING; + final DefaultRecord record = new DefaultRecord(singleText); + record.set(TestUtilities.getSingleton(singleText.getMembers()), "The quality is okay"); + /* + * Record type and record value are set independently in two properties. + * In current implementation, `record.type` is not equal to `recordType`. + */ + assertNotEquals(recordType, record.getRecordType()); // Actually a limitation, not an intended behavior. + final DefaultQuantitativeResult result = new DefaultQuantitativeResult(); + result.setValues(Collections.singletonList(record)); + result.setValueType(recordType); + /* + * Opportunistically test the redirection implemented in deprecated methods. + */ + final DefaultQuantitativeAttributeAccuracy element = new DefaultQuantitativeAttributeAccuracy(); + element.setNamesOfMeasure(Collections.singleton(new SimpleInternationalString("Some quality flag"))); + element.setResults(Collections.singleton(result)); + return element; + } + + /** + * Tests unmarshalling of an XML element containing result records. + * + * @throws JAXBException if an error occurred during unmarshalling. + */ + @Test + public void testUnmarshallingLegacy() throws JAXBException { + final String xml = // Following XML shall match the object built by `createResultInsideElement()`. + "<gmd:DQ_QuantitativeAttributeAccuracy xmlns:gmd=\"" + LegacyNamespaces.GMD + '"' + + " xmlns:gco=\"" + LegacyNamespaces.GCO + "\">\n" + + " <gmd:nameOfMeasure>\n" + + " <gco:CharacterString>Some quality flag</gco:CharacterString>\n" + + " </gmd:nameOfMeasure>\n" + + " <gmd:result>\n" + + " <gmd:DQ_QuantitativeResult>\n" + + " <gmd:value>\n" + + " <gco:Record>The quality is okay</gco:Record>\n" + + " </gmd:value>\n" + + " <gmd:valueType>\n" + + " <gco:RecordType>Result of quality measurement</gco:RecordType>\n" + + " </gmd:valueType>\n" + + " </gmd:DQ_QuantitativeResult>\n" + + " </gmd:result>\n" + + "</gmd:DQ_QuantitativeAttributeAccuracy>"; + + final Element unmarshalled = (Element) XML.unmarshal(xml); + final Element programmatic = createResultInsideElement(); + /* + * Before to compare the two `Element`, compare some individual components. + * The intent is to identify which metadata is not equal in case of test failure. + */ + final QuantitativeResult uResult = (QuantitativeResult) TestUtilities.getSingleton(unmarshalled.getResults()); + final QuantitativeResult pResult = (QuantitativeResult) TestUtilities.getSingleton(programmatic.getResults()); + final RecordType uType = uResult.getValueType(); + final RecordType pType = pResult.getValueType(); + final Map<MemberName,Type> uFields = uType.getFieldTypes(); + final Map<MemberName,Type> pFields = pType.getFieldTypes(); + final Iterator<MemberName> uIter = uFields.keySet().iterator(); + final Iterator<MemberName> pIter = pFields.keySet().iterator(); + assertEquals(uFields.size(), pFields.size()); + while (uIter.hasNext() | pIter.hasNext()) { + final MemberName uName = uIter.next(); + final MemberName pName = pIter.next(); + assertEquals(uName.scope(), pName.scope()); + assertEquals(uName, pName); + assertEquals(uFields.get(uName), pFields.get(pName)); + } + assertEquals(uFields, pFields); + assertEquals(uType.getMembers(), pType.getMembers()); + assertEquals(uType.getTypeName(), pType.getTypeName()); + assertEquals(uType, pType); + assertEquals(uResult, pResult); + assertEquals(unmarshalled, programmatic); + } }