001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Objects;
007import java.util.Optional;
008import java.util.Stack;
009import java.util.stream.Collectors;
010import java.util.stream.Stream;
011
012import org.openstreetmap.josm.io.GpxReader;
013import org.openstreetmap.josm.tools.Logging;
014import org.openstreetmap.josm.tools.Utils;
015import org.xml.sax.Attributes;
016
017/**
018 * Class extending <code>ArrayList&lt;GpxExtension&gt;</code>.
019 * Can be used to collect {@link GpxExtension}s while reading GPX files, see {@link GpxReader}
020 * @since 15496
021 */
022public class GpxExtensionCollection extends ArrayList<GpxExtension> {
023
024    private static final long serialVersionUID = 1L;
025
026    private Stack<GpxExtension> childStack;
027    private IWithAttributes parent;
028
029    /**
030     * Constructs a new {@link GpxExtensionCollection}
031     */
032    public GpxExtensionCollection() {}
033
034    /**
035     * Constructs a new {@link GpxExtensionCollection} with the given parent
036     * @param parent the parent extending {@link IWithAttributes}
037     */
038    public GpxExtensionCollection(IWithAttributes parent) {
039        this.parent = parent;
040    }
041
042    /**
043     * Adds a child extension to the last extension and pushes it to the stack.
044     * @param namespaceURI the URI of the XML namespace, used to determine supported
045     *                     extensions (josm, gpxx, gpxd) regardless of the prefix.
046     * @param qName the qualified name of the XML element including prefix
047     * @param atts the attributes
048     */
049    public void openChild(String namespaceURI, String qName, Attributes atts) {
050        if (childStack == null) {
051            childStack = new Stack<>();
052        }
053        GpxExtension child = new GpxExtension(namespaceURI, qName, atts);
054        if (!childStack.isEmpty()) {
055            childStack.lastElement().getExtensions().add(child);
056        } else {
057            this.add(child);
058        }
059        childStack.add(child);
060    }
061
062    /**
063     * Sets the value for the last child and pops it from the stack, so the next one will be added to its parent.
064     * A warning is issued if the qualified name does not equal the currently opened child.
065     * @param qName the qualified name
066     * @param value the value
067     */
068    public void closeChild(String qName, String value) {
069        if (Utils.isEmpty(childStack)) {
070            Logging.warn("Can''t close child ''{0}'', no element in stack.", qName);
071            return;
072        }
073
074        GpxExtension child = childStack.pop();
075        String childQN = child.getQualifiedName();
076
077        if (!childQN.equals(qName))
078            Logging.warn("Couldn''t close child ''{0}'', closed ''{1}'' instead.", qName, childQN);
079
080        child.setValue(value);
081    }
082
083    @Override
084    public boolean add(GpxExtension gpx) {
085        gpx.setParent(parent);
086        return super.add(gpx);
087    }
088
089    /**
090     * Creates and adds a new {@link GpxExtension} from the given parameters.
091     * @param prefix the prefix
092     * @param key the key/tag
093     * @return the added GpxExtension
094     */
095    public GpxExtension add(String prefix, String key) {
096        return add(prefix, key, null);
097    }
098
099    /**
100     * Creates and adds a new {@link GpxExtension} from the given parameters.
101     * @param prefix the prefix
102     * @param key the key/tag
103     * @param value the value, can be <code>null</code>
104     * @return the added GpxExtension
105     */
106    public GpxExtension add(String prefix, String key, String value) {
107        GpxExtension gpx = new GpxExtension(prefix, key, value);
108        add(gpx);
109        return gpx;
110    }
111
112    /**
113     * Creates and adds a new {@link GpxExtension}, if it hasn't been added yet. Shows it if it has.
114     * @param prefix the prefix
115     * @param key the key/tag
116     * @return the added or found GpxExtension
117     * @see GpxExtension#show()
118     */
119    public GpxExtension addIfNotPresent(String prefix, String key) {
120        GpxExtension gpx = get(prefix, key);
121        if (gpx != null) {
122            gpx.show();
123            return gpx;
124        }
125        return add(prefix, key);
126    }
127
128    /**
129     * Creates and adds a new {@link GpxExtension} or updates its value and shows it if already present.
130     * @param prefix the prefix
131     * @param key the key/tag
132     * @param value the value
133     * @return the added or found GpxExtension
134     * @see GpxExtension#show()
135     */
136    public GpxExtension addOrUpdate(String prefix, String key, String value) {
137        GpxExtension gpx = get(prefix, key);
138        if (gpx != null) {
139            gpx.show();
140            gpx.setValue(value);
141            return gpx;
142        } else {
143            return add(prefix, key, value);
144        }
145    }
146
147    @Override
148    public boolean addAll(Collection<? extends GpxExtension> extensions) {
149        extensions.forEach(e -> e.setParent(parent));
150        return super.addAll(extensions);
151    }
152
153    /**
154     * Adds an extension from a flat chain without prefix, e.g. when converting from OSM
155     * @param chain the full key chain, e.g. ["extension", "gpxx", "TrackExtension", "DisplayColor"]
156     * @param value the value
157     */
158    public void addFlat(String[] chain, String value) {
159        if (chain.length >= 3 && "extension".equals(chain[0])) {
160            String prefix = "other".equals(chain[1]) ? "" : chain[1];
161            GpxExtensionCollection previous = this;
162            for (int i = 2; i < chain.length; i++) {
163                if (i != 2 || !"segment".equals(chain[2])) {
164                    previous = previous.add(prefix, chain[i], i == chain.length - 1 ? value : null).getExtensions();
165                }
166            }
167        }
168    }
169
170    /**
171     * Gets the extension with the given prefix and key
172     * @param prefix the prefix
173     * @param key the key/tag
174     * @return the {@link GpxExtension} if found or <code>null</code>
175     */
176    public GpxExtension get(String prefix, String key) {
177        return stream(prefix, key).findAny().orElse(null);
178    }
179
180    /**
181     * Gets all extensions with the given prefix and key
182     * @param prefix the prefix
183     * @param key the key/tag
184     * @return a {@link GpxExtensionCollection} with the extensions, empty collection if none found
185     */
186    public GpxExtensionCollection getAll(String prefix, String key) {
187        GpxExtensionCollection copy = new GpxExtensionCollection(this.parent);
188        copy.addAll(stream(prefix, key).collect(Collectors.toList()));
189        return copy;
190    }
191
192    /**
193     * Gets a stream with all extensions with the given prefix and key
194     * @param prefix the prefix
195     * @param key the key/tag
196     * @return the <code>Stream&lt;{@link GpxExtension}&gt;</code>
197     */
198    public Stream<GpxExtension> stream(String prefix, String key) {
199        return stream().filter(e -> Objects.equals(prefix, e.getPrefix()) && Objects.equals(key, e.getKey()));
200    }
201
202    /**
203     * Searches recursively for the extension with the given prefix and key in all children
204     * @param prefix the prefix to look for
205     * @param key the key to look for
206     * @return the extension if found, otherwise <code>null</code>
207     */
208    public GpxExtension find(String prefix, String key) {
209        return this.stream()
210                .map(child -> child.findExtension(prefix, key)).filter(Objects::nonNull)
211                .findFirst().orElse(null);
212    }
213
214    /**
215     * Searches and removes recursively all extensions with the given prefix and key in all children
216     * @param prefix the prefix to look for
217     * @param key the key to look for
218      */
219    public void findAndRemove(String prefix, String key) {
220        Optional.ofNullable(find(prefix, key)).ifPresent(GpxExtension::remove);
221    }
222
223    /**
224     * Removes all {@link GpxExtension}s with the given prefix and key in direct children
225     * @param prefix the prefix
226     * @param key the key/tag
227     */
228    public void remove(String prefix, String key) {
229        stream(prefix, key)
230        .collect(Collectors.toList()) //needs to be collected to avoid concurrent modification
231        .forEach(e -> super.remove(e));
232    }
233
234    /**
235     * Removes all extensions with the given prefix in direct children
236     * @param prefix the prefix
237     */
238    public void removeAllWithPrefix(String prefix) {
239        stream()
240        .filter(e -> Objects.equals(prefix, e.getPrefix()))
241        .collect(Collectors.toList()) //needs to be collected to avoid concurrent modification
242        .forEach(e -> super.remove(e));
243    }
244
245    /**
246     * Gets all prefixes of direct (writable) children
247     * @return stream with the prefixes
248     */
249    public Stream<String> getPrefixesStream() {
250        return stream()
251                .filter(GpxExtension::isVisible)
252                .map(GpxExtension::getPrefix)
253                .distinct();
254    }
255
256    /**
257     * Determines if this collection contains writable extensions.
258     * @return <code>true</code> if this collection contains writable extensions
259     */
260    public boolean isVisible() {
261        return stream().anyMatch(GpxExtension::isVisible);
262    }
263
264    @Override
265    public void clear() {
266        if (childStack != null) {
267            childStack.clear();
268            childStack = null;
269        }
270        super.clear();
271    }
272
273}