001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.io;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.io.IOException;
007    import java.io.InputStream;
008    import java.io.InputStreamReader;
009    import java.io.StringReader;
010    import java.io.UnsupportedEncodingException;
011    import java.util.Date;
012    
013    import javax.xml.parsers.ParserConfigurationException;
014    import javax.xml.parsers.SAXParserFactory;
015    
016    import org.openstreetmap.josm.data.coor.LatLon;
017    import org.openstreetmap.josm.data.osm.ChangesetDataSet;
018    import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
019    import org.openstreetmap.josm.data.osm.RelationMemberData;
020    import org.openstreetmap.josm.data.osm.User;
021    import org.openstreetmap.josm.data.osm.ChangesetDataSet.ChangesetModificationType;
022    import org.openstreetmap.josm.data.osm.history.HistoryNode;
023    import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
024    import org.openstreetmap.josm.data.osm.history.HistoryRelation;
025    import org.openstreetmap.josm.data.osm.history.HistoryWay;
026    import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
027    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
028    import org.openstreetmap.josm.tools.CheckParameterUtil;
029    import org.openstreetmap.josm.tools.DateUtils;
030    import org.xml.sax.Attributes;
031    import org.xml.sax.InputSource;
032    import org.xml.sax.Locator;
033    import org.xml.sax.SAXException;
034    import org.xml.sax.SAXParseException;
035    import org.xml.sax.helpers.DefaultHandler;
036    
037    /**
038     * Parser for OSM changeset content.
039     *
040     */
041    public class OsmChangesetContentParser {
042    
043        private InputSource source;
044        private ChangesetDataSet data;
045    
046        // FIXME: this class has many similarities with OsmHistoryReader.Parser and should be merged 
047        private class Parser extends DefaultHandler {
048    
049            /** the current primitive to be read */
050            private HistoryOsmPrimitive currentPrimitive;
051            /** the current change modification type */
052            private ChangesetDataSet.ChangesetModificationType currentModificationType;
053    
054            private Locator locator;
055    
056            @Override
057            public void setDocumentLocator(Locator locator) {
058                this.locator = locator;
059            }
060    
061            protected void throwException(String message) throws OsmDataParsingException {
062                throw new OsmDataParsingException(
063                        message
064                ).rememberLocation(locator);
065            }
066    
067            protected void throwException(Exception e) throws OsmDataParsingException {
068                throw new OsmDataParsingException(
069                        e
070                ).rememberLocation(locator);
071            }
072    
073            protected long getMandatoryAttributeLong(Attributes attr, String name) throws SAXException{
074                String v = attr.getValue(name);
075                if (v == null) {
076                    throwException(tr("Missing mandatory attribute ''{0}''.", name));
077                }
078                Long l = 0l;
079                try {
080                    l = Long.parseLong(v);
081                } catch(NumberFormatException e) {
082                    throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long. Got ''{1}''.", name, v));
083                }
084                if (l < 0) {
085                    throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long (>=0). Got ''{1}''.", name, v));
086                }
087                return l;
088            }
089    
090            protected Long getAttributeLong(Attributes attr, String name) throws SAXException{
091                String v = attr.getValue(name);
092                if (v == null)
093                    return null;
094                Long l = null;
095                try {
096                    l = Long.parseLong(v);
097                } catch(NumberFormatException e) {
098                    throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long. Got ''{1}''.", name, v));
099                }
100                if (l < 0) {
101                    throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long (>=0). Got ''{1}''.", name, v));
102                }
103                return l;
104            }
105    
106            protected Double getAttributeDouble(Attributes attr, String name) throws SAXException{
107                String v = attr.getValue(name);
108                if (v == null) {
109                    return null;
110                }
111                double d = 0.0;
112                try {
113                    d = Double.parseDouble(v);
114                } catch(NumberFormatException e) {
115                    throwException(tr("Illegal value for attribute ''{0}'' of type double. Got ''{1}''.", name, v));
116                }
117                return d;
118            }
119    
120            protected String getMandatoryAttributeString(Attributes attr, String name) throws SAXException{
121                String v = attr.getValue(name);
122                if (v == null) {
123                    throwException(tr("Missing mandatory attribute ''{0}''.", name));
124                }
125                return v;
126            }
127    
128            protected boolean getMandatoryAttributeBoolean(Attributes attr, String name) throws SAXException{
129                String v = attr.getValue(name);
130                if (v == null) {
131                    throwException(tr("Missing mandatory attribute ''{0}''.", name));
132                }
133                if ("true".equals(v)) return true;
134                if ("false".equals(v)) return false;
135                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type boolean. Got ''{1}''.", name, v));
136                // not reached
137                return false;
138            }
139    
140            protected  HistoryOsmPrimitive createPrimitive(Attributes atts, OsmPrimitiveType type) throws SAXException {
141                long id = getMandatoryAttributeLong(atts,"id");
142                long version = getMandatoryAttributeLong(atts,"version");
143                long changesetId = getMandatoryAttributeLong(atts,"changeset");
144                boolean visible= getMandatoryAttributeBoolean(atts, "visible");
145                
146                Long uid = getAttributeLong(atts, "uid");
147                String userStr = atts.getValue("user");
148                User user;
149                if (userStr != null) {
150                    if (uid != null) {
151                        user = User.createOsmUser(uid, userStr);
152                    } else {
153                        user = User.createLocalUser(userStr);
154                    }
155                } else {
156                    user = User.getAnonymous();
157                }
158    
159                String v = getMandatoryAttributeString(atts, "timestamp");
160                Date timestamp = DateUtils.fromString(v);
161                HistoryOsmPrimitive primitive = null;
162                if (type.equals(OsmPrimitiveType.NODE)) {
163                    Double lat = getAttributeDouble(atts, "lat");
164                    Double lon = getAttributeDouble(atts, "lon");
165                    LatLon coor = (lat != null && lon != null) ? new LatLon(lat,lon) : null;
166                    primitive = new HistoryNode(
167                            id,version,visible,user,changesetId,timestamp,coor
168                    );
169    
170                } else if (type.equals(OsmPrimitiveType.WAY)) {
171                    primitive = new HistoryWay(
172                            id,version,visible,user,changesetId,timestamp
173                    );
174                }if (type.equals(OsmPrimitiveType.RELATION)) {
175                    primitive = new HistoryRelation(
176                            id,version,visible,user,changesetId,timestamp
177                    );
178                }
179                return primitive;
180            }
181    
182            protected void startNode(Attributes atts) throws SAXException {
183                currentPrimitive= createPrimitive(atts, OsmPrimitiveType.NODE);
184            }
185    
186            protected void startWay(Attributes atts) throws SAXException {
187                currentPrimitive= createPrimitive(atts, OsmPrimitiveType.WAY);
188            }
189            protected void startRelation(Attributes atts) throws SAXException {
190                currentPrimitive= createPrimitive(atts, OsmPrimitiveType.RELATION);
191            }
192    
193            protected void handleTag(Attributes atts) throws SAXException {
194                String key= getMandatoryAttributeString(atts, "k");
195                String value= getMandatoryAttributeString(atts, "v");
196                currentPrimitive.put(key,value);
197            }
198    
199            protected void handleNodeReference(Attributes atts) throws SAXException {
200                long ref = getMandatoryAttributeLong(atts, "ref");
201                ((HistoryWay)currentPrimitive).addNode(ref);
202            }
203    
204            protected void handleMember(Attributes atts) throws SAXException {
205                long ref = getMandatoryAttributeLong(atts, "ref");
206                String v = getMandatoryAttributeString(atts, "type");
207                OsmPrimitiveType type = null;
208                try {
209                    type = OsmPrimitiveType.fromApiTypeName(v);
210                } catch(IllegalArgumentException e) {
211                    throwException(tr("Illegal value for mandatory attribute ''{0}'' of type OsmPrimitiveType. Got ''{1}''.", "type", v));
212                }
213                String role = getMandatoryAttributeString(atts, "role");
214                RelationMemberData member = new RelationMemberData(role, type,ref);
215                ((HistoryRelation)currentPrimitive).addMember(member);
216            }
217    
218            @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
219                if (qName.equals("node")) {
220                    startNode(atts);
221                } else if (qName.equals("way")) {
222                    startWay(atts);
223                } else if (qName.equals("relation")) {
224                    startRelation(atts);
225                } else if (qName.equals("tag")) {
226                    handleTag(atts);
227                } else if (qName.equals("nd")) {
228                    handleNodeReference(atts);
229                } else if (qName.equals("member")) {
230                    handleMember(atts);
231                } else if (qName.equals("osmChange")) {
232                    // do nothing
233                } else if (qName.equals("create")) {
234                    currentModificationType = ChangesetModificationType.CREATED;
235                } else if (qName.equals("modify")) {
236                    currentModificationType = ChangesetModificationType.UPDATED;
237                } else if (qName.equals("delete")) {
238                    currentModificationType = ChangesetModificationType.DELETED;
239                } else {
240                    System.err.println(tr("Warning: unsupported start element ''{0}'' in changeset content at position ({1},{2}). Skipping.", qName, locator.getLineNumber(), locator.getColumnNumber()));
241                }
242            }
243    
244            @Override
245            public void endElement(String uri, String localName, String qName) throws SAXException {
246                if (qName.equals("node")
247                        || qName.equals("way")
248                        || qName.equals("relation")) {
249                    if (currentModificationType == null) {
250                        throwException(tr("Illegal document structure. Found node, way, or relation outside of ''create'', ''modify'', or ''delete''."));
251                    }
252                    data.put(currentPrimitive, currentModificationType);
253                } else if (qName.equals("osmChange")) {
254                    // do nothing
255                } else if (qName.equals("create")) {
256                    currentModificationType = null;
257                } else if (qName.equals("modify")) {
258                    currentModificationType = null;
259                } else if (qName.equals("delete")) {
260                    currentModificationType = null;
261                } else if (qName.equals("tag")) {
262                    // do nothing
263                } else if (qName.equals("nd")) {
264                    // do nothing
265                } else if (qName.equals("member")) {
266                    // do nothing
267                } else {
268                    System.err.println(tr("Warning: unsupported end element ''{0}'' in changeset content at position ({1},{2}). Skipping.", qName, locator.getLineNumber(), locator.getColumnNumber()));
269                }
270            }
271    
272            @Override
273            public void error(SAXParseException e) throws SAXException {
274                throwException(e);
275            }
276    
277            @Override
278            public void fatalError(SAXParseException e) throws SAXException {
279                throwException(e);
280            }
281        }
282    
283        /**
284         * Create a parser
285         *
286         * @param source the input stream with the changeset content as XML document. Must not be null.
287         * @throws IllegalArgumentException thrown if source is null.
288         */
289        public OsmChangesetContentParser(InputStream source) throws UnsupportedEncodingException {
290            CheckParameterUtil.ensureParameterNotNull(source, "source");
291            this.source = new InputSource(new InputStreamReader(source, "UTF-8"));
292            data = new ChangesetDataSet();
293        }
294    
295        public OsmChangesetContentParser(String source) {
296            CheckParameterUtil.ensureParameterNotNull(source, "source");
297            this.source = new InputSource(new StringReader(source));
298            data = new ChangesetDataSet();
299        }
300    
301        /**
302         * Parses the content
303         *
304         * @param progressMonitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE}
305         * if null
306         * @return the parsed data
307         * @throws OsmDataParsingException thrown if something went wrong. Check for chained
308         * exceptions.
309         */
310        public ChangesetDataSet parse(ProgressMonitor progressMonitor) throws OsmDataParsingException {
311            if (progressMonitor == null) {
312                progressMonitor = NullProgressMonitor.INSTANCE;
313            }
314            try {
315                progressMonitor.beginTask("");
316                progressMonitor.indeterminateSubTask(tr("Parsing changeset content ..."));
317                SAXParserFactory.newInstance().newSAXParser().parse(source, new Parser());
318            } catch(OsmDataParsingException e){
319                throw e;
320            } catch (ParserConfigurationException e) {
321                throw new OsmDataParsingException(e);
322            } catch(SAXException e) {
323                throw new OsmDataParsingException(e);
324            } catch(IOException e) {
325                throw new OsmDataParsingException(e);
326            } finally {
327                progressMonitor.finishTask();
328            }
329            return data;
330        }
331    
332        /**
333         * Parses the content from the input source
334         *
335         * @return the parsed data
336         * @throws OsmDataParsingException thrown if something went wrong. Check for chained
337         * exceptions.
338         */
339        public ChangesetDataSet parse() throws OsmDataParsingException {
340            return parse(null);
341        }
342    }