Ruben Laguna’s blog

JTable Bound to a Database With Lazy Loading

I’ve been doing experiments with JTables bound to database tables in the past. But I was not satisfied with the result to be honest. The pagination helps but there is still noticeable pauses each time a database query has to be issued. So I started looking into other possibilities.

I come up with the idea of doing lazy loading and presenting some fake data until the data is really retrieved from the database, I did a small proof of concept and seems to work ok.

The solution goes like this. It uses Beans Binding to bind a JTable to a custom List like in the previous post. This custom list (ResultListDerby in the example below) returns @Bean@s (the Bean is called Customer in the example) that are initially empty. By empty I mean that all its properties return something like “<data not retrieved from the database yet>”. So the JTable will be initially full of “<data not retrieved yet>”.

The picture above shows the JTable showing the List @of @Customers. Each row represents a Customer instance.

Having empty/fake Customer beans allow a fast drawing to screen but it’s not very useful. Now, if at the same point as each Customer is created we create SwingWorker and schedule it to run in a Executor then we can get something more meaningful. So what we do in the SwingWorker? The SwingWorker, you guessed it, will retrieve the data from the database and update the bean (look at ResultListDerby.getItem() method). When the bean (Customer.java) is updated it will notify its listeners of the changes, automatically by means of bound properties , in this case JTableBinding will be notified and the JTable that will repaint the cells to reflect the changes in the Bean. (Check how bound properties are implemented with of PropertyChangeSupport in Customer.java below). The rows will be updated one by one, as soon as the Executor completes a SwingWorker task a row will be updated.

This solution is free from annoying GUI freezes. The beans are always created fast because there is no need to go to the database to create them and that keeps the GUI liveness high. By moving the database access to @SwingWorker@s we keep the EDT (event dispatch thread) lightweight. The solution also avoids having too many running @SwingWorker@s at the same time by using a ExecutorService instead of executing them directly via the SwingWorker.execute() method (Check ResultListDerby.getItem() to see how it’s done).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<span class='line'>package jtablepagination;
</span><span class='line'>
</span><span class='line'>import java.lang.ref.WeakReference;
</span><span class='line'>import java.sql.Connection;
</span><span class='line'>import java.sql.DriverManager;
</span><span class='line'>import java.sql.ResultSet;
</span><span class='line'>import java.sql.SQLException;
</span><span class='line'>import java.util.AbstractList;
</span><span class='line'>import java.util.HashMap;
</span><span class='line'>import java.util.List;
</span><span class='line'>import java.util.Map;
</span><span class='line'>import java.util.concurrent.ExecutorService;
</span><span class='line'>import java.util.concurrent.Executors;
</span><span class='line'>import java.util.logging.Level;
</span><span class='line'>import java.util.logging.Logger;
</span><span class='line'>import javax.swing.SwingWorker;
</span><span class='line'>
</span><span class='line'>/**
</span><span class='line'> *
</span><span class='line'> * @author Ruben Laguna &lt;ruben.laguna at gmail.com>
</span><span class='line'> */
</span><span class='line'>public class MainWindow extends javax.swing.JFrame {
</span><span class='line'>
</span><span class='line'>    
</span><span class='line'>
</span><span class='line'>    /** Creates new form MainWindow */
</span><span class='line'>    public MainWindow() {
</span><span class='line'>        initComponents();
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    /** This method is called from within the constructor to
</span><span class='line'>     * initialize the form.
</span><span class='line'>     * WARNING: Do NOT modify this code. The content of this method is
</span><span class='line'>     * always regenerated by the Form Editor.
</span><span class='line'>     */
</span><span class='line'>    @SuppressWarnings("unchecked")
</span><span class='line'>    // &lt;editor-fold defaultstate="collapsed" desc="Generated Code">                          
</span><span class='line'>    private void initComponents() {
</span><span class='line'>        bindingGroup = new org.jdesktop.beansbinding.BindingGroup();
</span><span class='line'>
</span><span class='line'>        list1 = getList();
</span><span class='line'>        jScrollPane1 = new javax.swing.JScrollPane();
</span><span class='line'>        jTable1 = new javax.swing.JTable();
</span><span class='line'>
</span><span class='line'>        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
</span><span class='line'>
</span><span class='line'>        org.jdesktop.swingbinding.JTableBinding jTableBinding = org.jdesktop.swingbinding.SwingBindings.createJTableBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, list1, jTable1);
</span><span class='line'>        org.jdesktop.swingbinding.JTableBinding.ColumnBinding columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${id}"));
</span><span class='line'>        columnBinding.setColumnName("id");
</span><span class='line'>        columnBinding.setColumnClass(Integer.class);
</span><span class='line'>        columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${firstName}"));
</span><span class='line'>        columnBinding.setColumnName("first name");
</span><span class='line'>        columnBinding.setColumnClass(String.class);
</span><span class='line'>        columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${lastName}"));
</span><span class='line'>        columnBinding.setColumnName("last name");
</span><span class='line'>        columnBinding.setColumnClass(String.class);
</span><span class='line'>        bindingGroup.addBinding(jTableBinding);
</span><span class='line'>        jTableBinding.bind();
</span><span class='line'>        jScrollPane1.setViewportView(jTable1);
</span><span class='line'>
</span><span class='line'>        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
</span><span class='line'>        getContentPane().setLayout(layout);
</span><span class='line'>        layout.setHorizontalGroup(
</span><span class='line'>            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
</span><span class='line'>            .addGroup(layout.createSequentialGroup()
</span><span class='line'>                .addContainerGap()
</span><span class='line'>                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 464, Short.MAX_VALUE)
</span><span class='line'>                .addContainerGap())
</span><span class='line'>        );
</span><span class='line'>        layout.setVerticalGroup(
</span><span class='line'>            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
</span><span class='line'>            .addGroup(layout.createSequentialGroup()
</span><span class='line'>                .addContainerGap()
</span><span class='line'>                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 344, Short.MAX_VALUE)
</span><span class='line'>                .addContainerGap())
</span><span class='line'>        );
</span><span class='line'>
</span><span class='line'>        bindingGroup.bind();
</span><span class='line'>
</span><span class='line'>        pack();
</span><span class='line'>    }// &lt;/editor-fold>                        
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>    private List&lt;Customer> getList() {
</span><span class='line'>        List&lt;Customer> toReturn = new ResultListDerby();
</span><span class='line'>        return toReturn;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    /**
</span><span class='line'>    * @param args the command line arguments
</span><span class='line'>    */
</span><span class='line'>    public static void main(String args[]) {
</span><span class='line'>        java.awt.EventQueue.invokeLater(new Runnable() {
</span><span class='line'>            public void run() {
</span><span class='line'>                new MainWindow().setVisible(true);
</span><span class='line'>            }
</span><span class='line'>        });
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    // Variables declaration - do not modify                     
</span><span class='line'>    private javax.swing.JScrollPane jScrollPane1;
</span><span class='line'>    private javax.swing.JTable jTable1;
</span><span class='line'>    private java.util.List&lt;Customer> list1;
</span><span class='line'>    private org.jdesktop.beansbinding.BindingGroup bindingGroup;
</span><span class='line'>    // End of variables declaration                   
</span><span class='line'>
</span><span class='line'>}
</span><span class='line'>
</span><span class='line'> class ResultListDerby extends AbstractList {
</span><span class='line'>
</span><span class='line'>    private Connection connection;
</span><span class='line'>    private final ExecutorService ex = Executors.newSingleThreadExecutor();
</span><span class='line'>    private int size = -1;
</span><span class='line'>    //maintain a cache with the Customer instances already created and alive
</span><span class='line'>    private Map&lt;Integer, WeakReference&lt;Customer>> cache = new HashMap&lt;Integer, WeakReference&lt;Customer>>();
</span><span class='line'>
</span><span class='line'>    ResultListDerby() {
</span><span class='line'>        try {
</span><span class='line'>            Class.forName("org.apache.derby.jdbc.ClientDriver");
</span><span class='line'>            this.connection = DriverManager.getConnection("jdbc:derby://localhost:1527/customer", "nbuser", "nbuser");
</span><span class='line'>        } catch (Exception ex) {
</span><span class='line'>            Logger.getLogger(ResultListDerby.class.getName()).log(Level.SEVERE, null, ex);
</span><span class='line'>            throw new RuntimeException(ex);
</span><span class='line'>        }
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public int size() {
</span><span class='line'>        if (this.size == -1) {
</span><span class='line'>            try {
</span><span class='line'>                final ResultSet resultset = connection.createStatement().executeQuery("SELECT COUNT(ID) FROM CUSTOMERS");
</span><span class='line'>                resultset.next();
</span><span class='line'>                final int toReturn = resultset.getInt(1);
</span><span class='line'>                this.size = toReturn;
</span><span class='line'>            } catch (SQLException ex) {
</span><span class='line'>                Logger.getLogger(ResultListDerby.class.getName()).log(Level.SEVERE, null, ex);
</span><span class='line'>                throw new RuntimeException(ex);
</span><span class='line'>            }
</span><span class='line'>        }
</span><span class='line'>        return this.size;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public Customer get(int rowIndex) {
</span><span class='line'>        //this way we ensure that we don't create several Customer instances 
</span><span class='line'>        //for the same id. Otherwise it would be confusing for beansbindings. 
</span><span class='line'>        Customer toReturn = null;
</span><span class='line'>        if (null != this.cache.get(rowIndex)) {
</span><span class='line'>            toReturn = this.cache.get(rowIndex).get();
</span><span class='line'>        }
</span><span class='line'>        if (null == toReturn) {
</span><span class='line'>            toReturn = getItem(rowIndex);
</span><span class='line'>            this.cache.put(rowIndex, new WeakReference&lt;Customer>(toReturn));
</span><span class='line'>        }
</span><span class='line'>
</span><span class='line'>        return toReturn;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    private Customer getItem(final int j) {
</span><span class='line'>        final Customer customer = new Customer(j);
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>        Runnable task = new SwingWorker() {
</span><span class='line'>
</span><span class='line'>            private String firstNameValue;
</span><span class='line'>            private String lastNameValue;
</span><span class='line'>
</span><span class='line'>            @Override
</span><span class='line'>            protected Object doInBackground() throws Exception {
</span><span class='line'>                //this is always executed in a different thread from the current thread
</span><span class='line'>                //it doesn't matter if the current thread is the EDT or a thread in the Executor
</span><span class='line'>                final java.sql.Statement stmt = connection.createStatement();
</span><span class='line'>                ResultSet executeQuery = stmt.executeQuery("SELECT ID, FIRST_NAME,LAST_NAME FROM CUSTOMERS ORDERDESC OFFSET " + j + " ROWS FETCH NEXT ROWS ONLY");
</span><span class='line'>                executeQuery.next();
</span><span class='line'>                firstNameValue = executeQuery.getString(2);
</span><span class='line'>                lastNameValue = executeQuery.getString(3);
</span><span class='line'>                return null;
</span><span class='line'>            }
</span><span class='line'>
</span><span class='line'>            @Override
</span><span class='line'>            protected void done() {
</span><span class='line'>                //this in the other hand will always be executed on the EDT.
</span><span class='line'>                //This has to be done in the EDT because currently JTableBinding
</span><span class='line'>                //is not smart enough to realize that the notification comes in another 
</span><span class='line'>                //thread and do a SwingUtilities.invokeLater. So we are force to execute this
</span><span class='line'>                // in the EDT. Seee http://markmail.org/thread/6ehh76zt27qc5fis and
</span><span class='line'>                // https://beansbinding.dev.java.net/issues/show_bug.cgi?id=60
</span><span class='line'>    
</span><span class='line'>                customer.setFirstName(firstNameValue);
</span><span class='line'>                customer.setLastName(lastNameValue);
</span><span class='line'>                Logger.getLogger(ResultListDerby.class.getName()).info("updating customer " + customer);
</span><span class='line'>            }
</span><span class='line'>        };
</span><span class='line'>
</span><span class='line'>        //NOTE that we don do task.execute()
</span><span class='line'>        //posting the task to an Executor gives more control on 
</span><span class='line'>        //how many threads are created. 
</span><span class='line'>        ex.execute(task);
</span><span class='line'>        return customer;
</span><span class='line'>    }
</span><span class='line'>}</span>

and Customer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<span class='line'>package jtablepagination;
</span><span class='line'>
</span><span class='line'>import java.beans.PropertyChangeListener;
</span><span class='line'>import java.beans.PropertyChangeSupport;
</span><span class='line'>import java.util.logging.Logger;
</span><span class='line'>
</span><span class='line'>/**
</span><span class='line'> *
</span><span class='line'> * @author Ruben Laguna &lt;ruben.laguna at gmail.com>
</span><span class='line'> */
</span><span class='line'>public class Customer {
</span><span class='line'>
</span><span class='line'>    private static final Logger LOG = Logger.getLogger(Customer.class.getName());
</span><span class='line'>    private static final String NOT_RETRIEVED_YET = "&lt;not retrieved yet>";
</span><span class='line'>    private int id = 0;
</span><span class='line'>    public static final String PROP_ID = "id";
</span><span class='line'>    protected String firstName = NOT_RETRIEVED_YET;
</span><span class='line'>    public static final String PROP_FIRSTNAME = "firstName";
</span><span class='line'>    protected String lastName = NOT_RETRIEVED_YET;
</span><span class='line'>    public static final String PROP_LASTNAME = "lastName";
</span><span class='line'>    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
</span><span class='line'>
</span><span class='line'>    Customer(int j) {
</span><span class='line'>        this.id = j;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public int getId() {
</span><span class='line'>        return id;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public String getFirstName() {
</span><span class='line'>        return firstName;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void setFirstName(String firstName) {
</span><span class='line'>        String oldFirstName = this.firstName;
</span><span class='line'>        this.firstName = firstName;
</span><span class='line'>        propertyChangeSupport.firePropertyChange(PROP_FIRSTNAME, oldFirstName, firstName);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public String getLastName() {
</span><span class='line'>        return lastName;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void setLastName(String lastName) {
</span><span class='line'>        String oldLastName = this.lastName;
</span><span class='line'>        this.lastName = lastName;
</span><span class='line'>        if (propertyChangeSupport.hasListeners(PROP_LASTNAME)) {
</span><span class='line'>            LOG.info("notifying!!!");
</span><span class='line'>        } else {
</span><span class='line'>            LOG.info("there is no listeners for the property");
</span><span class='line'>        }
</span><span class='line'>
</span><span class='line'>        propertyChangeSupport.firePropertyChange(PROP_LASTNAME, oldLastName, lastName);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void addPropertyChangeListener(PropertyChangeListener listener) {
</span><span class='line'>        LOG.info("add listener to customer " + this.id);
</span><span class='line'>        propertyChangeSupport.addPropertyChangeListener(listener);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void removePropertyChangeListener(PropertyChangeListener listener) {
</span><span class='line'>        propertyChangeSupport.removePropertyChangeListener(listener);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    @Override
</span><span class='line'>    public String toString() {
</span><span class='line'>        return "id: " + id + " firstName: " + firstName;
</span><span class='line'>    }
</span><span class='line'>}</span>

References

Migrating From Blinklist to Delicious.com: CSV to HTML

Apparently blinklist doesn’t export bookmarks to JSON format any longer and delicious has changed authentication scheme for its delicious API for new accounts (now it forces new users to use the OAuth / Yahoo ID). So the solutions described in this old post of mine doesn’t work.

So given the current state of affairs the only way to get your bookmarks out of Blinklist is CSV (actually tab-separated) and the only easy way to import them to delicious is to use the HTML import. So we need a way to transforms Blinklist’s CSV to HTML bookmark file format. So I created this ruby scripts that takes bookmark.csv and generates bookmarks.html that you can import to delicious.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<span class='line'>#!/usr/bin/ruby
</span><span class='line'>require "rubygems"
</span><span class='line'>require "csv" 
</span><span class='line'>
</span><span class='line'>i=0
</span><span class='line'>File.open('bookmarks.html', 'w') do |f|   
</span><span class='line'>  f.print &lt;&lt;EOF
</span><span class='line'>  &lt;!DOCTYPE NETSCAPE-Bookmark-file-1>
</span><span class='line'>  &lt;META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
</span><span class='line'>  &lt;!-- This is an automatically generated file.
</span><span class='line'>  It will be read and overwritten.
</span><span class='line'>  Do Not Edit! -->
</span><span class='line'>  &lt;TITLE>Bookmarks&lt;/TITLE>
</span><span class='line'>  &lt;H1>Bookmarks&lt;/H1>
</span><span class='line'>  &lt;DL>&lt;p>
</span><span class='line'>EOF
</span><span class='line'>
</span><span class='line'>  CSV.open('bookmark.csv','r',"\t") do  |item|
</span><span class='line'>    next if item[0] == "url"
</span><span class='line'>    i += 1
</span><span class='line'>    puts "#{i}: #{item[0]}" #the url is position 0 
</span><span class='line'>    puts "#{i}: #{item[1]}" #the name is position 1 
</span><span class='line'>    puts "#{i}: #{item[3]}" #the tags are in position 3
</span><span class='line'>    #next if i > 3229
</span><span class='line'>    #r.add(item[0],item[1],"no description", getTags(item), getTime(item), true, getIsPrivate(item)) #url, name,tags,time,
</span><span class='line'>    f.puts "&lt;DT>&lt;A HREF=\"#{item[0]}\" LAST_VISIT=\"1248434357\" ADD_DATE=\"1248434357\" TAGS=\"#{item[3]}\">#{item[1]}&lt;/A>"
</span><span class='line'>  end 
</span><span class='line'>  f.puts "&lt;/DL>&lt;p>"
</span><span class='line'>end
</span><span class='line'>puts "ended";</span>

UPDATE: It seems that in newer version of ruby there are changes in the csv module+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<span class='line'>#!/usr/bin/ruby
</span><span class='line'>require "rubygems"
</span><span class='line'>require "csv" 
</span><span class='line'>
</span><span class='line'>i=0
</span><span class='line'>File.open('bookmarks.html', 'w') do |f|
</span><span class='line'>  f.print &lt;&lt;EOF
</span><span class='line'>  &lt;!DOCTYPE NETSCAPE-Bookmark-file-1>
</span><span class='line'>  &lt;META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
</span><span class='line'>  &lt;!-- This is an automatically generated file.
</span><span class='line'>  It will be read and overwritten.
</span><span class='line'>  Do Not Edit! -->
</span><span class='line'>  &lt;TITLE>Bookmarks&lt;/TITLE>
</span><span class='line'>  &lt;H1>Bookmarks&lt;/H1>
</span><span class='line'>  &lt;DL>&lt;p>
</span><span class='line'>EOF
</span><span class='line'>
</span><span class='line'>  CSV.foreach('bookmark.csv',{:col_sep => "\t"}) do  |item|
</span><span class='line'>    next if item[0] == "url"
</span><span class='line'>    i += 1
</span><span class='line'>    puts "#{i}: #{item[0]}" #the url is position 0
</span><span class='line'>    puts "#{i}: #{item[1]}" #the name is position 1
</span><span class='line'>    puts "#{i}: #{item[2]}" #the description is in position 2
</span><span class='line'>    puts "#{i}: #{item[3]}" #the tags are in position 3
</span><span class='line'>    #next if i > 3229
</span><span class='line'>    #r.add(item[0],item[1],"no description", getTags(item), getTime(item), true, getIsPrivate(item)) #url, name,tags,time,
</span><span class='line'>    f.puts "&lt;DT>&lt;A HREF=\"#{item[0]}\" LAST_VISIT=\"1248434357\" ADD_DATE=\"1248434357\" TAGS=\"#{item[3]}\">#{item[1]}&lt;/A>
</span><span class='line'>&lt;DD>#{item[2]}"
</span><span class='line'>  end
</span><span class='line'>  f.puts "&lt;/DL>&lt;p>"
</span><span class='line'>end
</span><span class='line'>puts "ended";</span>

How to Handle Custom Tags Using Flying Saucer

If you have a customized xhtml variant, like evernote ENML format and you want to render it with Flying Saucer R8 you must first make sure that the extra element (in ENML case, en-media) is defined as a block-level element. To do that you create your own NamespaceHandler and you make sure that you return "display: block;" for en-media in the implementation of NamespaceHandler.getNonCssStyling. (See ENMLNamespaceHandler.java below)

        ....
        XHTMLPanel panel = new XHTMLPanel();
        panel.setDocument(doc,"",new ENMLNamespaceHandler(new XhtmlNamespaceHandler()));
        ....
        class ENMLNamespaceHandler implements NamespaceHandler {
        ....
           public String getNonCssStyling(Element e) {
               String toReturn = delegate.getNonCssStyling(e);
               if ("en-media".equalsIgnoreCase(e.getNodeName())) {
                  toReturn = "display: block;";
               }
               return toReturn;
           }
        ....
        }

With that you ensure that xhtmlrenderer will call ReplacedElementFactory.createReplacedElement for en-media. Now you must supply a ReplacedElementFactory that it’s able to process en-media. Usually the implementation of the createReplacedElement() involves creating a Swing JComponent, wrapping it in a SwingReplacedElement and adding it to the LayoutContext.getCanvas().

        ....
       ReplacedElementFactory cef = new ENMLReplacedElementFactory(new SwingReplacedElementFactory());
       XHTMLPanel panel = new XHTMLPanel();
       panel.getSharedContext().setReplacedElementFactory(cef);
       panel.setDocument(doc,"",new ENMLNamespaceHandler(new XhtmlNamespaceHandler()));
        ....

       public class ENMLReplacedElementFactory implements ReplacedElementFactory {
       ...
          public ReplacedElement createReplacedElement(LayoutContext context, BlockBox box,
                                                                                      UserAgentCallback uac, int cssWidth, int cssHeight) 
          {
        
             if ("en-media".equals(box.getElement().getNodeName())) {
                    JTextArea cc = new JTextArea();
                   cc.setText("Missing implementation for en-media");
                   cc.setSize(cc.getPreferredSize());

                   context.getCanvas().add(cc);

                   ReplacedElement toReturn = new SwingReplacedElement(cc) {
                     public boolean isRequiresInteractivePaint() {
                         return false;
                     }
                  };

                 return toReturn;
         }

        ReplacedElement toReturn = delegate.createReplacedElement(context, box, uac, cssWidth, cssHeight);
        return toReturn;
    }
....
}

References:


  1. Experiment: embedding Flash in Flying Saucer

  2. NamespaceHandler

  3. ReplacedElementFactory

  4. Re: custom layout support

  5. http://wiki.java.net/bin/view/Javadesktop/TheFlyingSaucerInProcessFAQ

ENMLNamespaceHandler.java:

package com.rubenlaguna.en4j.NoteContentViewModule;

import java.util.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xhtmlrenderer.css.extend.StylesheetFactory;
import org.xhtmlrenderer.css.sheet.StylesheetInfo;
import org.xhtmlrenderer.extend.NamespaceHandler;

/**
 *
 * @author ecerulm
 */
class ENMLNamespaceHandler implements NamespaceHandler {
    private final NamespaceHandler delegate;

    public ENMLNamespaceHandler(NamespaceHandler h) {
        this.delegate = h;
    }

    public boolean isImageElement(Element e) {
        return delegate.isImageElement(e);
    }

    public boolean isFormElement(Element e) {
        return delegate.isFormElement(e);
    }

    public StylesheetInfo[] getStylesheets(Document doc) {
        return delegate.getStylesheets(doc);
    }

    public String getNonCssStyling(Element e) {
        String toReturn = delegate.getNonCssStyling(e);
        if ("en-media".equalsIgnoreCase(e.getNodeName())) {
            toReturn = "display: block;";
        }
        Logger.getLogger(ENMLNamespaceHandler.class.getName()).info("style for ("+e.getNodeName()+") is ("+toReturn+")");
        return toReturn;
    }

    public String getNamespace() {
        return delegate.getNamespace();
    }

    public String getLinkUri(Element e) {
        return delegate.getLinkUri(e);
    }

    public String getLang(Element e) {
        return delegate.getLang(e);
    }

    public String getImageSourceURI(Element e) {
        return delegate.getImageSourceURI(e);
    }

    public String getID(Element e) {
        return delegate.getID(e);
    }

    public String getElementStyling(Element e) {
        return delegate.getElementStyling(e);
    }

    public String getDocumentTitle(Document doc) {
        return delegate.getDocumentTitle(doc);
    }

    public StylesheetInfo getDefaultStylesheet(StylesheetFactory factory) {
        return delegate.getDefaultStylesheet(factory);
    }

    public String getClass(Element e) {
        return delegate.getClass(e);
    }

    public String getAttributeValue(Element e, String namespaceURI, String attrName) {
        return delegate.getAttributeValue(e, namespaceURI, attrName);
    }

    public String getAttributeValue(Element e, String attrName) {
        return delegate.getAttributeValue(e, attrName);
    }

    public String getAnchorName(Element e) {
        return delegate.getAnchorName(e);
    }
    
}

ENMLReplacedElementFactory.java:


/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */
    package com.rubenlaguna.en4j.NoteContentViewModule;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import org.w3c.dom.Element;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
import org.xhtmlrenderer.swing.EmptyReplacedElement;
import org.xhtmlrenderer.swing.ImageReplacedElement;
import org.xhtmlrenderer.swing.SwingReplacedElement;
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
import org.xhtmlrenderer.util.ImageUtil;

/**
*

  • @author ecerulm
    */
    class ENMLReplacedElementFactory implements ReplacedElementFactory {
private final SwingReplacedElementFactory delegate; private final Logger LOG = Logger.getLogger(ENMLReplacedElementFactory.class.getName()); public ENMLReplacedElementFactory(SwingReplacedElementFactory delegate) { this.delegate = delegate; } public void setFormSubmissionListener(FormSubmissionListener fsl) { delegate.setFormSubmissionListener(fsl); } public void reset() { delegate.reset(); } public void remove(Element e) { delegate.remove(e); } public ReplacedElement createReplacedElement(LayoutContext context, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) { ReplacedElement toReturn = null; Logger.getLogger(ENMLReplacedElementFactory.class.getName()).log(Level.INFO, “Element:” + box.getElement().getNodeName()); Logger.getLogger(ENMLReplacedElementFactory.class.getName()).log(Level.INFO, “Element content:” + box.getElement().getNodeValue()); if (“en-media”.equals(box.getElement().getNodeName())) { //if(“image/jpeg”.equalsIgnoreCase(box.getElement().getAttribute(“type”))){ // // toReturn = loadImage() // return toReturn; //} toReturn = brokenImage(context, 100, 100); } if (null == toReturn) { toReturn = delegate.createReplacedElement(context, box, uac, cssWidth, cssHeight); } return toReturn; } private ReplacedElement brokenImage(LayoutContext context, int cssWidth, int cssHeight) { //TODO: add a real implementation that returns an image ReplacedElement toReturn = null; JTextArea cc = new JTextArea(); cc.setText(“Missing implementation for en-media”); //cc.setPreferredSize(new Dimension(cssWidth, cssHeight)); cc.setSize(cc.getPreferredSize()); context.getCanvas().add(cc); toReturn = new SwingReplacedElement(cc) { public boolean isRequiresInteractivePaint() { return false; } }; return toReturn; }

}

Incorrect Track Length in iTunes

iTunes was reporting a incorrect track time length for an audiobook that I created concatenating several mp3 together.

I had to reencode the whole thing again to make iTunes recognize propertly the track lenght. I could’t reencode the concatenated file itself as that ended up with a Segmentation fault in lame

cat *.mp3 |lame --mp3input - - >resultfile.mp3

cat *Night.mp3 |lame --mp3input -  down.mp3
ID3v2 found. Be aware that the ID3 tag is currently lost when transcoding.
LAME 3.98.2 64bits (http://www.mp3dev.org/)
Using polyphase lowpass filter, transition band: 16538 Hz - 17071 Hz
Encoding <stdin> to down.mp3
Encoding as 44.1 kHz single-ch MPEG-1 Layer III (11x)  64 kbps qval=3
bitstream problem: resyncing...
bitstream problem: resyncing...
bitstream problem: resyncing...
bitstream problem: resyncing...

This uses the lame streaming mode, decodes the input stream (all mp3 concatenated) as mp3 and then reencodes it as mp3. Although it complains about “bitstream problem: resyncing...”, the resulting mp3 sounds fine.

Disable DOM DTD Validation

setValidating(false) will not prevent DTD validation, but you can set http://apache.org/xml/features/nonvalidating/load-external-dtd feature to false. I think is a Xerces-only feature, though, it may not be recognized by other parsers.

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        //factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); // it doesn't work
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        DocumentBuilder parser = factory.newDocumentBuilder();
        Document doc = parser.parse(getClass().getResourceAsStream("ennote.xml"));

Typical DTD not found exception stacktrace


java.io.FileNotFoundException: /Users/ecerulm/NetBeansProjects/en4j/NBPlatformApp/DTD/xhtml1-strict.dtd (No such file or directory)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.(FileInputStream.java:106)
at java.io.FileInputStream.(FileInputStream.java:66)
at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:70)
at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:161)
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.setupCurrentEntity(XMLEntityManager.java:653)
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startEntity(XMLEntityManager.java:1315)
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startDTDEntity(XMLEntityManager.java:1282)
at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.setInputSource(XMLDTDScannerImpl.java:283)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.dispatch(XMLDocumentScannerImpl.java:1193)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.next(XMLDocumentScannerImpl.java:1090)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$PrologDriver.next(XMLDocumentScannerImpl.java:1003)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:648)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:807)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:737)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:107)
at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:225)
at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:283)
at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:124)
at com.rubenlaguna.en4j.NoteContentViewModule.NoteContentViewTopComponent.resultChanged(NoteContentViewTopComponent.java:141)


Netbeans: Compile Schemas Into JAXB Classes With XJC Task

To add a XJC task to a Netbeans build.xml you need to add a -pre-compile target to your build.xml. If it’s a netbeans module build.xml then you need to make compile depend on -pre-compile and projectized-common.compile (see example below). For regular Java Applications just define the -pre-compile target as the standard build.xml will call -pre-compile before compile. Then it’s just a matter of defining the <taskdef> with the proper classpath to the jaxb’s jars.

But it’s just simpler to take a look to an example (this one is for a Netbeans module build.xml:

<project name="com.rubenlaguna.en4j.jaxb" default="netbeans" basedir=".">
    <description>Builds, tests, and runs the project com.rubenlaguna.en4j.jaxb.</description>
    <import file="nbproject/build-impl.xml"/>


 <target name="-pre-compile">
        <path id="xjc.classpath">

            <fileset dir="/Applications/NetBeans/NetBeans 6.7.app/Contents/Resources/NetBeans/ide11/modules/ext/jaxb/" includes="*.jar"/>

            <!-- import the JAXB 2.1 library if this task is in java application build.xml. you can't access library classpath from
                     a module's build.xml
            pathelement path="${libs.JAXB21.classpath}"/>
            <pathelement path="${libs.JAXB_21.classpath}"/>
            -->
        </path>

        <taskdef name="xjc" classname="com.sun.tools.xjc.XJCTask">
            <classpath refid="xjc.classpath"/>
        </taskdef>

        <xjc destdir="src" package="com.rubenlaguna.en4j.jaxb.generated">
            <schema  dir="src" includes="**/*.xsd"/>
            <produces dir="src/com/rubenlaguna/en4j/jaxb/generated" includes="* impl/*" />
        </xjc>
    </target>
    <target name="compile" depends="-pre-compile,projectized-common.compile"/>
</project>

Netbeans & Woodstox: ClassCastException: JAXBContextImpl Cannot Be Cast to JAXBContext

Are you getting a ClassCastException in a Netbeans Platform Application, when you are you sure that the class can be casted? Then check http://wiki.netbeans.org/PlainView.jsp?page=DevFaqModuleCCE. This is most likely the result of the the classes being loaded by different classloaders. So JAXBContextImpl cannot be cast to JAXBContext because that JAXBContext is from Classloader “A” and @JAXBContextImpl @can only be cast to @JAXBContext @from Classloader “B”.

SEVERE [global]
java.lang.ClassCastException: com.sun.xml.bind.v2.runtime.JAXBContextImpl cannot be cast to javax.xml.bind.JAXBContext
        at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:145)
        at javax.xml.bind.ContextFinder.find(ContextFinder.java:277)
        at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:372)
        at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:337)
        at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:244)
        at com.rubenlaguna.en4j.mainmodule.ImportEvernoteFile.actionPerformed(ImportEvernoteFile.java:65)
        at org.openide.awt.AlwaysEnabledAction.actionPerformed(AlwaysEnabledAction.java:115)
        at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:2028)
        at javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2351)
        at javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:387)
        at javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:242)
        at javax.swing.AbstractButton.doClick(AbstractButton.java:389)
        at com.apple.laf.ScreenMenuItem.actionPerformed(ScreenMenuItem.java:95)
        at java.awt.MenuItem.processActionEvent(MenuItem.java:627)
        at java.awt.MenuItem.processEvent(MenuItem.java:586)
        at java.awt.MenuComponent.dispatchEventImpl(MenuComponent.java:317)
        at java.awt.MenuComponent.dispatchEvent(MenuComponent.java:305)
        at java.awt.EventQueue.dispatchEvent(EventQueue.java:638)
        at org.netbeans.core.TimableEventQueue.dispatchEvent(TimableEventQueue.java:104)
[catch] at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:296)
        at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:211)
        at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:201)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:196)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:188)
        at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
BUILD SUCCESSFUL (total time: 28 minutes 5 seconds)

So this could happen for example if you have both woodstox in a Netbeans Library Wrapper module and you make you application dependent on ide11JAXB 2.1 Library
jaxb21.

In my case I was able to solve by just removing JAXB 2.1 Library from the application.

Copyright © 2015 - Ruben Laguna - Powered by Octopress