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<GpxExtension></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<{@link GpxExtension}></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}