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