1 package wjhk.jupload2.gui.filepanel.treeview;
2
3 import java.lang.reflect.Array;
4 import java.util.ArrayList;
5 import java.util.List;
6
7 import javax.swing.JTree;
8 import javax.swing.event.EventListenerList;
9 import javax.swing.event.TreeModelEvent;
10 import javax.swing.event.TreeModelListener;
11 import javax.swing.tree.TreeModel;
12 import javax.swing.tree.TreePath;
13
14 import wjhk.jupload2.policies.UploadPolicy;
15
16 /**
17 * This code is taken from the tutorial written by Jörn Hameister, <A
18 * HREF="http://www.hameister.org/JavaSwingTreeTable.html">available here</A>.<BR/>
19 * <BR/>
20 * From Jörn sample, the main modifications are:
21 * <UL>
22 * <LI>Use of the uploadPolicy</LI>
23 * <LI>Add of the getRoot and setRoot methods</LI>
24 * <LI>Capability to remove an item</LI>
25 * <LI>Transformation in a generic type</LI>
26 * <UL>
27 * <BR/>
28 * In the next step, the interface is MyTreeTableModel to the abstract class MyAbstractTreeTableModel expanded. In this
29 * class, the Wuzelknoten (root) is stored, there is a method for checking whether child nodes are available and manages
30 * all event listener verwerden. The EventListener ensure that structural changes in the data model will be forwarded to
31 * the tree and displayed.<BR/>
32 * <B>Note:</B> We can not use the DefaultTreeModel for various model. One is that we need multiple columns. Another one
33 * is that we must delegate the content to the FilePanel, as the view may be flat or treeview. So we stay with the
34 * implementation which existed before the tree view, there would otherwise be too much changes in the applet code.
35 *
36 * @author Jörn Hameister
37 */
38 public abstract class MyAbstractTreeTableModel<T extends MyTreeNode> implements MyTreeTableModel<T> {
39
40 UploadPolicy uploadPolicy = null;
41
42 /** The {@link JTree} which contains the data for this model */
43 MyTreeTableCellRenderer tree = null;
44
45 /**
46 * The absolute root of the hierarchical structure. It can't be delegated to the tree table, as the displayed root
47 * can change, when adding other files to upload.<BR/>
48 * Note: on Windows, there can be several roots. So the JUpload's absolute root is a non existing folder, which
49 * contain one folder under Unix ('/') and may contain several folders under Windows ('C:\', 'D:\'...).
50 */
51 protected T absoluteRoot = null;
52
53 protected T visibleRoot;
54
55 protected EventListenerList listenerList = new EventListenerList();
56
57 private static final int CHANGED = 0;
58
59 private static final int INSERTED = 1;
60
61 private static final int REMOVED = 2;
62
63 private static final int STRUCTURE_CHANGED = 3;
64
65 public MyAbstractTreeTableModel(UploadPolicy uploadPolicy, T root) {
66 this.uploadPolicy = uploadPolicy;
67 this.absoluteRoot = root;
68 this.visibleRoot = root;
69 this.absoluteRoot.setTreeModel(this);
70 this.visibleRoot.setTreeModel(this);
71 }
72
73 /**
74 * @return the tree
75 */
76 public MyTreeTableCellRenderer getTree() {
77 return tree;
78 }
79
80 /**
81 * @param tree the tree to set
82 */
83 public void setTree(MyTreeTableCellRenderer tree) {
84 this.tree = tree;
85 }
86
87 public T getAbsoluteRoot() {
88 return this.absoluteRoot;
89 }
90
91 /** @see MyTreeTableModel#getRoot() */
92 public T getRoot() {
93 return visibleRoot;
94 }
95
96 /**
97 * @see MyTreeTableModel#setRoot(MyTreeNode)
98 * @throws IllegalArgumentException If this root is not valid.
99 */
100 public synchronized void setRoot(T root) {
101 this.uploadPolicy.displayInfo("Setting visible Root to file " + root.toString());
102 this.visibleRoot = root;
103 reload();
104 }
105
106 /** @see MyTreeTableModel#remove(MyTreeNode) */
107 public synchronized void remove(T item) {
108 remove(item, true);
109 }
110
111 /** @see MyTreeTableModel#remove(MyTreeNode) */
112 public synchronized void removeAndClean(T item) {
113 removeAndClean(item, true);
114 }
115
116 /**
117 * Actual implementation of {@link #remove(MyTreeNode)}
118 *
119 * @param item The item to remove
120 * @param callReloadAfterRemoval true if the {@link #reload()} should be called after removal, false otherwise.
121 * @see MyTreeTableModel#remove(MyTreeNode)
122 */
123 @SuppressWarnings("unchecked")
124 synchronized void remove(T item, boolean callReloadAfterRemoval) {
125 if (item == absoluteRoot) {
126 throw new IllegalArgumentException("The absoluteRoot may not be removed");
127 }
128 if (item == visibleRoot) {
129 setRoot((T) visibleRoot.getParent());
130 }
131
132 FolderNode parent = (FolderNode) item.getParent();
133 if (parent != null) {
134 // Actually executes the removal. This removes the node, and detach all of its descendants.
135 parent.removeChild(item);
136 // If asked for, we indicates that the structure has changed.
137 if (callReloadAfterRemoval) {
138 reload();
139 }
140 }
141 }
142
143 /**
144 * Actual implementation of {@link #removeAndClean(MyTreeNode)}
145 *
146 * @param item The item to remove
147 * @param callReloadAfterRemoval true if the {@link #reload()} should be called after removal, false otherwise.
148 * @see wjhk.jupload2.gui.filepanel.treeview.MyTreeTableModel#removeAndClean(wjhk.jupload2.gui.filepanel.treeview.MyTreeNode)
149 */
150 @SuppressWarnings("unchecked")
151 synchronized void removeAndClean(T item, boolean callReloadAfterRemoval) {
152 if (item == absoluteRoot) {
153 throw new IllegalArgumentException("The absoluteRoot may not be removed");
154 }
155 if (item == visibleRoot) {
156 setRoot((T) visibleRoot.getParent());
157 }
158
159 T parentNode = (T) item.getParent();
160
161 // First step: actually remove the node
162 remove(item, false);
163
164 // Second step: remove the parent node, if it had no other child.
165 if (parentNode.getChildCount() == 0) {
166 // No reload during recursion
167 removeAndClean(parentNode, false);
168 } else if (callReloadAfterRemoval) {
169 reload();
170 }
171 }
172
173 /** @see MyTreeTableModel#getTreePath(MyTreeNode) */
174 @SuppressWarnings("unchecked")
175 public TreePath getTreePath(T item) {
176 if (item == null) {
177 // This method may be called with a null Node. In this case, we return .. an empty TreePath
178 return null;
179 } else if (item == this.absoluteRoot) {
180 return new TreePath(this.absoluteRoot);
181 } else if (item.getParent() == null) {
182 throw new IllegalArgumentException("Root not found for node " + item);
183 } else {
184 return getTreePath((T) item.getParent()).pathByAddingChild(item);
185 }
186 }
187
188 public void addTreeModelListener(TreeModelListener l) {
189 listenerList.add(TreeModelListener.class, l);
190 }
191
192 public void removeTreeModelListener(TreeModelListener l) {
193 listenerList.remove(TreeModelListener.class, l);
194 }
195
196 private void fireTreeNode(int changeType, Object source, TreePath path, int[] childIndices, T[] children) {
197 Object[] listeners = listenerList.getListenerList();
198 TreeModelEvent e = new TreeModelEvent(source, path, childIndices, children);
199 for (int i = listeners.length - 2; i >= 0; i -= 2) {
200 if (listeners[i] == TreeModelListener.class) {
201 switch (changeType) {
202 case CHANGED:
203 ((TreeModelListener) listeners[i + 1]).treeNodesChanged(e);
204 break;
205 case INSERTED:
206 ((TreeModelListener) listeners[i + 1]).treeNodesInserted(e);
207 break;
208 case REMOVED:
209 ((TreeModelListener) listeners[i + 1]).treeNodesRemoved(e);
210 break;
211 case STRUCTURE_CHANGED:
212 ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e);
213 break;
214 default:
215 break;
216 }
217 }
218 }
219 }
220
221 /** @see wjhk.jupload2.gui.filepanel.treeview.MyTreeTableModel#cleanHierarchy() */
222 synchronized public boolean cleanHierarchy() {
223 return cleanHierarchy(absoluteRoot, true);
224 }
225
226 /**
227 * Cleans the hierarchy behind a given node. It's actually the recursive implementation for
228 * {@link wjhk.jupload2.gui.filepanel.treeview.MyTreeTableModel#cleanHierarchy()}
229 *
230 * @return true if at least one node was removed, due to cleaning. false if no cleaning was needed.
231 */
232 synchronized public boolean cleanHierarchy(T node) {
233 return cleanHierarchy(node, true);
234 }
235
236 /**
237 * Cleans the hierarchy behind a given node. It's actually the recursive implementation for
238 * {@link wjhk.jupload2.gui.filepanel.treeview.MyTreeTableModel#cleanHierarchy()}
239 *
240 * @param node The node where cleaning starts. This node will not be removed.
241 * @param callReloadAfterRemoval true if the {@link #reload()} should be called after removal, false otherwise.
242 * @return true if at least one node was removed, due to cleaning. false if no cleaning was needed.
243 */
244 @SuppressWarnings("unchecked")
245 synchronized private boolean cleanHierarchy(T node, boolean callReloadAfterRemoval) {
246 boolean ret = false;
247 if (node != null) {
248 if (node.getChildren().size() > 0) {
249 // To avoid that a ConcurrentModificationException is thrown, we must not use the list. Here is another
250 // way to loop into it, while the list size can diminish (due to node removal during recursion)
251 List<T> children = new ArrayList<T>();
252 for (MyTreeNode child : node.getChildren()) {
253 children.add((T) child);
254 }
255
256 for (T child : children) {
257 if (cleanHierarchy(child, false)) {
258 ret = true;
259 }
260 // If we've a non leaf folder (typically a FolderNode), with no children, we remove it.
261 if (!child.isLeaf() && child.getChildCount() == 0) {
262 node.removeChild(child);
263 ret = true;
264 }
265 }// for
266 }// if (node.getChildren().size() > 0)
267 }// if (node != null)
268
269 if (ret && callReloadAfterRemoval) {
270 reload();
271 }
272 return ret;
273 }
274
275 /** @see MyTreeTableModel#reload() */
276 public synchronized void reload() {
277 reload(visibleRoot);
278 }
279
280 /** @see MyTreeTableModel#reload(MyTreeNode) */
281 @SuppressWarnings("unchecked")
282 public synchronized void reload(T node) {
283 //
284 int n = getChildCount(node);
285 int[] childIdx = new int[n];
286 T[] children = (T[]) (T[]) Array.newInstance(MyTreeNode.class, n);
287
288 for (int i = 0; i < n; i++) {
289 childIdx[i] = i;
290 children[i] = getChild(node, i);
291 }
292
293 fireTreeStructureChanged(this, getPathToRoot(node), childIdx, (T[]) children);
294 }
295
296 /**
297 * Builds the parents of node up to and including the root node, where the original node is the last element in the
298 * returned array. The length of the returned array gives the node's depth in the tree.
299 *
300 * @param node - the TreeNode to get the path for
301 * @return TreeNode[] - the path from node to the root
302 * @throws IllegalArgumentException If the node has not visibleRoot as a ancestror.
303 */
304 private TreePath getPathToRoot(MyTreeNode node) {
305 if (node == visibleRoot) {
306 return new TreePath(visibleRoot);
307 } else if (node == absoluteRoot) {
308 throw new IllegalArgumentException("The visibleRoot is not an ancestror of the given node");
309 } else {
310 MyTreeNode parent = (MyTreeNode) node.getParent();
311 if (parent == null) {
312 return null;
313 } else {
314 return getPathToRoot(parent).pathByAddingChild(node);
315 }
316 }
317 }
318
319 /** @see MyTreeTableModel#fireTreeNodesChanged(MyTreeNode, MyTreeNode[], int[], MyTreeNode[]) */
320 public void fireTreeNodesChanged(Object source, TreePath path, int[] childIndices, T[] children) {
321 fireTreeNode(CHANGED, source, path, childIndices, children);
322 }
323
324 /** @see MyTreeTableModel#fireTreeNodesInserted(MyTreeNode, MyTreeNode[], int[], MyTreeNode[]) */
325 public void fireTreeNodesInserted(Object source, TreePath path, int[] childIndices, T[] children) {
326 fireTreeNode(INSERTED, source, path, childIndices, children);
327 }
328
329 /** @see MyTreeTableModel#fireTreeNodesRemoved(MyTreeNode, MyTreeNode[], int[], MyTreeNode[]) */
330 public void fireTreeNodesRemoved(Object source, TreePath path, int[] childIndices, T[] children) {
331 fireTreeNode(REMOVED, source, path, childIndices, children);
332 }
333
334 /** @see MyTreeTableModel#fireTreeStructureChanged(MyTreeNode, MyTreeNode[], int[], MyTreeNode[]) */
335 public void fireTreeStructureChanged(Object source, TreePath path, int[] childIndices, T[] children) {
336 fireTreeNode(STRUCTURE_CHANGED, source, path, childIndices, children);
337 }
338
339 // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
340 // Implementation of the TreeModel interface: can't override these methods by narrowing Object to
341 // MyHierarchicalFileData. So we must 'manually' manage the cast from T to MyHierarchicalFileData.
342 // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
343
344 /** @see TreeModel#getChild(Object, int) */
345 @SuppressWarnings("unchecked")
346 public final T getChild(Object parent, int index) {
347 return (T) ((T) parent).getChild(index);
348 }
349
350 /** @see TreeModel#getChildCount(Object) */
351 @SuppressWarnings("unchecked")
352 public final int getChildCount(Object child) {
353 return ((T) child).getChildCount();
354 }
355
356 /** @see TreeModel#isLeaf(Object) */
357 @SuppressWarnings("unchecked")
358 public final boolean isLeaf(Object node) {
359 return ((T) node).isLeaf();
360 }
361
362 /**
363 * This is normally not called
364 *
365 * @see TreeModel#valueForPathChanged(TreePath, Object)
366 */
367 public final void valueForPathChanged(TreePath path, Object newValue) {
368 uploadPolicy.displayErr("The method MyAbstractTreeTableModel.valueForPathChanged should not be called");
369 }
370
371 /**
372 * This is normally not called
373 *
374 * @see TreeModel#getIndexOfChild(Object, Object)
375 */
376 @SuppressWarnings("unchecked")
377 public final int getIndexOfChild(Object parent, Object child) {
378 return ((T) parent).getChildren().indexOf((T) child);
379 }
380
381 public abstract Object getValueAt(T nodeForRow, int column);
382
383 }