# SAXBuilder und SwingWorker



## Daniel_L (14. Jul 2008)

Hallo,

ich wollte größere XML-Dateien (zw. 1 und 4MB) gerne mittels SAXBuilder (JDom) einlesen, allerdings in einem background-Thread (SwingWorker). Meine Frage ist, ob ich den Status der bereits eingelesenen Daten durch den SAXBuilder irgendwie abfragen kann, sodass ich eine ProgressBar animieren kann, die in etwa angibt, wann die Datei vollständig eingelesen ist? Oder muss ich die XML-Datei dazu anders einlesen, um mit einer ProgressBar den Fortschritt des Ladens der Datei anzeigen zu können?

Gruß
Daniel


----------



## Niki (14. Jul 2008)

Das könnte dir helfen:

```
public static void main(String[] args) throws IOException, JDOMException {
		File f = new File(args[0]);
		final long l = f.length();
		SAXBuilder builder = new SAXBuilder();
		ProgressInputStream pis = new ProgressInputStream(
				new FileInputStream(f));
		pis.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				String name = evt.getPropertyName();
				if (ProgressInputStream.PROPERTY_PROGRESS.equals(name)) {
					long newVal = (Long) evt.getNewValue();
					System.out.println(newVal + " bytes von " + l + " gelesen");
				}
			}
		});
		builder.build(pis);
	}
```


```
public class ProgressInputStream extends InputStream {

		public static final String PROPERTY_PROGRESS = "progressInputStreamProperty";

		private PropertyChangeSupport pcs = null;

		private InputStream is = null;

		private long read = 0;

		public ProgressInputStream(InputStream is) {
			this.is = is;
			this.pcs = new PropertyChangeSupport(this);
		}

		public void addPropertyChangeListener(PropertyChangeListener listener) {
			pcs.addPropertyChangeListener(listener);
		}

		public void removePropertyChangeListener(PropertyChangeListener listener) {
			pcs.removePropertyChangeListener(listener);
		}

		@Override
		public int available() throws IOException {
			return is.available();
		}

		@Override
		public synchronized void mark(int readlimit) {
			is.mark(readlimit);
		}

		@Override
		public boolean markSupported() {
			return is.markSupported();
		}

		@Override
		public synchronized void reset() throws IOException {
			is.reset();
		}

		@Override
		public long skip(long n) throws IOException {
			return is.skip(n);
		}

		@Override
		public int read() throws IOException {
			progress(1);
			return is.read();
		}

		@Override
		public int read(byte[] b) throws IOException {
			int l = is.read(b);
			progress(l);
			return l;
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			int l = is.read(b, off, len);
			progress(l);
			return l;
		}

		private void progress(int l) {
			if (l == -1)
				return;
			long old = read;
			read += l;
			pcs.firePropertyChange(ProgressInputStream.PROPERTY_PROGRESS, old,
					read);
		}

		@Override
		public void close() throws IOException {
			is.close();
		}
	}
```


----------



## Daniel_L (14. Jul 2008)

Super, das versuche ich mal. Vielen Dank!


----------



## Daniel_L (14. Jul 2008)

Im Konstruktor der Klasse  ProgressInputStream ist ein InputStream, aber in deinem ersten Source-Beispiel übergibst du ein FileInputStream. Was von beiden muss ich nun verwenden? Weil so gibt er mir eine Fehlermeldung... ich müsste dann auch die Variabel "is" aus der PIS-Klasse in einen FileInputStream-Typ umwandeln...


----------



## Daniel_L (14. Jul 2008)

Kurz noch der Source: Ich habe die ProgressInputStream als eigene Klasse erstellt (Name: CProgressInputStream) und verwende die folgendermaßen:



```
@Override protected Object doInBackground() throws IOException, JDOMException {
            // Your Task's code here.  This method runs
            // on a background thread, so don't reference
            // the Swing GUI from here.
            // prevent task from processing when the file path is incorrect
            
            // get the file path from the data file which has to be opened
            File fp = dataObj.getFilePath();
            
            if( null == fp || !fp.exists() ) {
                errorMessage = ERR_LOAD;
            }
            
            final long l = fp.length(); 
            SAXBuilder builder = new SAXBuilder();
            
            pis = new CProgressInputStream( new FileInputStream(fp) );
            pis.addPropertyChangeListener(new PropertyChangeListener() { 
                public void propertyChange(PropertyChangeEvent evt) { 
                    String name = evt.getPropertyName(); 
                    if (CProgressInputStream.PROPERTY_PROGRESS.equals(name)) { 
                        long newVal = (Long) evt.getNewValue(); 
                        System.out.println(newVal + " bytes von " + l + " gelesen"); 
                    } 
                }
            });
            // open the file via saxbuilder input parser
            // throws JDOMException
            try {
                builder.build(pis);                 
            } catch (IOException ex) {
                Logger.getLogger(CDaten.class.getName()).log(Level.SEVERE, null, ex);
            }
            
            return null;  // return your result
        }
```

Ganz zu Beginn der Klassen, in der dieser Thread läuft, habe ich als globale Variable (Feld) dann pis definiert:
    CProgressInputStream pis;

Gruß
Daniel


----------



## Siassei (14. Jul 2008)

Daniel_L hat gesagt.:
			
		

> Im Konstruktor der Klasse  ProgressInputStream ist ein InputStream, aber in deinem ersten Source-Beispiel übergibst du ein FileInputStream. Was von beiden muss ich nun verwenden? Weil so gibt er mir eine Fehlermeldung... ich müsste dann auch die Variabel "is" aus der PIS-Klasse in einen FileInputStream-Typ umwandeln...


Fehlermeldung? FileInputStream ist eine Implementation der abstrakten Klasse InputStream :###


----------



## Daniel_L (14. Jul 2008)

Das NetBeans Log zeigt mir beim Compilieren:

/Users/daniel/Documents/Coding/JavaProjects/Zettelkasten/src/zettelkasten/CLoadSave.java:254: cannot find symbol
symbol  : constructor CProgressInputStream(java.io.FileInputStream)
location: class zettelkasten.CProgressInputStream
            pis = new CProgressInputStream( new FileInputStream(fp) );
/Users/daniel/Documents/Coding/JavaProjects/Zettelkasten/src/zettelkasten/CLoadSave.java:256: cannot find symbol
symbol: class PropertyChangeEvent
                public void propertyChange(PropertyChangeEvent evt) { 
2 errors

NetBeans bietet mir dann an, einen Constructor zu erstellen, der einen FileInputStream-Typ als Parameter hat...


----------



## Niki (14. Jul 2008)

Zeig mal deine ganze Klasse her. Bei mir funktioniert der Code


----------



## Daniel_L (14. Jul 2008)

Die separate Klasse CProgressInputStream:


```
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package zettelkasten;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.InputStream;

public class CProgressInputStream extends InputStream {

      public static final String PROPERTY_PROGRESS = "progressInputStreamProperty"; 
      private PropertyChangeSupport pcs = null; 
      private InputStream is = null; 
      private long read = 0;

      
      public void ProgressInputStream(InputStream is) { 
         this.is = is; 
         this.pcs = new PropertyChangeSupport(this); 
      } 

      public void addPropertyChangeListener(PropertyChangeListener listener) { 
         pcs.addPropertyChangeListener(listener); 
      } 

      public void removePropertyChangeListener(PropertyChangeListener listener) { 
         pcs.removePropertyChangeListener(listener); 
      } 

      @Override 
      public int available() throws IOException { 
         return is.available(); 
      } 

      @Override 
      public synchronized void mark(int readlimit) { 
         is.mark(readlimit); 
      } 

      @Override 
      public boolean markSupported() { 
         return is.markSupported(); 
      } 

      @Override 
      public synchronized void reset() throws IOException { 
         is.reset(); 
      } 

      @Override 
      public long skip(long n) throws IOException { 
         return is.skip(n); 
      } 

      @Override 
      public int read() throws IOException { 
         progress(1); 
         return is.read(); 
      } 

      @Override 
      public int read(byte[] b) throws IOException { 
         int l = is.read(b); 
         progress(l); 
         return l; 
      } 

      @Override 
      public int read(byte[] b, int off, int len) throws IOException { 
         int l = is.read(b, off, len); 
         progress(l); 
         return l; 
      } 

      private void progress(int l) { 
         if (l == -1) 
            return; 
         long old = read; 
         read += l; 
         pcs.firePropertyChange(CProgressInputStream.PROPERTY_PROGRESS, old, read); 
      } 

      @Override 
      public void close() throws IOException { 
         is.close(); 
      } 
}
```


Und der JDialog, der laden/speichern soll, mit dem Background-Thread:


```
/*
 * CLoadSave.java
 *
 * Created on 14. Juli 2008, 19:14
 */

package zettelkasten;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import javax.swing.Icon;
import javax.swing.Timer;
import org.jdesktop.application.Action;
import org.jdesktop.application.Task;
import org.jdesktop.application.TaskMonitor;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;


public class CLoadSave extends javax.swing.JDialog {

    /**
     * Global variables
     */
    
    //  of the CDaten object, which contains the XML data of the Zettelkasten
    CDaten dataObj;
    CProgressInputStream pis;
    // get the strings for file descriptions from the resource map
    org.jdesktop.application.ResourceMap resourceMap = org.jdesktop.application.Application.getInstance(zettelkasten.ZettelkastenApp.class).getContext().getResourceMap(ZettelkastenView.class);
    Document zknFile; // stores the main data    
    int errorMessage; // in case error occured during important operations (loading/
                      // saving files ec.), this variable stores the error message which
                      // can be received via the "getErrorMessage()" method

    /**
     * Constants for error messages, received by getErrorMessage();
     */
    int ERR_LOAD = 1;
    int ERR_SAVE = 2;

    private Timer messageTimer;
    private Timer busyIconTimer;
    private Icon idleIcon;
    private Icon[] busyIcons = new Icon[15];
    private int busyIconIndex = 0;
       
    
    /**
     * Creates new form CLoadSave
     * 
     * Parameters:
     * @parent the parent window
     * @modal whether the dialog is modal or not
     * @d CDaten class object, which manages the data storage
     * @title the dialog's title, e.g. either "Open File" or "Save File"
     */
    public CLoadSave(java.awt.Frame parent, boolean modal, CDaten d, String title ) {
        super(parent, modal);
        dataObj = d;        
        initComponents();
        setTitle( title );
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */

     ((hier kommt der automatisch generierteNetBeans-Matisse-Code...))


    /**
     * Initiates the status bar for background tasks.
     * Catches messages from the doInBackground task
     * and changes the progressbar state, the busy icon animation
     * and - if necessary - the status message.
     */

    private void initStatusBar() {
        int messageTimeout = resourceMap.getInteger("StatusBar.messageTimeout");
        messageTimer = new Timer(messageTimeout, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                statusMessageLabel.setText("");
            }
        });
        messageTimer.setRepeats(false);
        int busyAnimationRate = resourceMap.getInteger("StatusBar.busyAnimationRate");
        for (int i = 0; i < busyIcons.length; i++) {
            busyIcons[i] = resourceMap.getIcon("StatusBar.busyIcons[" + i + "]");
        }
        busyIconTimer = new Timer(busyAnimationRate, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                busyIconIndex = (busyIconIndex + 1) % busyIcons.length;
                statusAnimationLabel.setIcon(busyIcons[busyIconIndex]);
            }
        });
        idleIcon = resourceMap.getIcon("StatusBar.idleIcon");
        statusAnimationLabel.setIcon(idleIcon);
        progressBar.setVisible(false);

        // connecting action tasks to status bar via TaskMonitor
        TaskMonitor taskMonitor = new TaskMonitor(org.jdesktop.application.Application.getInstance(zettelkasten.ZettelkastenApp.class).getContext());
        taskMonitor.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
            @Override
            public void propertyChange(java.beans.PropertyChangeEvent evt) {
                String propertyName = evt.getPropertyName();
                if ("started".equals(propertyName)) {
                    if (!busyIconTimer.isRunning()) {
                        statusAnimationLabel.setIcon(busyIcons[0]);
                        busyIconIndex = 0;
                        busyIconTimer.start();
                    }
                    progressBar.setVisible(true);
                    progressBar.setIndeterminate(true);
                } else if ("done".equals(propertyName)) {
                    busyIconTimer.stop();
                    statusAnimationLabel.setIcon(idleIcon);
                    progressBar.setVisible(false);
                    progressBar.setValue(0);
                } else if ("message".equals(propertyName)) {
                    String text = (String)(evt.getNewValue());
                    statusMessageLabel.setText((text == null) ? "" : text);
                    messageTimer.restart();
                } else if ("progress".equals(propertyName)) {
                    int value = (Integer)(evt.getNewValue());
                    progressBar.setVisible(true);
                    progressBar.setIndeterminate(false);
                    progressBar.setValue(value);
                }
            }
        });        
    }

    
    /**
     * Method which returns the error message as a string,
     * if an error occured during certain operations like
     * loading or saveing files etc...
     * @return
     */
    public String getErrorMessage() {
        String msg = new String("");
        
        switch( errorMessage ) {
            // loading error
            case 1:  msg = resourceMap.getString("ERR_MSG_LOAD");
                     break;
            // saving error
            case 2:  msg = resourceMap.getString("ERR_MSG_SAVE");
                     break;
            default: msg = "abc";
                     break;
        }
        
        return msg;
    }

    
    /**
     * Action with background task, which imorts the file
     * @return
     */
    @Action
    public Task loadFile() {
        // initiate the "statusbar" (the loading splash screen), giving visiual
        // feedback during open and save operations
        initStatusBar();
        return new LoadFileTask(org.jdesktop.application.Application.getInstance(zettelkasten.ZettelkastenApp.class));
    }

    private class LoadFileTask extends org.jdesktop.application.Task<Object, Void> {
        LoadFileTask(org.jdesktop.application.Application app) {
            // Runs on the EDT.  Copy GUI state that
            // doInBackground() depends on from parameters
            // to ImportFileTask fields, here.
            super(app);
        }
        @Override protected Object doInBackground() throws IOException, JDOMException {
            // Your Task's code here.  This method runs
            // on a background thread, so don't reference
            // the Swing GUI from here.
            // prevent task from processing when the file path is incorrect
            
            // get the file path from the data file which has to be opened
            File fp = dataObj.getFilePath();
            
            if( null == fp || !fp.exists() ) {
                errorMessage = ERR_LOAD;
            }
            
            final long l = fp.length(); 
            SAXBuilder builder = new SAXBuilder();
            
            pis = new CProgressInputStream( new FileInputStream(fp) );
            pis.addPropertyChangeListener(new PropertyChangeListener() { 
                public void propertyChange(PropertyChangeEvent evt) { 
                    String name = evt.getPropertyName(); 
                    if (CProgressInputStream.PROPERTY_PROGRESS.equals(name)) { 
                        long newVal = (Long) evt.getNewValue(); 
                        System.out.println(newVal + " bytes von " + l + " gelesen"); 
                    } 
                }
            });
            // open the file via saxbuilder input parser
            // throws JDOMException
            try {
                builder.build(pis);                 
            } catch (IOException ex) {
                Logger.getLogger(CDaten.class.getName()).log(Level.SEVERE, null, ex);
            }
            
            return null;  // return your result
        }
        @Override protected void succeeded(Object result) {
            // Runs on the EDT.  Update the GUI based on
            // the result computed by doInBackground().
        }
    }
    
    
    // Variables declaration - do not modify                     
    private javax.swing.JProgressBar progressBar;
    private javax.swing.JLabel statusAnimationLabel;
    private javax.swing.JLabel statusMessageLabel;
    // End of variables declaration                   

}
```


----------



## Daniel_L (14. Jul 2008)

ach ja, ich arbeite mit NetBeans 6.1 unter Mac OS X 10.5.4, Java 1.6_05


----------



## Daniel_L (14. Jul 2008)

im zweiten Source Zeile 205 ist der Fehler


----------



## Siassei (14. Jul 2008)

Ähm  :bahnhof: der Code ist korrekt bei Zeile 205 ???:L Importiere mal den java.io.InputStream in deinem zweiten Codestück. Vielleicht benötigt Java dies beim impliziten Typecasting.

Oder du machst es gleich explizit in der Form von

```
pis = new CProgressInputStream((InputStream) (new FileInputStream(fp)) );
```
Dies ist jedoch nicht nötig. Aber es erklärt, warum du die Klasse InputStream importieren musst.


----------



## Daniel_L (15. Jul 2008)

Ok, hab's. Hier der korrigierte Code, musste ein @Override einsetzen.


```
final long l = fp.length(); 
            SAXBuilder builder = new SAXBuilder();
            
            pis = new CProgressInputStream( new FileInputStream(fp) );
            pis.addPropertyChangeListener(new PropertyChangeListener() { 
                @Override
                public void propertyChange(PropertyChangeEvent evt) { 
                    String name = evt.getPropertyName(); 
                    if (CProgressInputStream.PROPERTY_PROGRESS.equals(name)) { 
                        long newVal = (Long) evt.getNewValue(); 
                        System.out.println(newVal + " bytes von " + l + " gelesen"); 
                    } 
                }
            });
```

Der Fehler lag aber tatsächlich in der Klasse CProgressInputStream, nämlich im Konstruktor: Zum einen hieß dieser immer noch "ProgressInputStream", zum anderen musste das "public void" davor weg:


```
CProgressInputStream(InputStream is) { 
         this.is = is; 
         this.pcs = new PropertyChangeSupport(this); 
      }
```


Ich muss mich erstmal von meiner alte Windows-95-C++-IDE verabschieden. Dass es sowas wie Refactoring gibt, um sicher Klassennamen etc. umzubenennen, daran muss ich mich erst gewöhnen... ;-)


Vielen Dank für die Hilfe!


----------



## Daniel_L (15. Jul 2008)

Eine Frage habe ich noch: in der ProgressInputStream-Klasse sind drei read-Methoden. Woher weiß ich, wann welche zum Einsatz kommt? Wann liest er immer nur einzelne Bytes, wann mehrere mit einer Länge von "offset" ein?


----------



## Niki (15. Jul 2008)

Genau kann ich das nicht sagen, das hängt von der Klasse ab, die den InputStream verwendet (also in diesem Fall der SAXBuilder). Du kannst ja log-Statements einbauen, dann siehst du wann welche Methode aufgerufen wird. Bei einem 7k File hat er ca 30 mal die normale read() Methode aufgerufen, danach die read(byte[], int, int) Methode. Wahrscheinlich ist da eine Art Optimierung implementiert.


----------



## Daniel_L (15. Jul 2008)

Gut, wenn da eine Art Optimierung bereits drin ist, reicht mir das vollkommen aus.

Sähe ein "OutputStream" äquivalent aus, nur mit write- statt read-Befehlen?


----------



## Niki (15. Jul 2008)

Ja, es ist ja nichts anderes als ein Wrapper um einen bestehenden OutputStream. Nur dass du vor den tatsächlichen Befehlen halt noch etwas zusätzliches machst. Du musst halt alle Befehle zum eigentlichen OutputStream durchreichen.


----------

