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