blob: 414e092375ef1c48bfcefb43d43000085e6f0308 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sis.xml;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Iterator;
import javax.xml.XMLConstants;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.Namespace;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
/**
* Base class of events that are wrappers over the events emitted during the reading or writing of an XML document.
* Those wrappers are used for changing the namespace and sometimes the name of XML elements or attributes.
*
* @author Martin Desruisseaux (Geomatys)
*/
abstract class TransformedEvent<E extends XMLEvent> implements XMLEvent {
/**
* The event to be exported in a different namespace.
*/
final E event;
/**
* Exported name of the attribute or element. Will often (but not necessarily) have
* the same local part as {@code event.getName()} but a different namespace.
*/
final QName name;
/**
* Exports a new event.
*
* @param event the event to be exported in a different namespace.
* @param name the exported name of the attribute or element.
*/
TransformedEvent(final E event, final QName name) {
this.event = event;
this.name = name;
}
@Override public boolean isStartElement() {return false;}
@Override public boolean isAttribute() {return false;}
@Override public boolean isNamespace() {return false;}
@Override public boolean isEndElement() {return false;}
@Override public boolean isEntityReference() {return false;}
@Override public boolean isProcessingInstruction() {return false;}
@Override public boolean isCharacters() {return false;}
@Override public boolean isStartDocument() {return false;}
@Override public boolean isEndDocument() {return false;}
@Override public StartElement asStartElement() {throw new ClassCastException();}
@Override public EndElement asEndElement() {throw new ClassCastException();}
@Override public Characters asCharacters() {throw new ClassCastException();}
@Override public Location getLocation() {return event.getLocation();}
@Override public QName getSchemaType() {return event.getSchemaType();}
public final QName getName() {return name;}
/**
* Appends the name to the given output.
* This is a convenience method for {@link #write(Appendable)} implementations.
*/
final Appendable name(final Appendable out) throws IOException {
final String prefix = name.getPrefix();
if (prefix != null && !prefix.isEmpty()) {
out.append(prefix).append(':');
}
return out.append(name.getLocalPart());
}
/**
* Implementation of {@link #writeAsEncodedUnicode(Writer)} and {@link #toString()}.
*/
abstract void write(Appendable out) throws IOException;
/**
* Writes the event as per the XML 1.0 without indentation or whitespace.
* This implementation delegates to {@link #write(Appendable)}.
*/
@Override
public final void writeAsEncodedUnicode(final Writer writer) throws XMLStreamException {
try {
write(writer);
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
/**
* Returns the event as per the XML 1.0 without indentation or whitespace.
* This implementation delegates to {@link #write(Appendable)}.
*/
@Override
public final String toString() {
final StringBuilder out = new StringBuilder();
try {
write(out);
} catch (IOException e) { // Should never happen since we write to a StringBuilder.
return e.toString();
}
return out.toString();
}
/**
* Wrapper over a namespace emitted during the reading or writing of an XML document.
* This wrapper is used for changing the namespace URI. The wrapped {@link #event}
* should be a {@link Namespace}, but this class accepts also the {@link Attribute}
* super-type for allowing the {@link Type} attribute to create synthetic namespaces.
*/
static final class NS extends TransformedEvent<Attribute> implements Namespace {
/** The URI of the namespace. */
private final String namespaceURI;
/** Wraps the given event with a different prefix and URI. */
NS(final Attribute event, final String prefix, final String namespaceURI) {
super(event, new QName(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, prefix, XMLConstants.XMLNS_ATTRIBUTE));
this.namespaceURI = namespaceURI;
}
@Override public boolean isNamespace() {return true;}
@Override public int getEventType() {return NAMESPACE;}
@Override public String getNamespaceURI() {return namespaceURI;}
@Override public String getValue() {return namespaceURI;}
@Override public String getDTDType() {return event.getDTDType();}
@Override public boolean isSpecified() {return event instanceof Namespace && event.isSpecified();}
@Override public String getPrefix() {return (name != null) ? name.getLocalPart() : null;}
@Override public boolean isDefaultNamespaceDeclaration() {return (name != null) && name.getLocalPart().isEmpty();}
@Override void write(final Appendable out) throws IOException {
name(out).append("=\"").append(namespaceURI).append('"');
}
}
/**
* Wrapper over an attribute emitted during the reading or writing of an XML document.
* This wrapper is used for changing the namespace of the attribute.
*/
static class Attr extends TransformedEvent<Attribute> implements Attribute {
/** Wraps the given event with a different name. */
Attr(final Attribute event, final QName name) {
super(event, name);
}
/** Cast or wrap the given attribute to an {@code Attr} instance. */
static Attr castOrWrap(final Attribute a) {
if (a instanceof Attr) return (Attr) a;
else return new Attr(a, a.getName());
}
@Override public boolean isAttribute() {return true;}
@Override public int getEventType() {return ATTRIBUTE;}
@Override public String getValue() {return event.getValue();}
@Override public String getDTDType() {return event.getDTDType();}
@Override public boolean isSpecified() {return event.isSpecified();}
@Override void write(final Appendable out) throws IOException {
name(out).append("=\"").append(getValue()).append('"');
}
}
/**
* The {@code "xsi:type"} attribute. Contrarily to other attributes, the name is unchanged compared
* to the original attribute; instead the value is different. Even in unchanged, the {@link QName}
* is specified at construction time because it is required by the parent class.
*/
static final class Type extends Attr {
/** The attribute value. */
private final String value;
/** If the value requires a new prefix to be bound, the namespace declaration for it. */
Namespace namespace;
/** Wraps the given event with a different value. */
Type(final Attribute event, final QName name, final String value) {
super(event, name);
this.value = value;
}
/** Returns the {@code "xsi:type"} attribute value. */
@Override public String getValue() {return value;}
}
/**
* Wrapper over an element emitted during the reading or writing of an XML document.
* This wrapper is used for changing the namespace and sometimes the name of the element.
*/
static final class End extends TransformedEvent<EndElement> implements EndElement {
/** The namespaces, may or may not be the same as the wrapped event. */
private final List<Namespace> namespaces;
/** Wraps the given event with potentially different name and namespaces. */
End(final EndElement event, final QName name, final List<Namespace> namespaces) {
super(event, name);
this.namespaces = namespaces;
}
@Override public boolean isEndElement() {return true;}
@Override public EndElement asEndElement() {return this;}
@Override public int getEventType() {return END_ELEMENT;}
@Override public Iterator<Namespace> getNamespaces() {return namespaces.iterator();}
@Override void write(final Appendable out) throws IOException {
name(out.append("</")).append('>');
}
}
/**
* Wrapper over an element emitted during the reading or writing of an XML document.
* This wrapper is used for changing the namespace and sometimes the name of the element.
* The attributes may also be modified.
*/
static class Start extends TransformedEvent<StartElement> implements StartElement {
/** The namespaces, may or may not be the same as the wrapped event. */
private final List<Namespace> namespaces;
/** The attributes, may or may not be the same as the wrapped event. */
private final List<Attribute> attributes;
/** The version to export, used for wrapping namespace context. */
final TransformVersion version;
/** Wraps the given event with potentially different name, namespaces and attributes. */
Start(StartElement event, QName name, List<Namespace> namespaces, List<Attribute> attributes, TransformVersion version) {
super(event, name);
this.namespaces = namespaces;
this.attributes = attributes;
this.version = version;
}
@Override public final boolean isStartElement() {return true;}
@Override public final StartElement asStartElement() {return this;}
@Override public final int getEventType() {return START_ELEMENT;}
@Override public final Iterator<Namespace> getNamespaces() {return namespaces.iterator();}
@Override public final Iterator<Attribute> getAttributes() {return attributes.iterator();}
/**
* Returns the attribute referred to by the given name, or {@code null} if none.
* Current implementation is okay on the assumption that there is few attributes.
*/
@Override
public final Attribute getAttributeByName(final QName name) {
for (final Attribute attr : attributes) {
if (name.equals(attr.getName())) {
return attr;
}
}
return null;
}
/**
* Gets the URI used by JAXB annotations for the given prefix used in the XML document.
* Returns {@code null} if no unique URI can be provided for the given prefix.
* In particular, the {@code "gmd"} prefix from legacy ISO 19139:2007 standard can map
* to the {@code "http://standards.iso.org/iso/19115/-3/mdb/1.0"}, {@code "…/cit/1.0"}
* and other namespaces in ISO 19115-3:2016. Because of this ambiguity,
* this method returns {@code null} for the {@code "gmd"} prefix.
*
* <p>At unmarshalling time, events are created by an arbitrary {@link javax.xml.stream.XMLEventReader}
* with namespaces used in the XML document. {@link TransformingReader} wraps those events using this
* class for converting the XML namespaces to the namespaces used by JAXB annotations.</p>
*
* <p>At marshalling time, events are created by JAXB using namespaces used in JAXB annotations.
* {@link TransformingWriter} wraps those events for converting those namespaces to the ones used
* in the XML document. This is the opposite than the work performed by this default implementation
* and must be handled by a {@code Start} subclass.</p>
*/
@Override
public String getNamespaceURI(final String prefix) {
return version.importNS(event.getNamespaceURI(prefix));
}
/**
* Returns a context mapping prefixes used in XML document to namespaces used in JAXB annotations.
* The {@code TransformingNamespaces.Inverse.getNamespaceURI(String)} method in that context shall do
* the same work as {@link #getNamespaceURI(String)} in this event.
*/
@Override
public NamespaceContext getNamespaceContext() {
return TransformingNamespaces.asJAXB(event.getNamespaceContext(), version);
}
/**
* Writes the event as per the XML 1.0 without indentation or whitespace.
*/
@Override
final void write(final Appendable out) throws IOException {
name(out.append('<'));
final int n = attributes.size();
for (int i=0; i<n; i++) {
out.append(' ');
Attr.castOrWrap(attributes.get(i)).write(out);
}
out.append('>');
}
}
}