001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.Utils.equal;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.List;
012import java.util.Stack;
013
014import javax.xml.parsers.ParserConfigurationException;
015import javax.xml.parsers.SAXParserFactory;
016
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.MirroredInputStream;
022import org.openstreetmap.josm.io.UTFInputStreamReader;
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            InputStream in = new MirroredInputStream(source);
054            InputSource is = new InputSource(UTFInputStreamReader.create(in, "UTF-8"));
055            factory.newSAXParser().parse(is, parser);
056            return parser.entries;
057        } catch (SAXException e) {
058            throw e;
059        } catch (ParserConfigurationException e) {
060            e.printStackTrace(); // broken SAXException chaining
061            throw new SAXException(e);
062        }
063    }
064
065    private static class Parser extends DefaultHandler {
066        private StringBuffer accumulator = new StringBuffer();
067
068        private Stack<State> states;
069
070        List<ImageryInfo> entries;
071
072        /**
073         * Skip the current entry because it has mandatory attributes
074         * that this version of JOSM cannot process.
075         */
076        boolean skipEntry;
077
078        ImageryInfo entry;
079        ImageryBounds bounds;
080        Shape shape;
081        List<String> projections;
082
083        @Override public void startDocument() {
084            accumulator = new StringBuffer();
085            skipEntry = false;
086            states = new Stack<State>();
087            states.push(State.INIT);
088            entries = new ArrayList<ImageryInfo>();
089            entry = null;
090            bounds = null;
091            projections = null;
092        }
093
094        @Override
095        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
096            accumulator.setLength(0);
097            State newState = null;
098            switch (states.peek()) {
099            case INIT:
100                if (qName.equals("imagery")) {
101                    newState = State.IMAGERY;
102                }
103                break;
104            case IMAGERY:
105                if (qName.equals("entry")) {
106                    entry = new ImageryInfo();
107                    skipEntry = false;
108                    newState = State.ENTRY;
109                }
110                break;
111            case ENTRY:
112                if (Arrays.asList(new String[] {
113                        "name",
114                        "type",
115                        "default",
116                        "url",
117                        "eula",
118                        "min-zoom",
119                        "max-zoom",
120                        "attribution-text",
121                        "attribution-url",
122                        "logo-image",
123                        "logo-url",
124                        "terms-of-use-text",
125                        "terms-of-use-url",
126                        "country-code",
127                        "icon",
128                }).contains(qName)) {
129                    newState = State.ENTRY_ATTRIBUTE;
130                } else if (qName.equals("bounds")) {
131                    try {
132                        bounds = new ImageryBounds(
133                                atts.getValue("min-lat") + "," +
134                                        atts.getValue("min-lon") + "," +
135                                        atts.getValue("max-lat") + "," +
136                                        atts.getValue("max-lon"), ",");
137                    } catch (IllegalArgumentException e) {
138                        break;
139                    }
140                    newState = State.BOUNDS;
141                } else if (qName.equals("projections")) {
142                    projections = new ArrayList<String>();
143                    newState = State.PROJECTIONS;
144                }
145                break;
146            case BOUNDS:
147                if (qName.equals("shape")) {
148                    shape = new Shape();
149                    newState = State.SHAPE;
150                }
151                break;
152            case SHAPE:
153                if (qName.equals("point")) {
154                    try {
155                        shape.addPoint(atts.getValue("lat"), atts.getValue("lon"));
156                    } catch (IllegalArgumentException e) {
157                        break;
158                    }
159                }
160                break;
161            case PROJECTIONS:
162                if (qName.equals("code")) {
163                    newState = State.CODE;
164                }
165                break;
166            }
167            /**
168             * Did not recognize the element, so the new state is UNKNOWN.
169             * This includes the case where we are already inside an unknown
170             * element, i.e. we do not try to understand the inner content
171             * of an unknown element, but wait till it's over.
172             */
173            if (newState == null) {
174                newState = State.UNKNOWN;
175            }
176            states.push(newState);
177            if (newState == State.UNKNOWN && equal(atts.getValue("mandatory"), "true")) {
178                skipEntry = true;
179            }
180            return;
181        }
182
183        @Override
184        public void characters(char[] ch, int start, int length) {
185            accumulator.append(ch, start, length);
186        }
187
188        @Override
189        public void endElement(String namespaceURI, String qName, String rqName) {
190            switch (states.pop()) {
191            case INIT:
192                throw new RuntimeException("parsing error: more closing than opening elements");
193            case ENTRY:
194                if (qName.equals("entry")) {
195                    if (!skipEntry) {
196                        entries.add(entry);
197                    }
198                    entry = null;
199                }
200                break;
201            case ENTRY_ATTRIBUTE:
202                if (qName.equals("name")) {
203                    entry.setName(tr(accumulator.toString()));
204                } else if (qName.equals("type")) {
205                    boolean found = false;
206                    for (ImageryType type : ImageryType.values()) {
207                        if (equal(accumulator.toString(), type.getUrlString())) {
208                            entry.setImageryType(type);
209                            found = true;
210                            break;
211                        }
212                    }
213                    if (!found) {
214                        skipEntry = true;
215                    }
216                } else if (qName.equals("default")) {
217                    if (accumulator.toString().equals("true")) {
218                        entry.setDefaultEntry(true);
219                    } else if (accumulator.toString().equals("false")) {
220                        entry.setDefaultEntry(false);
221                    } else {
222                        skipEntry = true;
223                    }
224                } else if (qName.equals("url")) {
225                    entry.setUrl(accumulator.toString());
226                } else if (qName.equals("eula")) {
227                    entry.setEulaAcceptanceRequired(accumulator.toString());
228                } else if (qName.equals("min-zoom") || qName.equals("max-zoom")) {
229                    Integer val = null;
230                    try {
231                        val = Integer.parseInt(accumulator.toString());
232                    } catch(NumberFormatException e) {
233                        val = null;
234                    }
235                    if (val == null) {
236                        skipEntry = true;
237                    } else {
238                        if (qName.equals("min-zoom")) {
239                            entry.setDefaultMinZoom(val);
240                        } else {
241                            entry.setDefaultMaxZoom(val);
242                        }
243                    }
244                } else if (qName.equals("attribution-text")) {
245                    entry.setAttributionText(accumulator.toString());
246                } else if (qName.equals("attribution-url")) {
247                    entry.setAttributionLinkURL(accumulator.toString());
248                } else if (qName.equals("logo-image")) {
249                    entry.setAttributionImage(accumulator.toString());
250                } else if (qName.equals("logo-url")) {
251                    entry.setAttributionImageURL(accumulator.toString());
252                } else if (qName.equals("terms-of-use-text")) {
253                    entry.setTermsOfUseText(accumulator.toString());
254                } else if (qName.equals("terms-of-use-url")) {
255                    entry.setTermsOfUseURL(accumulator.toString());
256                } else if (qName.equals("country-code")) {
257                    entry.setCountryCode(accumulator.toString());
258                } else if (qName.equals("icon")) {
259                    entry.setIcon(accumulator.toString());
260                }
261                break;
262            case BOUNDS:
263                entry.setBounds(bounds);
264                bounds = null;
265                break;
266            case SHAPE:
267                bounds.addShape(shape);
268                shape = null;
269                break;
270            case CODE:
271                projections.add(accumulator.toString());
272                break;
273            case PROJECTIONS:
274                entry.setServerProjections(projections);
275                projections = null;
276                break;
277            }
278        }
279    }
280}