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