001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.io.imagery;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.Utils.equal;
006    
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.util.ArrayList;
010    import java.util.Arrays;
011    import java.util.List;
012    import java.util.Stack;
013    
014    import javax.xml.parsers.ParserConfigurationException;
015    import javax.xml.parsers.SAXParserFactory;
016    
017    import org.openstreetmap.josm.data.imagery.ImageryInfo;
018    import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
019    import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
020    import org.openstreetmap.josm.data.imagery.Shape;
021    import org.openstreetmap.josm.io.MirroredInputStream;
022    import org.openstreetmap.josm.io.UTFInputStreamReader;
023    import org.xml.sax.Attributes;
024    import org.xml.sax.InputSource;
025    import org.xml.sax.SAXException;
026    import org.xml.sax.helpers.DefaultHandler;
027    
028    public 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                    } else {
261                    }
262                    break;
263                case BOUNDS:
264                    entry.setBounds(bounds);
265                    bounds = null;
266                    break;
267                case SHAPE:
268                    bounds.addShape(shape);
269                    shape = null;
270                    break;
271                case CODE:
272                    projections.add(accumulator.toString());
273                    break;
274                case PROJECTIONS:
275                    entry.setServerProjections(projections);
276                    projections = null;
277                    break;
278                }
279            }
280        }
281    }