001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import java.util.Objects;
005import java.util.Optional;
006
007import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
008import org.xml.sax.Attributes;
009
010/**
011 * A GpxExtension that has attributes and child extensions (implements {@link IWithAttributes} and {@link GpxConstants}).
012 * @since 15496
013 */
014public class GpxExtension extends WithAttributes {
015    private final String qualifiedName, prefix, key;
016    private IWithAttributes parent;
017    private String value;
018    private boolean visible = true;
019
020    /**
021     * Constructs a new {@link GpxExtension}.
022     * @param prefix the prefix
023     * @param key the key
024     * @param value the value
025     */
026    public GpxExtension(String prefix, String key, String value) {
027        this.prefix = Optional.ofNullable(prefix).orElse("");
028        this.key = key;
029        this.value = value;
030        this.qualifiedName = (this.prefix.isEmpty() ? "" : this.prefix + ":") + key;
031    }
032
033    /**
034     * Creates a new {@link GpxExtension}
035     *
036     * @param namespaceURI the URI of the XML namespace, used to determine supported extensions
037     *                     (josm, gpxx, gpxd) regardless of the prefix that could legally vary from file to file.
038     * @param qName the qualified name of the XML element including prefix
039     * @param atts the attributes
040     */
041    public GpxExtension(String namespaceURI, String qName, Attributes atts) {
042        qualifiedName = qName;
043        int dot = qName.indexOf(':');
044        String p = findPrefix(namespaceURI);
045        if (p == null) {
046            if (dot != -1) {
047                prefix = qName.substring(0, dot);
048            } else {
049                prefix = "";
050            }
051        } else {
052            prefix = p;
053        }
054        key = qName.substring(dot + 1);
055        for (int i = 0; i < atts.getLength(); i++) {
056            attr.put(atts.getLocalName(i), atts.getValue(i));
057        }
058    }
059
060    /**
061     * Finds the default prefix used by JOSM for the given namespaceURI as the document is free specify another one.
062     * @param namespaceURI namespace URI
063     * @return the prefix
064     */
065    public static String findPrefix(String namespaceURI) {
066        if (XML_URI_EXTENSIONS_DRAWING.equals(namespaceURI))
067            return "gpxd";
068
069        if (XML_URI_EXTENSIONS_GARMIN.equals(namespaceURI))
070            return "gpxx";
071
072        if (XML_URI_EXTENSIONS_JOSM.equals(namespaceURI))
073            return "josm";
074
075        return null;
076    }
077
078    /**
079     * Finds the namespace for the given default prefix, if supported with schema location
080     * @param prefix the prefix used by JOSM
081     * @return the {@link XMLNamespace} element, location and URI can be <code>null</code> if not found.
082     */
083    public static XMLNamespace findNamespace(String prefix) {
084        switch (prefix) {
085        case "gpxx":
086            return new XMLNamespace("gpxx", XML_URI_EXTENSIONS_GARMIN, XML_XSD_EXTENSIONS_GARMIN);
087        case "gpxd":
088            return new XMLNamespace("gpxd", XML_URI_EXTENSIONS_DRAWING, XML_XSD_EXTENSIONS_DRAWING);
089        case "josm":
090            return new XMLNamespace("josm", XML_URI_EXTENSIONS_JOSM, XML_XSD_EXTENSIONS_JOSM);
091        }
092        return null;
093    }
094
095    /**
096     * @return the qualified name of the XML element
097     */
098    public String getQualifiedName() {
099        return qualifiedName;
100    }
101
102    /**
103     * @return the prefix of the XML namespace
104     */
105    public String getPrefix() {
106        return prefix;
107    }
108
109    /**
110     * @return the key (local element name) of the extension
111     */
112    public String getKey() {
113        return key;
114    }
115
116    /**
117     * @return the flattened extension key of this extension, used for conversion to OSM layers
118     */
119    public String getFlatKey() {
120        String ret = "";
121        if (parent != null && parent instanceof GpxExtension) {
122            GpxExtension ext = (GpxExtension) parent;
123            ret = ext.getFlatKey() + ":";
124        }
125        return ret + getKey();
126    }
127
128    /**
129     * Searches recursively for the extension with the given key in all children
130     * @param sPrefix the prefix to look for
131     * @param sKey the key to look for
132     * @return the extension if found, otherwise <code>null</code>
133     */
134    public GpxExtension findExtension(String sPrefix, String sKey) {
135        if (prefix.equalsIgnoreCase(sPrefix) && key.equalsIgnoreCase(sKey)) {
136            return this;
137        } else {
138            for (GpxExtension child : getExtensions()) {
139                GpxExtension ext = child.findExtension(sPrefix, sKey);
140                if (ext != null) {
141                    return ext;
142                }
143            }
144            return null;
145        }
146    }
147
148    /**
149     * @return the value of the extension
150     */
151    public String getValue() {
152        return value;
153    }
154
155    /**
156     * @param value the value to set
157     */
158    public void setValue(String value) {
159        this.value = value;
160    }
161
162    /**
163     * Removes this extension from its parent and all then-empty parents
164     * @throws IllegalStateException if parent not set
165     */
166    public void remove() {
167        if (parent == null)
168            throw new IllegalStateException("Extension " + qualifiedName + " has no parent, can't remove it.");
169
170        parent.getExtensions().remove(this);
171        if (parent instanceof GpxExtension) {
172            GpxExtension gpx = ((GpxExtension) parent);
173            if ((gpx.getValue() == null || gpx.getValue().trim().isEmpty())
174                    && gpx.getAttributes().isEmpty()
175                    && gpx.getExtensions().isEmpty()) {
176                gpx.remove();
177            }
178        }
179    }
180
181    /**
182     * Hides this extension and all then-empty parents so it isn't written
183     * @see #isVisible()
184     */
185    public void hide() {
186        visible = false;
187        if (parent != null && parent instanceof GpxExtension) {
188            GpxExtension gpx = (GpxExtension) parent;
189            if ((gpx.getValue() == null || gpx.getValue().trim().isEmpty())
190                    && gpx.getAttributes().isEmpty()
191                    && !gpx.getExtensions().isVisible()) {
192                gpx.hide();
193            }
194        }
195    }
196
197    /**
198     * Shows this extension and all parents so it can be written
199     * @see #isVisible()
200     */
201    public void show() {
202        visible = true;
203        if (parent != null && parent instanceof GpxExtension) {
204            ((GpxExtension) parent).show();
205        }
206    }
207
208    /**
209     * @return if this extension should be written, used for hiding colors during export without removing them
210     */
211    public boolean isVisible() {
212        return visible;
213    }
214
215    /**
216     * @return the parent element of this extension, can be another extension or gpx elements (data, track, segment, ...)
217     */
218    public IWithAttributes getParent() {
219        return parent;
220    }
221
222    /**
223     * Sets the parent for this extension
224     * @param parent the parent
225     * @throws IllegalStateException if parent already set
226     */
227    public void setParent(IWithAttributes parent) {
228        if (this.parent != null)
229            throw new IllegalStateException("Parent of extension " + qualifiedName + " is already set");
230
231        this.parent = parent;
232    }
233
234    @Override
235    public int hashCode() {
236        return Objects.hash(prefix, key, value, attr, visible, super.hashCode());
237    }
238
239    @Override
240    public boolean equals(Object obj) {
241        if (this == obj)
242            return true;
243        if (obj == null)
244            return false;
245        if (!super.equals(obj))
246            return false;
247        if (!(obj instanceof GpxExtension))
248            return false;
249        GpxExtension other = (GpxExtension) obj;
250        if (visible != other.visible)
251            return false;
252        if (prefix == null) {
253            if (other.prefix != null)
254                return false;
255        } else if (!prefix.equals(other.prefix))
256            return false;
257        if (key == null) {
258            if (other.key != null)
259                return false;
260        } else if (!key.equals(other.key))
261            return false;
262        if (value == null) {
263            if (other.value != null)
264                return false;
265        } else if (!value.equals(other.value))
266            return false;
267        if (attr == null) {
268            if (other.attr != null)
269                return false;
270        } else if (!attr.equals(other.attr))
271            return false;
272        return true;
273    }
274}