001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.imagery;
003
004import java.io.IOException;
005import java.io.InputStream;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.Objects;
012import java.util.Stack;
013
014import javax.xml.parsers.ParserConfigurationException;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.imagery.ImageryInfo;
018import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
019import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
020import org.openstreetmap.josm.data.imagery.Shape;
021import org.openstreetmap.josm.io.CachedFile;
022import org.openstreetmap.josm.io.UTFInputStreamReader;
023import org.openstreetmap.josm.tools.LanguageInfo;
024import org.openstreetmap.josm.tools.Utils;
025import org.xml.sax.Attributes;
026import org.xml.sax.InputSource;
027import org.xml.sax.SAXException;
028import org.xml.sax.helpers.DefaultHandler;
029
030public class ImageryReader {
031
032    private String source;
033
034    private enum State {
035        INIT,               // initial state, should always be at the bottom of the stack
036        IMAGERY,            // inside the imagery element
037        ENTRY,              // inside an entry
038        ENTRY_ATTRIBUTE,    // note we are inside an entry attribute to collect the character data
039        PROJECTIONS,
040        CODE,
041        BOUNDS,
042        SHAPE,
043        NO_TILE,
044        METADATA,
045        UNKNOWN,            // element is not recognized in the current context
046    }
047
048    public ImageryReader(String source) {
049        this.source = source;
050    }
051
052    public List<ImageryInfo> parse() throws SAXException, IOException {
053        Parser parser = new Parser();
054        try {
055            try (InputStream in = new CachedFile(source)
056                    .setMaxAge(1*CachedFile.DAYS)
057                    .setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince)
058                    .getInputStream()) {
059                InputSource is = new InputSource(UTFInputStreamReader.create(in));
060                Utils.parseSafeSAX(is, parser);
061                return parser.entries;
062            }
063        } catch (SAXException e) {
064            throw e;
065        } catch (ParserConfigurationException e) {
066            Main.error(e); // broken SAXException chaining
067            throw new SAXException(e);
068        }
069    }
070
071    private static class Parser extends DefaultHandler {
072        private StringBuilder accumulator = new StringBuilder();
073
074        private Stack<State> states;
075
076        private List<ImageryInfo> entries;
077
078        /**
079         * Skip the current entry because it has mandatory attributes
080         * that this version of JOSM cannot process.
081         */
082        private boolean skipEntry;
083
084        private ImageryInfo entry;
085        private ImageryBounds bounds;
086        private Shape shape;
087        // language of last element, does only work for simple ENTRY_ATTRIBUTE's
088        private String lang;
089        private List<String> projections;
090        private Map<String, String> noTileHeaders;
091        private Map<String, String> metadataHeaders;
092
093        @Override
094        public void startDocument() {
095            accumulator = new StringBuilder();
096            skipEntry = false;
097            states = new Stack<>();
098            states.push(State.INIT);
099            entries = new ArrayList<>();
100            entry = null;
101            bounds = null;
102            projections = null;
103            noTileHeaders = null;
104        }
105
106        @Override
107        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
108            accumulator.setLength(0);
109            State newState = null;
110            switch (states.peek()) {
111            case INIT:
112                if ("imagery".equals(qName)) {
113                    newState = State.IMAGERY;
114                }
115                break;
116            case IMAGERY:
117                if ("entry".equals(qName)) {
118                    entry = new ImageryInfo();
119                    skipEntry = false;
120                    newState = State.ENTRY;
121                    noTileHeaders = new HashMap<>();
122                    metadataHeaders = new HashMap<>();
123                }
124                break;
125            case ENTRY:
126                if (Arrays.asList(new String[] {
127                        "name",
128                        "id",
129                        "type",
130                        "description",
131                        "default",
132                        "url",
133                        "eula",
134                        "min-zoom",
135                        "max-zoom",
136                        "attribution-text",
137                        "attribution-url",
138                        "logo-image",
139                        "logo-url",
140                        "terms-of-use-text",
141                        "terms-of-use-url",
142                        "country-code",
143                        "icon",
144                        "tile-size",
145                }).contains(qName)) {
146                    newState = State.ENTRY_ATTRIBUTE;
147                    lang = atts.getValue("lang");
148                } else if ("bounds".equals(qName)) {
149                    try {
150                        bounds = new ImageryBounds(
151                                atts.getValue("min-lat") + ',' +
152                                        atts.getValue("min-lon") + ',' +
153                                        atts.getValue("max-lat") + ',' +
154                                        atts.getValue("max-lon"), ",");
155                    } catch (IllegalArgumentException e) {
156                        break;
157                    }
158                    newState = State.BOUNDS;
159                } else if ("projections".equals(qName)) {
160                    projections = new ArrayList<>();
161                    newState = State.PROJECTIONS;
162                } else if ("no-tile-header".equals(qName)) {
163                    noTileHeaders.put(atts.getValue("name"), atts.getValue("value"));
164                    newState = State.NO_TILE;
165                } else if ("metadata-header".equals(qName)) {
166                    metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
167                    newState = State.METADATA;
168                }
169                break;
170            case BOUNDS:
171                if ("shape".equals(qName)) {
172                    shape = new Shape();
173                    newState = State.SHAPE;
174                }
175                break;
176            case SHAPE:
177                if ("point".equals(qName)) {
178                    try {
179                        shape.addPoint(atts.getValue("lat"), atts.getValue("lon"));
180                    } catch (IllegalArgumentException e) {
181                        break;
182                    }
183                }
184                break;
185            case PROJECTIONS:
186                if ("code".equals(qName)) {
187                    newState = State.CODE;
188                }
189                break;
190            }
191            /**
192             * Did not recognize the element, so the new state is UNKNOWN.
193             * This includes the case where we are already inside an unknown
194             * element, i.e. we do not try to understand the inner content
195             * of an unknown element, but wait till it's over.
196             */
197            if (newState == null) {
198                newState = State.UNKNOWN;
199            }
200            states.push(newState);
201            if (newState == State.UNKNOWN && "true".equals(atts.getValue("mandatory"))) {
202                skipEntry = true;
203            }
204        }
205
206        @Override
207        public void characters(char[] ch, int start, int length) {
208            accumulator.append(ch, start, length);
209        }
210
211        @Override
212        public void endElement(String namespaceURI, String qName, String rqName) {
213            switch (states.pop()) {
214            case INIT:
215                throw new RuntimeException("parsing error: more closing than opening elements");
216            case ENTRY:
217                if ("entry".equals(qName)) {
218                    entry.setNoTileHeaders(noTileHeaders);
219                    noTileHeaders = null;
220                    entry.setMetadataHeaders(metadataHeaders);
221                    metadataHeaders = null;
222
223                    if (!skipEntry) {
224                        entries.add(entry);
225                    }
226                    entry = null;
227                }
228                break;
229            case ENTRY_ATTRIBUTE:
230                switch(qName) {
231                case "name":
232                    entry.setName(lang == null ? LanguageInfo.getJOSMLocaleCode(null) : lang, accumulator.toString());
233                    break;
234                case "description":
235                    entry.setDescription(lang, accumulator.toString());
236                    break;
237                case "id":
238                    entry.setId(accumulator.toString());
239                    break;
240                case "type":
241                    boolean found = false;
242                    for (ImageryType type : ImageryType.values()) {
243                        if (Objects.equals(accumulator.toString(), type.getTypeString())) {
244                            entry.setImageryType(type);
245                            found = true;
246                            break;
247                        }
248                    }
249                    if (!found) {
250                        skipEntry = true;
251                    }
252                    break;
253                case "default":
254                    switch (accumulator.toString()) {
255                    case "true":
256                        entry.setDefaultEntry(true);
257                        break;
258                    case "false":
259                        entry.setDefaultEntry(false);
260                        break;
261                    default:
262                        skipEntry = true;
263                    }
264                    break;
265                case "url":
266                    entry.setUrl(accumulator.toString());
267                    break;
268                case "eula":
269                    entry.setEulaAcceptanceRequired(accumulator.toString());
270                    break;
271                case "min-zoom":
272                case "max-zoom":
273                    Integer val = null;
274                    try {
275                        val = Integer.valueOf(accumulator.toString());
276                    } catch (NumberFormatException e) {
277                        val = null;
278                    }
279                    if (val == null) {
280                        skipEntry = true;
281                    } else {
282                        if ("min-zoom".equals(qName)) {
283                            entry.setDefaultMinZoom(val);
284                        } else {
285                            entry.setDefaultMaxZoom(val);
286                        }
287                    }
288                    break;
289                case "attribution-text":
290                    entry.setAttributionText(accumulator.toString());
291                    break;
292                case "attribution-url":
293                    entry.setAttributionLinkURL(accumulator.toString());
294                    break;
295                case "logo-image":
296                    entry.setAttributionImage(accumulator.toString());
297                    break;
298                case "logo-url":
299                    entry.setAttributionImageURL(accumulator.toString());
300                    break;
301                case "terms-of-use-text":
302                    entry.setTermsOfUseText(accumulator.toString());
303                    break;
304                case "terms-of-use-url":
305                    entry.setTermsOfUseURL(accumulator.toString());
306                    break;
307                case "country-code":
308                    entry.setCountryCode(accumulator.toString());
309                    break;
310                case "icon":
311                    entry.setIcon(accumulator.toString());
312                    break;
313                case "tile-size":
314                    Integer tileSize = null;
315                    try {
316                        tileSize = Integer.valueOf(accumulator.toString());
317                    } catch (NumberFormatException e) {
318                        tileSize = null;
319                    }
320                    if (tileSize == null) {
321                        skipEntry = true;
322                    } else {
323                        entry.setTileSize(tileSize.intValue());
324                    }
325                    break;
326                }
327                break;
328            case BOUNDS:
329                entry.setBounds(bounds);
330                bounds = null;
331                break;
332            case SHAPE:
333                bounds.addShape(shape);
334                shape = null;
335                break;
336            case CODE:
337                projections.add(accumulator.toString());
338                break;
339            case PROJECTIONS:
340                entry.setServerProjections(projections);
341                projections = null;
342                break;
343            case NO_TILE:
344                break;
345
346            }
347        }
348    }
349}