SVN 1.7: status through command line: fix NPE
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn17 / commandLine / SvnStatusHandler.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.idea.svn17.commandLine;
17
18 import com.intellij.openapi.util.Getter;
19 import com.intellij.openapi.util.text.StringUtil;
20 import com.intellij.util.containers.Convertor;
21 import com.intellij.util.containers.MultiMap;
22 import org.jetbrains.idea.svn17.portable.PortableStatus;
23 import org.jetbrains.idea.svn17.portable.StatusCallbackConvertor;
24 import org.tmatesoft.svn.core.SVNNodeKind;
25 import org.tmatesoft.svn.core.internal.util.SVNDate;
26 import org.tmatesoft.svn.core.wc.SVNInfo;
27 import org.tmatesoft.svn.core.wc.SVNRevision;
28 import org.tmatesoft.svn.core.wc.SVNStatusType;
29 import org.xml.sax.Attributes;
30 import org.xml.sax.SAXException;
31 import org.xml.sax.helpers.DefaultHandler;
32
33 import java.io.File;
34 import java.util.*;
35
36 /**
37  * Created with IntelliJ IDEA.
38  * User: Irina.Chernushina
39  * Date: 1/25/12
40  * Time: 7:59 PM
41  */
42 public class SvnStatusHandler extends DefaultHandler {
43   private String myChangelistName;
44   private List<PortableStatus> myDefaultListStatuses;
45   private MultiMap<String, PortableStatus> myCurrentListChanges;
46   private PortableStatus myPending;
47
48   private final List<ElementHandlerBase> myParseStack;
49   private final Map<String, Getter<ElementHandlerBase>> myElementsMap;
50   private final DataCallback myDataCallback;
51   private final File myBase;
52   private final StringBuilder mySb;
53
54   public SvnStatusHandler(final DataCallback dataCallback, File base, final Convertor<File, SVNInfo> infoGetter) {
55     myBase = base;
56     myParseStack = new ArrayList<ElementHandlerBase>();
57     myParseStack.add(new Fake());
58
59     myElementsMap = new HashMap<String, Getter<ElementHandlerBase>>();
60     fillElements();
61
62     if (dataCallback != null) {
63       myDataCallback = new DataCallback() {
64         @Override
65         public void switchPath() {
66           dataCallback.switchPath();
67           newPending(infoGetter);
68         }
69
70         @Override
71         public void switchChangeList(String newList) {
72           dataCallback.switchChangeList(newList);
73         }
74       };
75     } else {
76       myDataCallback = new DataCallback() {
77         @Override
78         public void switchPath() {
79           if (myChangelistName == null) {
80             myDefaultListStatuses.add(myPending);
81           } else {
82             myCurrentListChanges.putValue(myChangelistName, myPending);
83           }
84           newPending(infoGetter);
85         }
86
87         @Override
88         public void switchChangeList(String newList) {
89           myChangelistName = newList;
90         }
91       };
92     }
93     newPending(infoGetter);
94     mySb = new StringBuilder();
95   }
96
97   private void newPending(final Convertor<File, SVNInfo> infoGetter) {
98     final PortableStatus status = new PortableStatus();
99     myPending = status;
100     status.setInfoGetter(new Getter<SVNInfo>() {
101       @Override
102       public SVNInfo get() {
103         return infoGetter.convert(status.getFile());
104       }
105     });
106   }
107
108   public PortableStatus getPending() {
109     return myPending;
110   }
111
112   public List<PortableStatus> getDefaultListStatuses() {
113     return myDefaultListStatuses;
114   }
115
116   public MultiMap<String, PortableStatus> getCurrentListChanges() {
117     return myCurrentListChanges;
118   }
119
120   private void fillElements() {
121     myElementsMap.put("status", new Getter<ElementHandlerBase>() {
122       @Override
123       public ElementHandlerBase get() {
124         return new Status();
125       }
126     });
127     myElementsMap.put("author", new Getter<ElementHandlerBase>() {
128       @Override
129       public ElementHandlerBase get() {
130         return new Author();
131       }
132     });
133     myElementsMap.put("changelist", new Getter<ElementHandlerBase>() {
134       @Override
135       public ElementHandlerBase get() {
136         return new Changelist();
137       }
138     });
139     myElementsMap.put("commit", new Getter<ElementHandlerBase>() {
140       @Override
141       public ElementHandlerBase get() {
142         return new Commit();
143       }
144     });
145     myElementsMap.put("date", new Getter<ElementHandlerBase>() {
146       @Override
147       public ElementHandlerBase get() {
148         return new Date();
149       }
150     });
151     myElementsMap.put("entry", new Getter<ElementHandlerBase>() {
152       @Override
153       public ElementHandlerBase get() {
154         return new Entry(myBase);
155       }
156     });
157     myElementsMap.put("target", new Getter<ElementHandlerBase>() {
158       @Override
159       public ElementHandlerBase get() {
160         return new Target();
161       }
162     });
163     myElementsMap.put("wc-status", new Getter<ElementHandlerBase>() {
164       @Override
165       public ElementHandlerBase get() {
166         return new WcStatus();
167       }
168     });
169   }
170
171   @Override
172   public void endElement(String uri, String localName, String qName) throws SAXException {
173     //
174   }
175
176   @Override
177   public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
178     assertSAX(! myParseStack.isEmpty());
179     ElementHandlerBase current = myParseStack.get(myParseStack.size() - 1);
180     if (mySb.length() > 0) {
181       current.characters(mySb.toString().trim(), myPending);
182       mySb.setLength(0);
183     }
184
185     while (true) {
186       final boolean createNewChild = current.startElement(uri, localName, qName, attributes);
187       if (createNewChild) {
188         assertSAX(myElementsMap.containsKey(qName));
189         final ElementHandlerBase newChild = myElementsMap.get(qName).get();
190         newChild.updateStatus(attributes, myPending);
191         myParseStack.add(newChild);
192         return;
193       } else {
194         // go up
195         current.postEffect(myDataCallback);
196         myParseStack.remove(myParseStack.size() - 1);
197         assertSAX(! myParseStack.isEmpty());
198         current = myParseStack.get(myParseStack.size() - 1);
199       }
200     }
201   }
202
203   @Override
204   public void characters(char[] ch, int start, int length) throws SAXException {
205     assertSAX(! myParseStack.isEmpty());
206     mySb.append(ch, start, length);
207   }
208
209   @Override
210   public void endDocument() throws SAXException {
211     assertSAX(! myParseStack.isEmpty());
212     for (int i = myParseStack.size() - 1; i >= 0; -- i) {
213       ElementHandlerBase current = myParseStack.get(i);
214       current.postEffect(myDataCallback);
215     }
216     myParseStack.clear();
217   }
218
219   private static void assertSAX(final boolean shouldBeTrue) throws SAXException {
220     if (! shouldBeTrue) {
221       throw new SAXException("can not parse output");
222     }
223   }
224
225   private static class Fake extends ElementHandlerBase {
226     private Fake() {
227       super(new String[]{"status"}, new String[]{});
228     }
229
230     @Override
231     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
232     }
233
234     @Override
235     public void postEffect(DataCallback callback) {
236     }
237
238     @Override
239     public void preEffect(DataCallback callback) {
240     }
241
242     @Override
243     public void characters(String s, PortableStatus pending) {
244     }
245   }
246
247   private static class Date extends ElementHandlerBase {
248     private Date() {
249       super(new String[]{}, new String[]{});
250     }
251
252     @Override
253     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
254     }
255
256     @Override
257     public void postEffect(DataCallback callback) {
258     }
259
260     @Override
261     public void preEffect(DataCallback callback) {
262     }
263
264     @Override
265     public void characters(String s, PortableStatus pending) {
266       final SVNDate date = SVNDate.parseDate(s);
267       //if (SVNDate.NULL.equals(date)) return;
268       pending.setRemoteDate(date);
269     }
270   }
271
272   private static class Author extends ElementHandlerBase {
273     private Author() {
274       super(new String[]{}, new String[]{});
275     }
276
277     @Override
278     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
279     }
280
281     @Override
282     public void postEffect(DataCallback callback) {
283     }
284
285     @Override
286     public void preEffect(DataCallback callback) {
287     }
288
289     @Override
290     public void characters(String s, PortableStatus pending) {
291       pending.setRemoteAuthor(s);
292     }
293   }
294
295   /*        <commit
296               revision="25">
297             <author>admin</author>
298             <date>2011-11-09T12:21:02.401530Z</date>
299   */
300   private static class Commit extends ElementHandlerBase {
301     private Commit() {
302       super(new String[]{"author", "date"}, new String[]{});
303     }
304
305     @Override
306     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
307       final String revision = attributes.getValue("revision");
308       if (! StringUtil.isEmptyOrSpaces(revision)) {
309         try {
310           final long number = Long.parseLong(revision);
311           status.setRemoteRevision(SVNRevision.create(number));
312         } catch (NumberFormatException e) {
313           throw new SAXException(e);
314         }
315       }
316     }
317
318     @Override
319     public void postEffect(DataCallback callback) {
320     }
321
322     @Override
323     public void preEffect(DataCallback callback) {
324     }
325
326     @Override
327     public void characters(String s, PortableStatus pending) {
328     }
329   }
330
331   /*      <wc-status   props="none"   copied="true"   tree-conflicted="true"   item="added">
332       <wc-status       props="none"   item="unversioned">
333       <wc-status       props="none"   item="added"   revision="-1">
334       <wc-status       props="none"   item="modified"   revision="112">
335       <wc-status       props="conflicted"  item="normal"  revision="112">
336   */
337   private static class WcStatus extends ElementHandlerBase {
338     private WcStatus() {
339       super(new String[]{"commit"}, new String[]{});
340     }
341
342     @Override
343     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
344       final String props = attributes.getValue("props");
345       assertSAX(props != null);
346       final SVNStatusType propertiesStatus = StatusCallbackConvertor.convert(org.apache.subversion.javahl.types.Status.Kind.valueOf(props));
347       status.setPropertiesStatus(propertiesStatus);
348       final String item = attributes.getValue("item");
349       assertSAX(item != null);
350       final SVNStatusType contentsStatus = StatusCallbackConvertor.convert(org.apache.subversion.javahl.types.Status.Kind.valueOf(item));
351       status.setContentsStatus(contentsStatus);
352
353       if (SVNStatusType.STATUS_CONFLICTED.equals(propertiesStatus) || SVNStatusType.STATUS_CONFLICTED.equals(contentsStatus)) {
354         status.setIsConflicted(true);
355       }
356
357       // optional
358       final String copied = attributes.getValue("copied");
359       if (copied != null && Boolean.parseBoolean(copied)) {
360         status.setIsCopied(true);
361       }
362       final String treeConflicted = attributes.getValue("tree-conflicted");
363       if (treeConflicted != null && Boolean.parseBoolean(treeConflicted)) {
364         status.setIsConflicted(true);
365       }
366
367       final String revision = attributes.getValue("revision");
368       if (! StringUtil.isEmptyOrSpaces(revision)) {
369         try {
370           final long number = Long.parseLong(revision);
371           status.setRevision(SVNRevision.create(number));
372         } catch (NumberFormatException e) {
373           throw new SAXException(e);
374         }
375       }
376     }
377
378     @Override
379     public void postEffect(DataCallback callback) {
380     }
381
382     @Override
383     public void preEffect(DataCallback callback) {
384     }
385
386     @Override
387     public void characters(String s, PortableStatus pending) {
388     }
389   }
390
391   private static class Entry extends ElementHandlerBase {
392     private final File myBase;
393
394     private Entry(final File base) {
395       super(new String[]{"wc-status"}, new String[]{});
396       myBase = base;
397     }
398
399     @Override
400     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
401       final String path = attributes.getValue("path");
402       assertSAX(path != null);
403       final File file = new File(myBase, path);
404       status.setFile(file);
405       status.setKind(file.isDirectory() ? SVNNodeKind.DIR : SVNNodeKind.FILE);
406       status.setPath(path);
407     }
408
409     @Override
410     public void postEffect(DataCallback callback) {
411       callback.switchPath();
412     }
413
414     @Override
415     public void preEffect(DataCallback callback) {
416     }
417
418     @Override
419     public void characters(String s, PortableStatus pending) {
420     }
421   }
422
423   private static class Changelist extends ElementHandlerBase {
424     private String myName;
425     
426     private Changelist() {
427       super(new String[]{}, new String[]{"entry"});
428     }
429
430     @Override
431     protected void updateStatus(Attributes attributes, PortableStatus status) throws SAXException {
432       final String name = attributes.getValue("name");
433       assertSAX(! StringUtil.isEmptyOrSpaces(name));
434       myName = name;
435     }
436
437     @Override
438     public void postEffect(DataCallback callback) {
439     }
440
441     @Override
442     public void preEffect(DataCallback callback) {
443       callback.switchChangeList(myName);
444     }
445
446     @Override
447     public void characters(String s, PortableStatus pending) {
448     }
449   }
450
451   private static class Target extends ElementHandlerBase {
452     private Target() {
453       super(new String[]{}, new String[]{"entry"});
454     }
455
456     @Override
457     protected void updateStatus(Attributes attributes, PortableStatus status) {
458     }
459
460     @Override
461     public void postEffect(DataCallback callback) {
462     }
463
464     @Override
465     public void preEffect(DataCallback callback) {
466     }
467
468     @Override
469     public void characters(String s, PortableStatus pending) {
470     }
471   }
472
473   private static class Status extends ElementHandlerBase {
474     private Status() {
475       super(new String[]{"target"}, new String[]{"changelist"});
476     }
477
478     @Override
479     protected void updateStatus(Attributes attributes, PortableStatus status) {
480     }
481
482     @Override
483     public void postEffect(final DataCallback callback) {
484     }
485
486     @Override
487     public void preEffect(DataCallback callback) {
488     }
489
490     @Override
491     public void characters(String s, PortableStatus pending) {
492     }
493   }
494
495   public abstract static class ElementHandlerBase {
496     private final Set<String> myAwaitedChildren;
497     private final Set<String> myAwaitedChildrenMultiple;
498
499     ElementHandlerBase(String[] awaitedChildren, String[] awaitedChildrenMultiple) {
500       myAwaitedChildren = new HashSet<String>(Arrays.asList(awaitedChildren));
501       myAwaitedChildrenMultiple = new HashSet<String>(Arrays.asList(awaitedChildrenMultiple));
502     }
503
504     protected abstract void updateStatus(Attributes attributes, PortableStatus status) throws SAXException;
505     public abstract void postEffect(final DataCallback callback);
506     public abstract void preEffect(final DataCallback callback);
507
508     public boolean startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
509       if (myAwaitedChildrenMultiple.contains(qName)) {
510         return true;
511       }
512       return myAwaitedChildren.remove(qName);
513     }
514
515     public abstract void characters(String s, PortableStatus pending);
516   }
517
518   public interface DataCallback {
519     void switchPath();
520     void switchChangeList(final String newList);
521   }
522 }