JTree mittels SwingWorker füllen

Hallo,

ich habe ein JFrame „MainView“. Darauf sind u.a. 2 JTrees und eine JList.

Die beiden JTrees sollen über einen SwingWorker gefüllt werden. Die Daten dafür stammen aus einer HSQLDB Datenbank. Beide werden mit den selben Daten gefüllt. Nur unterschiedlich sortiert.

Ich erhalte beim Laden sporadische Fehlermeldungen. Nullpointer Exception, IndexOutOfBounds Exception.

Hier die Methode, um die beiden JTrees zu füllen:

private class LoadingWorker extends SwingWorker<Integer, Integer> {
	
    protected Integer doInBackground() throws Exception {
	
	  		ArrayList<ParameterList> parameterlist1 = new ArrayList<ParameterList>();
	  		parameterlist1.add(new ParameterList("Integer", Util.getInstance().getCurrentDatapoolId()));
	  		
	  		int countthemeid = -1;
			ResultSet rscountthemeid = dbmanager.executePQuery("SELECT COUNT (DISTINCT themeid)" +
				" FROM content WHERE datapoolid = ?", parameterlist1);	
				
			if(rscountthemeid != null){
			  		try {
						for (;rscountthemeid.next();){	 

							countthemeid =  rscountthemeid.getInt(1);
							
						}
					} catch (SQLException e) {
						e.printStackTrace();
					}
  	  			}
			
  		  		ArrayList<ParameterList> parameterlist2 = new ArrayList<ParameterList>();
  		  		parameterlist2.add(new ParameterList("Integer", Util.getInstance().getCurrentDatapoolId()));
  		  		
  		  		int countdate = -1;
  				ResultSet rscountdate = dbmanager.executePQuery("SELECT COUNT (DISTINCT date)" +
						" FROM content WHERE datapoolid = ?", parameterlist2);	
						
					if(rscountdate != null){
	  			  		try {
							for (;rscountdate.next();){	 
							
								countdate =  rscountdate.getInt(1);
								
							}
						} catch (SQLException e) {
							e.printStackTrace();
						}
		  	  		}
				  	
				  	mainview.setStatusProgressBarMaximum(countthemeid);
					mainview.setStatusText("Lade thematische Liste: " + countthemeid);
		
	 		int datapoolid = Util.getInstance().getCurrentDatapoolId();
	 		int countTopTopics = 0;
	 		
			try{
				
		  		TopTreeModel topmodel = null;
		  		int themeid = -1;

		  		ArrayList<ParameterList> parameterlist3 = new ArrayList<ParameterList>();
		  		parameterlist3.add(new ParameterList("Integer", datapoolid));
		  			 		
		  		ResultSet rsthemeid = dbmanager.executePQuery("SELECT themeid, theme FROM theme " +
		  			  		"WHERE datapoolid = ? ORDER BY theme ASC", parameterlist3);

		  		if(rsthemeid != null){
		  			for (;rsthemeid.next();){
		  				
		  				themeid = rsthemeid.getInt(1);	
		  		  		
		  		  		ArrayList<ParameterList> parameterlist4 = new ArrayList<ParameterList>();
		  		  		parameterlist4.add(new ParameterList("Integer", themeid));

		  				ResultSet rstheme = dbmanager.executePQuery("SELECT theme" +
								" FROM theme" +
								" WHERE themeid = ?" +
								" ORDER BY theme.theme ASC", parameterlist4);	
		  				
		  				int cntoptopicid = -1;
		  		  		String cntoptopic = null;
		  		  		String cntopsubtopic = null;
		  			
		  		  		String toplast = null;

		  		 		topmodel = toptree.getModel();
		  		 		
		  		  		TreePath root = new TreePath(topmodel.getRoot());
		  		  		toptree.setSelectionPath(root); 
		  		    
		  		  		if(rstheme != null){
		  			  		for (;rstheme.next();){
		  			  			
		  			  			cntoptopicid = themeid;	
		  			  			cntoptopic = rstheme.getString(1);	
		  			
		  		  		  		ArrayList<ParameterList> parameterlist5 = new ArrayList<ParameterList>();
		  		  		  		parameterlist5.add(new ParameterList("Integer", cntoptopicid));
		  			  			
		  		  				ResultSet rssubtopic = dbmanager.executePQuery("SELECT content.subtheme" +
		  								" FROM content" +
		  								" WHERE content.themeid = ?" +
		  								" ORDER BY content.subtheme", parameterlist5);	
		  		  		  		
		  		  	  		if(rssubtopic != null){
			  			  		for (;rssubtopic.next();){
			  			  			
			  			  			cntopsubtopic =  rssubtopic.getString(1);
			  						    			
			  			  			if(cntoptopic.equals(toplast)){
			  			  				TreePath parent = toptree.getSelectionPath().getParentPath();
			  						
			  			  				toptree.setSelectionPath(parent);
			  			  				new AddActionTopTree(toptree, topmodel, cntopsubtopic);
			  			  				
			  			  			}
			  			  			else{
			  			  				toptree.setSelectionPath(root);
			  			  				
			  			  				countTopTopics++;
			  			  				
			  			  				publish(countTopTopics);
			  			  				
			  			  				
			  			  				new AddActionTopTree(toptree, topmodel, cntoptopic);
			  			  				new AddActionTopTree(toptree, topmodel, cntopsubtopic);
			  			  			}
			  			  			toplast = cntoptopic;
			  				
			  			  		}
		  		  	  		}
		  			  			

		  			  		}
		  		  		}
		  			}
		  		}
		  	}catch(Exception e){
		  		e.printStackTrace();
		  	}
			
			mainview.setStatusProgressBarMaximum(countdate);
			
			mainview.setStatusText("Lade chronologische Liste: " + countdate);
			mainview.setStatusProgressBarValue(0);
			
	  		try{
	  			
	  			ArrayList<ParameterList> parameterlist6 = new ArrayList<ParameterList>();
		  		parameterlist6.add(new ParameterList("Integer", datapoolid));
	  			
		  		ResultSet rschron = dbmanager.executePQuery("SELECT content.date, content.subtheme " +
							"FROM content, datapool WHERE content.datapoolid = datapool.datapoolid AND " +
							"datapool.datapoolid = ? " +
							"ORDER BY content.date, content.subtheme ASC", parameterlist6);

		  		Date cnchrondate = null;
		  		String cnchronsubtopic = null;
		  		
		  		String datelast = null;

		  		ChronTreeModel chronmodel = chrontree.getModel();

		  		TreePath rootChron = new TreePath(chronmodel.getRoot());
		  					
		  		chrontree.setSelectionPath(rootChron);
		  		
				int countdates = 0;

		  		if(rschron != null){
			  		for (;rschron.next();){
			  			
			  			cnchrondate = rschron.getDate(1);
			  			cnchronsubtopic = rschron.getString(2);
				    	
			  			if(cnchrondate.toString().equals(datelast)){
			  				
			  				TreePath parent = chrontree.getSelectionPath().getParentPath();
				
			  				chrontree.setSelectionPath(parent);
			  				new AddActionChronTree(chrontree, chronmodel, cnchronsubtopic);
			  				
			  			}
			  			else{
			  				
			  				chrontree.setSelectionPath(rootChron);
			  				new AddActionChronTree(chrontree, chronmodel, cnchrondate.toString());
			  				new AddActionChronTree(chrontree, chronmodel, cnchronsubtopic);
			  				
			  			}
			  			
			  			datelast = cnchrondate.toString();
			  			
			  			countdates++;
			  			
			  			
			  			publish(countdates);
			  			
			  		}
		  		}
		  	}catch(Exception e){
		  		e.printStackTrace();
		  	
		  	}

			for(int a=0;a<toptree.getRowCount();a++){
	  			toptree.collapseRow(a);
	  			
	  		}
			
			for(int a=0;a<chrontree.getRowCount();a++){
	  			chrontree.collapseRow(a);
	  			
	  		}

	  		TopTreeListener toptreelistener = new TopTreeListener(mainview);
	  		toptree.getSelectionModel().addTreeSelectionListener(toptreelistener);
	  	
	  		ChronTreeListener chrontreelistener = new ChronTreeListener(mainview);	
	  		chrontree.getSelectionModel().addTreeSelectionListener(chrontreelistener);
			     	
  		return 0;
    	
    	
    }

Ich glaube, dass es mit der JList zu tun hat. Deshalb hier ein TreeSelectionListener, der u.a. die JList füllt:

	import java.io.File;
	import java.sql.ResultSet;
	import java.sql.SQLException;
	import java.util.ArrayList;
	import java.util.Date;

	import javax.swing.event.TreeSelectionEvent;
	import javax.swing.event.TreeSelectionListener;
	import javax.swing.tree.TreePath;

	import view.MainView;

	public class TopTreeListener implements TreeSelectionListener{
	
	private MainView mainview;
	private DBManager dbmanager;
	
	public TopTreeListener(MainView amainview){
		this.mainview = amainview;
		
	  	try{
	  		dbmanager = DBManager.getInstance();
	  		dbmanager.connect(Util.getInstance().getAppName());
	  		
	  	}catch(Exception e){  		
	  		e.printStackTrace();
	  		
	  	}
	}
	
	public void valueChanged(TreeSelectionEvent e)
    {	
		int datapoolid = -1;
		datapoolid = Util.getInstance().getCurrentDatapoolId();
		
        TreePath selectionpath = e.getNewLeadSelectionPath();;
	
        if(selectionpath != null){
        	
        	Object[] elements = selectionpath.getPath();
        	int elementscount = selectionpath.getPathCount();
		
        	if(elementscount == 3){
					
        		String elementtopic = elements[1].toString();
        		String elementsubtopic = elements[2].toString();
			
        		int cncontentid = -1;
        		String cntopic = "";
        		Date cndate = null;
        		String cnsubtopic = "";
        		String cncontent = "";
			
        		try{
				
        			dbmanager = DBManager.getInstance();
			  	
	    	 		ArrayList<ParameterList> parameterlist1 = new ArrayList<ParameterList>();
    				parameterlist1.add(new ParameterList("String", elementtopic));
       				parameterlist1.add(new ParameterList("String", elementsubtopic));				
        			
        			ResultSet rsdetail = dbmanager.executePQuery("SELECT theme.theme, content.contentid, content.subtheme, content.date, content.content FROM theme, content " +
        					"WHERE content.themeId = theme.themeId AND " +
        					"theme.theme = ? AND " +
        					"content.subtheme = ?", parameterlist1);

        			if(rsdetail != null){
        				for (;rsdetail.next();){
        					
            				cntopic = rsdetail.getString(1);
            				cncontentid = rsdetail.getInt(2);
            				cnsubtopic = rsdetail.getString(3);
            				cndate = rsdetail.getDate(4);
            				cncontent = rsdetail.getString(5);
    				
            				String olddate = cndate.toString();
            				
            		    	String[] values = new String[3];
            		    	values = olddate.split("-");
            		    	String date = "";
            		    	
            		    	if(values != null){
            		    		date = values[2] + "." + values[1] + "." + values[0];
            		    		
            		    	}
            				
            				mainview.setTopic(cntopic);
            				mainview.setSubTopic(cnsubtopic);
            				mainview.setDate(date);

            				mainview.setContent(cncontent);
            				mainview.setCaretPosition();
    				
    				
            			}
        			}
        		
        			if(!mainview.getTopic().equals("") && !mainview.getSubTopic().equals("") && !mainview.getDate().equals("")){
        				
        	  		String cnurl = "";
        			
	    	 		ArrayList<ParameterList> parameterlist2 = new ArrayList<ParameterList>();
    				parameterlist2.add(new ParameterList("Integer", cncontentid));			
        			
        			ResultSet rsattachments = dbmanager.executePQuery("SELECT url FROM attachments " +
        					"WHERE contentid = ?", parameterlist2);

        			mainview.clearAttachments();
        			
        			if(rsattachments != null){
        	   			for (;rsattachments.next();){
            				
            				cnurl = rsattachments.getString(1);
            				
            				File[] attachment = new File[2];
            				
            				File document = new File(cnurl);
            				
            				String topic = mainview.getTopic();
            				String subtopic = mainview.getSubTopic();
            				
                			String filename = document.getName();
        	    			int point = filename.lastIndexOf(".");
        	    			String thumbnailname = filename.substring(0, point);
        	    			
           	    			File thumbnail =  new File("attachments"+File.separator+
           	    					datapoolid+File.separator+
           	    					"thumbnails"+File.separator+
           	    					topic+File.separator+
           	    					subtopic+File.separator+
           	    					thumbnailname+".png");
            				
           	    			attachment[0] = document;
            				attachment[1] = thumbnail;
            				
            				mainview.addAttachment(attachment);
            				
            				
            			}
        			}
        		}
        	}catch(SQLException sqle){
        		sqle.printStackTrace();
        	}
        }
      }
   }
}

Wie gesagt, es kommen sporadische Fehler. Meistens, wenn Eintäge in der JList vorhanden sind.

Hier eine Exception die u.a. auftritt:

Exception in thread AWT-EventQueue-0 java.lang.ArrayIndexOutOfBoundsException: 3 >= 3
at java.base/java.util.Vector.elementAt(Vector.java:466)
at java.desktop/javax.swing.DefaultListModel.getElementAt(DefaultListModel.java:95)
at java.desktop/javax.swing.plaf.basic.BasicListUI.updateLayoutState(BasicListUI.java:1443)
at java.desktop/javax.swing.plaf.basic.BasicListUI.maybeUpdateLayoutState(BasicListUI.java:1394)
at java.desktop/javax.swing.plaf.basic.BasicListUI.paintImpl(BasicListUI.java:330)
at java.desktop/javax.swing.plaf.basic.BasicListUI.paint(BasicListUI.java:306)
at java.desktop/javax.swing.plaf.synth.SynthListUI.update(SynthListUI.java:90)
at java.desktop/javax.swing.JComponent.paintComponent(JComponent.java:852)
at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128)
at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:961)
at java.desktop/javax.swing.JComponent.paint(JComponent.java:1137)
at java.desktop/javax.swing.JViewport.paint(JViewport.java:736)
at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5318)
at java.desktop/javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:246)
at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1336)
at java.desktop/javax.swing.JComponent._paintImmediately(JComponent.java:5266)
at java.desktop/javax.swing.JComponent.paintImmediately(JComponent.java:5076)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:878)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:861)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:861)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:834)
at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:784)
at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1897)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90

Bitte zur Tat schreiten… Der Augenkrebs naht. :rofl:

@schmitt19 Sieht so als, als würden die CRUD-Operationen bei dir nicht sauber programmiert sein. update everything vergessen?

Die Formatierung ist wirklich (ein bißchen kaputt, aber) sehr kreativ: Auf die Idee, einen stack trace in eine Tabelle zu packen, wäre ich nicht gekommen.

Ich habe den Code nicht gelesen und nicht ausprobiert. Ist auch anscheinend nicht compilierbar, und selbst wenn es compilierbar wäre, könnte man es ohne die Datenbank nicht testen.

Aber … man braucht bei der Symptombeschreibung nicht den Code im Detail zu lesen (und man braucht auch nicht die >20 Jahre Swing-Erfahrung, die ich habe) um zu wissen, was die Ursache des Fehlers ist:

Bei „manchmal“ und „sporadisch“ und verschiedenen Exceptions an verschiedenen Stellen kann man recht schnell sagen: „Ja, wird wohl 'ne race condition sein“.

Im speziellen: Swing hat eine „Single Thread Rule“. Leider ist die in den aktuellen Dokumentationen nicht mehr so deutlich hervorgehoben, wie früher mal. Die Regel ist:

Once a Swing component has been realized, all code that might affect or depend on the state of that component should be executed in the event-dispatching thread.

Man könnte da lange über Gründe und Details und Lösungen reden. Und selbst erfahrene Swing-Programmierer machen da manchmal Fehler (es ist leicht, da Fehler zu machen).

Auf dein Beispiel bezogen:

Du darfst innerhalb der doInBackground-Methode keine Swing components verändern!

Die doInBackground-Methode wird ja von einem Hintergrundthread ausgeführt. Jetzt weiß man halt nicht, was new AddActionChronTree(...) macht (es sieht aus, als wäre das ein Konstruktor, der den übergebenen Tree verändert :no_mouth: ). Wenn in dieser Methode sowas wie chrontree.setSelectionPath(rootChron); oder toptree.collapseRow(a); oder sogar eine strukturelle Veränderung des Trees (d.h. das Einfügen von Knoten) vorgenommen wird, dann kann das an beliebigen Stellen zu beliebigen Fehlern führen.

Wie man das löst?

Naja, dazu müßte man mehr code kennen. Aber das allgemeine Muster ist, dass man die Veränderungen, die man an Swing Components machen will, dann wieder auf den Event-Dispatch-Thread legt. Sinngemäß:

protected Integer doInBackground() throws Exception {
    // Datenbankabfrage...
    Object result = database.query();

    // Konten für das Ergebnis erstellen
    DefaultMutableTreeNode newNode = 
        new DefaultMutableTreeNode("Database result: "+result);

    // Knoten in das TreeModel (d.h. den JTree) einfügen:

    // NEIN!
    // Das würde den Tree verändern, wird aber NICHT auf dem 
    // Event-Dispatch-Thread ausgeführt!
    //treeModel.insertNodeInto(newNode, treeModel.getRoot(), 0);

    // Stattdessen:
    // Führe das Einfügen des Knotens auf dem Event-Dispatch-Thread aus:
    SwingUtilities.invokeLater(new Runnable() {
        @Override  
        public void run() {
            // Hier darf man das machen:
            treeModel.insertNodeInto(newNode, treeModel.getRoot(), 0);
        }
     });
}

Ähnliche Fragestellungen ergeben sich bei irgendwelchen Listenern, wobei z.B. so ein TreeSelectionListener natürlich auf dem EvenDispatchThread ausgeführt wird WENN (und NUR wenn) die eigentliche Veränderung (die den Event verursacht hat) auf dem EventDispatchThread ausgeführt wurde. Dass man darin dann wiederum keine Datenbankabfrage machen sollte, ist ein anderes Thema.

Wenn einen Freelancer suchst, der das ganze für 100€/Stunde glattzieht, schreib’ mir eine Nachricht.

Danke.

Ich möchte noch zwei Punkte einbringen.

a) Die Klasse kann ohne Datenbankanbindung getestet werden, wenn man die Datenbankbindung/den Service mockt (vorausgesetzt, der Rest funktioniert auch…).
b) Vermutlich wäre es für die weitere Entwicklung sinnvoll, die Datenbankanfragen „zu buffern“, also nicht direkt im EDT stattfinden zu lassen.