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