Author: desruisseaux Date: Fri Feb 16 11:58:08 2018 New Revision: 1824452 URL: http://svn.apache.org/viewvc?rev=1824452&view=rev Log: Allow re-ordering of elements at marshalling time. This allow DefaultServiceIdentificationTest to pass.
Modified: sis/branches/ISO-19115-3/core/sis-utility/src/main/java/org/apache/sis/xml/FilteredWriter.java Modified: sis/branches/ISO-19115-3/core/sis-utility/src/main/java/org/apache/sis/xml/FilteredWriter.java URL: http://svn.apache.org/viewvc/sis/branches/ISO-19115-3/core/sis-utility/src/main/java/org/apache/sis/xml/FilteredWriter.java?rev=1824452&r1=1824451&r2=1824452&view=diff ============================================================================== --- sis/branches/ISO-19115-3/core/sis-utility/src/main/java/org/apache/sis/xml/FilteredWriter.java [UTF-8] (original) +++ sis/branches/ISO-19115-3/core/sis-utility/src/main/java/org/apache/sis/xml/FilteredWriter.java [UTF-8] Fri Feb 16 11:58:08 2018 @@ -17,8 +17,12 @@ package org.apache.sis.xml; import java.util.Map; +import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Set; import java.util.List; +import java.util.Queue; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -52,6 +56,35 @@ import static javax.xml.stream.XMLStream */ final class FilteredWriter extends FilteredXML implements XMLEventWriter { /** + * Elements that appear in different order in ISO 19139:2007 (or other legacy standards) compared + * to ISO 19115-3:2016 (or other newer standards). Key are names of elements to reorder. Values + * are the elements to skip before to write the element to reorder. + * + * <div class="note"><b>Example:</b> + * In {@code SV_ServiceIdentification}, {@code <srv:couplingType>} appears before {@code <srv:coupledResource>} + * according ISO 19115-3:2016. But in ISO 19139:2007, it was the reverse order. Since Apache SIS writes elements + * in the order defined by the newer standard, {@code <couplingType>} is encountered first. The set associated + * to that key tells us that, when writing legacy ISO 19139 document, we should skip {@code <coupledResource>} + * before to write {@code <srv:couplingType>}.</div> + * + * While this map is used for reordering elements according legacy standards, the {@link QName} keys and values + * use the namespaces of newer standards. This is because newer standards like ISO 19115-3 uses many namespaces + * where legacy ISO 19139:2007 used only one namespace, so using the newer names reduce the risk of confusion. + * + * @see #toSkip + * @see #deferred + * + * @todo Hard-coded for now. Should move to a resource file in a future version. + */ + private static final Map<QName, Set<QName>> ELEMENTS_TO_REORDER; + static { + final Map<QName, Set<QName>> m = new HashMap<>(4); + m.put(new QName(Namespaces.SRV, "couplingType", "srv"), Collections.singleton(new QName(Namespaces.SRV, "coupledResource", "srv"))); + m.put(new QName(Namespaces.SRV, "connectPoint", "srv"), Collections.singleton(new QName(Namespaces.SRV, "parameter", "srv"))); + ELEMENTS_TO_REORDER = m; + } + + /** * Where events are sent. */ private final XMLEventWriter out; @@ -68,12 +101,52 @@ final class FilteredWriter extends Filte private final Map<String,Namespace> uniqueNamespaces; /** + * {@code true} if events should be sent to {@link #deferred} instead than to {@link #out}. + * This is set to {@code true} when we see a {@link StartElement} having one of the names + * contained in the {@link #ELEMENTS_TO_REORDER} keys set. + */ + private boolean isDeferring; + + /** + * Events for which writing is deferred as long as there is elements {@linkplain #toSkip to skip}. + * The intent is to reorder elements that appear in a different order in legacy standards compared + * to newer standards. This is a FIFO (First-In-First-Out) queue. Namespaces are the exported ones + * (the ones after conversions from JAXB to the XML document to write). + * + * @see #ELEMENTS_TO_REORDER + */ + private final Queue<XMLEvent> deferred; + + /** + * If non-null, elements to skip before we can write the {@linkplain #deferred} events. + * Should be the {@link #ELEMENTS_TO_REORDER} value associated to the element to defer. + * A null value means that events can be written immediately to {@link #out}. + */ + private Set<QName> toSkip; + + /** + * Name of the the root element of a sub-tree to handle in a special way, or {@code null} if none. + * At first, this is the name of the {@link StartElement} of a sub-tree to defer (i.e. one of the + * keys in the {@link #ELEMENTS_TO_REORDER} map). Later, it becomes the names of sub-trees to skip + * (i.e. the {@link #toSkip} values). + */ + private QName subtreeRootName; + + /** + * Number of times that {@link #subtreeRootName} has been found. A value of 1 means that we started + * receiving events for that subtree. A value of 0 means that we finished receiving events for that + * subtree. A value greater than 1 means that there is nested sub-trees (should not happen). + */ + private int subtreeNesting; + + /** * Creates a new filter for the given version of the standards. */ FilteredWriter(final XMLEventWriter out, final FilterVersion version) { super(version); this.out = out; uniqueNamespaces = new LinkedHashMap<>(); + deferred = new ArrayDeque<>(); } /** @@ -182,8 +255,11 @@ final class FilteredWriter extends Filte } /** - * Adds an event to the output stream. This method may wrap the given event into another event - * for changing the namespace and/or the prefix. + * Converts an event from the namespaces used in JAXB annotations to the namespaces used in the XML document + * to write. This method may wrap the given event into another event for changing the namespace and prefix, + * or use the event as-is if no change is needed. + * + * @param event the event using JAXB namespaces. */ @Override @SuppressWarnings("unchecked") // TODO: remove on JDK9 @@ -201,6 +277,23 @@ final class FilteredWriter extends Filte } break; } + case END_ELEMENT: { + final EndElement e = event.asEndElement(); + final QName originalName = e.getName(); + final QName name = export(originalName); + final List<Namespace> namespaces = export(e.getNamespaces(), name != originalName); + if (namespaces != null) { + event = new FilteredEvent.End(e, name, namespaces); + } + if (toSkip != null) { // Check if a previous START_ELEMENT found a need to reorder. + if (originalName.equals(subtreeRootName)) { + subtreeNesting--; // May reach 0 but be followed by another element to skip. + } else if (subtreeNesting == 0) { + writeDeferred(); // About to exit the parent element containing deferred element. + } + } + break; + } case START_ELEMENT: { uniqueNamespaces.clear(); // Discard entries created by NAMESPACE events. final StartElement e = event.asStartElement(); @@ -219,20 +312,52 @@ final class FilteredWriter extends Filte } else { renamedAttributes.clear(); } - break; - } - case END_ELEMENT: { - final EndElement e = event.asEndElement(); - final QName originalName = e.getName(); - final QName name = export(originalName); - final List<Namespace> namespaces = export(e.getNamespaces(), name != originalName); - if (namespaces != null) { - event = new FilteredEvent.End(e, name, namespaces); + /* + * At this point, we finished to export the event (i.e. to convert namespaces to the URI + * used in the XML documents). The remaining code in this block is for handling the case + * where the elements should not be written immediately, but after some other events. + * This happen when legacy standards ordered some elements differently. + */ + if (toSkip == null) { + toSkip = ELEMENTS_TO_REORDER.get(originalName); + if (toSkip != null) { + subtreeRootName = originalName; // Found a new element to defer. + subtreeNesting = 1; + isDeferring = true; + } + } else if (subtreeNesting == 0) { + if (toSkip.contains(originalName)) { + subtreeRootName = originalName; // Defer until after that element. + subtreeNesting = 1; + } else { + writeDeferred(); // End of deferring. + } + } else if (originalName.equals(subtreeRootName)) { + subtreeNesting++; } break; } } - out.add(event); + if (isDeferring) { + deferred.add(event); + isDeferring = (subtreeNesting != 0); + } else { + out.add(event); + } + } + + /** + * Writes immediately all elements that were deferred. This happen because the next {@link StartElement} + * to write should be after the deferred element, or because we are about to exit the parent element that + * contains the deferred element, or because {@link #flush()} has been invoked. + * + * @see #ELEMENTS_TO_REORDER + */ + private void writeDeferred() throws XMLStreamException { + subtreeRootName = null; + toSkip = null; + XMLEvent v; + while ((v = deferred.poll()) != null) out.add(v); } /** @@ -351,6 +476,7 @@ final class FilteredWriter extends Filte */ @Override public void flush() throws XMLStreamException { + writeDeferred(); out.flush(); }