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