/** *######################################################################### * BrowseDisplay.java - part of the demo-client for Greenstone 3, of the * Greenstone digital library suite from the New Zealand Digital Library * Project at the * University of Waikato, New Zealand. *

* Copyright (C) 2008 New Zealand Digital Library Project *

* This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. *

* This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. *######################################################################## */ package org.greenstone.gs3client; import javax.swing.JPopupMenu; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JEditorPane; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JSplitPane; import javax.swing.JLabel; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.BorderLayout; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import java.net.MalformedURLException; import java.net.URL; import java.util.Vector; import java.util.HashMap; import javax.swing.JTree; import javax.swing.text.html.HTMLDocument; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeSelectionModel; import javax.swing.tree.DefaultTreeModel; import javax.swing.event.TreeWillExpandListener; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import org.apache.log4j.Logger; import org.greenstone.gs3client.data.BrowseResponseData; import org.greenstone.gs3client.data.DocumentNodeData; import org.greenstone.gs3client.data.ClassifierNodeData; import org.greenstone.gs3client.data.NodeData; import org.greenstone.gs3client.data.ParseUtil; import org.greenstone.gsdl3.util.GSXML; /** * The Browse panel inside the Java-client's tab pane that's labelled "Browse". * This panel contains a tree view for expanding classifiers and their documents. * It also contains an area where the metadata is displayed, and a text area where * the textual or image content of a selected documentNode is displayed. * @author ak19 */ public class BrowseDisplay extends JPanel implements TreeSelectionListener, TreeWillExpandListener, ColourCombo.ColourChangeable { /** The Logger for this class */ static Logger LOG = Logger.getLogger(SearchResultsDisplay.class); /** Access to the running instance of GS3JavaClient */ protected GS3JavaClient client; /** A HashMap to store the displayData for the Browse operation. Usually * just 2 elements long at most: displayName and displayDescription. */ protected HashMap displayData; /* GUI items of this Browse Panel */ protected JLabel browseLabel; protected JPanel browsePanel; protected JSplitPane splitViewPane, structureMetaView; protected JPanel browseBar; protected ClassifierButton[] classifierList; protected JTree browsingTree; protected JEditorPane htmlPane; protected JList metanames, metavalues; /** Context menu that pops up when users right click in the browse tree area */ protected JPopupMenu popup; /** Constructor that creates the Browse Panel and its internal GUI items. * @param client is the running instance of the client application through * which its methods can be accessed. */ public BrowseDisplay(GS3JavaClient client) { super(new BorderLayout()); this.client = client; displayData = new HashMap(2); //usual maxsize browseBar = new JPanel(); // default FlowLayout L to R, centre aligned classifierList = null; this.add(browseBar, BorderLayout.NORTH); browsingTree = new JTree(new DefaultMutableTreeNode(null)); browsingTree.getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION); browsingTree.addTreeSelectionListener(this); browsingTree.addTreeWillExpandListener(this); browsePanel = new JPanel(new BorderLayout()); // new FlowLayout(FlowLayout.LEFT)); // don't use this, // else view of tree is limited to the very minimum browsePanel.add(new JScrollPane(browsingTree), BorderLayout.CENTER); browseLabel = new JLabel("Browsing by..."); browsePanel.add(browseLabel, BorderLayout.NORTH); JPanel metadataPanel = new JPanel(new BorderLayout()); this.metanames = new JList(); this.metavalues = new JList(); metadataPanel.add(metanames, BorderLayout.WEST); metadataPanel.add(metavalues, BorderLayout.CENTER); JPanel metaSuperPanel = new JPanel(new BorderLayout()); metaSuperPanel.add(new JLabel("Metadata"), BorderLayout.NORTH); //metaSuperPanel.add(metadataPanel, BorderLayout.CENTER); metaSuperPanel.add( new JScrollPane(metadataPanel), BorderLayout.CENTER); structureMetaView = new JSplitPane(JSplitPane.VERTICAL_SPLIT); structureMetaView.setTopComponent(browsePanel); structureMetaView.setBottomComponent(new JScrollPane(metaSuperPanel)); structureMetaView.setOneTouchExpandable(true); htmlPane = new JEditorPane(); htmlPane.setEditable(false); htmlPane.setContentType("text/html"); // Add the docStructure and metadata panels next to each // other within a split pane splitViewPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitViewPane.setLeftComponent(structureMetaView); splitViewPane.setRightComponent(new JScrollPane(htmlPane)); splitViewPane.setOneTouchExpandable(true); this.add(splitViewPane, BorderLayout.CENTER); // add a (rightclick) popup menu to the browsing tree // (can only do this after tree and htmlPane have been instantiated): popup = new JPopupMenu(); browsingTree.setComponentPopupMenu(popup); browsingTree.addMouseListener(new Displays.PopupListener( popup, browsingTree, client, this.htmlPane)); } /** Clears the service-specific buttons in the browseBar and the * service-specific display-data. The rest of the GUI (split panes, panels) * remain as they are. */ public void clear() { // empty the docStructureTree's popup menu too popup.removeAll(); browseBar.removeAll(); displayData.clear(); htmlPane.setText(""); // empty any classification/document metadata final String[] empty = {}; BrowseDisplay.this.metanames.setListData(empty); BrowseDisplay.this.metavalues.setListData(empty); //metanames.removeAll(); // doesn't work //metavalues.removeAll(); // doesn't work // empty the tree: ((DefaultTreeModel)browsingTree.getModel()).setRoot(null); classifierList = null; System.gc(); this.validate(); } /** Changes the colour of the query form and its controls to the * current colours set in class ColourCombo. Specified by * the ColourCombo.ColourChangeable interface. */ public void changeUIColour() { Component[] comps = { this, this.browseBar, this.browsePanel, browsingTree, metanames, metavalues, popup }; ColourCombo.changeColor(comps); // ensures that the "metadata" JLabel (not an instance variable) // is coloured appropriately as well ColourCombo.changeAncestorColor(metanames); } /** For some reason, the overriden getPreferredSize() is not * called upon resize of this panel, so I am calling it manually * whenever there's a call to paint this Panel. Painting will * be done when the parent container is resized (and this panel * made visible) anyway, so it might as well work out what size * the interior panels will have on every resize. * @param g is the Graphics object * @see "JPanel's paint(Graphics g)" */ public void paint(java.awt.Graphics g) { super.paint(g); // let the usual JPanel paint event happen this.getPreferredSize(); } // FIX ME: The following problem was 'fixed' by overriding paint: // Strangely, this method is called only once! Even though it should // always be called on resize as indeed happens correctly in // SearchResultsDisplay.java. // Reason might be because components are arranged differently in the // JSplitPanes nested in this JPanel (they're arranged different from // SearchResultsDisplay.java) /** Overrode this method to resize the splitpanes within, upon resize. * It calculates the size of this panel, as well as setting those of * the splitpanes it contains based on the size of the parent container. * @return the preferred dimensions of this JPanel. */ public Dimension getPreferredSize() { Dimension size = this.getParent().getSize(); int x = (int)size.getWidth() / 3; // WHY DOES THIS NOT WORK????? int y = (int)(size.getHeight() / 5 * 3); // WHY DOES THIS NOT WORK????? structureMetaView.setDividerLocation(y); // 400 // structureMetaView.setDividerLocation(1.0); // as far down as // possible to give more space to browse structure and less to meta splitViewPane.setDividerLocation(x); // 300 splitViewPane.setPreferredSize(size); //System.err.println("SIZE: " + this.getSize() + "y: " + y); return size; } /** Will clear previous browse service's classification options and widgets, * and redisplay browse options as specified by the describe Response Message * XML returned from the browse Service. * @param describeRespMsgTag - the (Classifier)Browse Service's describe * response message element, used to reset the classifier buttons in the * browseBar of this panel. */ public void displayBrowseOptions(Element describeRespMsgTag){ this.clear(); // First set the displayItems Element service = ParseUtil.getFirstDescElementCalled( describeRespMsgTag, GSXML.SERVICE_ELEM); Vector displayItems = ParseUtil.getAllChildElementsCalled( service, GSXML.DISPLAY_TEXT_ELEM); if(displayItems != null) { for(int i = 0; i < displayItems.size(); i++) { Element e = (Element)displayItems.get(i); if(!e.hasAttribute(GSXML.NAME_ATT)) continue; // we are looking for value String name = e.getAttribute(GSXML.NAME_ATT); String value = ParseUtil.getBodyTextValue(e); this.displayData.put(name, value); } // get descr String descr = (String)displayData.get( GSXML.DISPLAY_TEXT_DESCRIPTION); if(descr != null) this.setBorder(BorderFactory.createTitledBorder(descr)); } // Now, process all items inside NodeList nl = describeRespMsgTag.getElementsByTagName( GSXML.CLASSIFIER_ELEM+GSXML.LIST_MODIFIER); // There will be only one, as far as I can tell from the describe // response returned from gs2mgppdemo's ClassifierBrowse service if(nl.getLength() <= 0) return; // nothing to do; but this should not happen Element classifierListTag = (Element)nl.item(0); // now get the s children from nl = classifierListTag.getElementsByTagName(GSXML.CLASSIFIER_ELEM); int size = nl.getLength(); if(size > 0) classifierList = new ClassifierButton[size]; for(int i = 0; i < size; i++) { Element classifier = (Element)nl.item(i); classifierList[i] = new ClassifierButton( new ClassifierData(classifier)); this.browseBar.add(classifierList[i]); } } /** Called to populate the browsing tree with browse data. Only the * data for the top-level classifier and its direct descendants are * retrieved. * @param browseResponseObj - stores the data of the response XML * message returned by the (Classifier)Browse service. * @param rootName - the title/name of the root classifier */ public void displayBrowseResults( BrowseResponseData browseResponseObj, String rootName) { browsingTree.removeAll(); DefaultTreeModel model = (DefaultTreeModel)browsingTree.getModel(); ClassifierNodeData rootClassNode = browseResponseObj.getRootClassifier(); rootClassNode.setTitle(rootName); DefaultMutableTreeNode newRoot = new DefaultMutableTreeNode(rootClassNode); NodeData[] childNodes = rootClassNode.getChildren(); // First set their titles: client.retrieveTitledStructureFor(rootClassNode); if(childNodes != null) { for(int i = 0; i < childNodes.length; i++) { DefaultMutableTreeNode child = new DefaultMutableTreeNode(childNodes[i]); newRoot.add(child); if(childNodes[i].hasChildren) child.add(new DefaultMutableTreeNode(null)); //browsingTree.setRootVisible(false); //No, because it // has a root: the root classifierNode } model.setRoot(newRoot); } } /** This method of the TreeWillExpandListener interface is called when * the user clicked on an expandable node in the browsingTree. * Since we are loading substructures in the tree lazily (i.e. loading * childnodes lazily) we have dummy/null childTreeNodes that make * expandable parentNodes look like folders (make the parents look * expandable). * Therefore, when the user clicks on an expandable node, we first check * whether we already have loaded the datastructure/subtree or whether * its child is a dummy (null). If a dummy (null child), then we work * out whether it's a classifierNodeData or a documentNodeData that the * expanding treeNode represents. Based on that, we retrieve the sub- * structure for the treeNode that's expanding. */ public void treeWillExpand(TreeExpansionEvent e) { DefaultMutableTreeNode node = null; // Don't use browsingTree.getLastSelectedPathComponent(). That // method doesn't deal with the case when someone clicked on the // expand/collapse knob next to folders. // Use e.getPath().getLastPathComponent() instead: node = (DefaultMutableTreeNode)e.getPath().getLastPathComponent(); if(node == null || node.isLeaf()) return; // leafnode, shouldn't happen: we're in Expansion method! Object nodeInfo = node.getUserObject(); // First check whether this expanding/folder node's child contains // a dummy object (null) or is already set: DefaultMutableTreeNode child = (DefaultMutableTreeNode)node.getFirstChild(); Object childNodeInfo = child.getUserObject(); // if the child is null (therefore not a NodeData object), it was a // dummy we added for delayed lazy-loading - so remove child if(childNodeInfo != null) { return; // we've already retrieved the structure for // this node, don't need to process it any further } // Child is null: not a NodeData object, so we change the dummy child. // This is done by populating the node's children with the actual // document node/classifier node structure: node.removeAllChildren(); // first remove the dummy child // now, construct the structure of the treenode that's expanding // based on whether it represents a classifierNodeData or // documentNodeData object: NodeData nodeData = (NodeData)nodeInfo; if(nodeData instanceof ClassifierNodeData) { ClassifierNodeData classNode = (ClassifierNodeData)nodeData; this.client.retrieveTitledStructureFor((classNode)); // The above will have set the expanding classNode's children. // Now, we add a treeNode for each child of classNode - // and for each childclassNode that's expandable, add a dummy // child for them to make them expandable: NodeData[] children = classNode.getChildren(); for(int i = 0; i < children.length; i++) { DefaultMutableTreeNode childTreeNode = new DefaultMutableTreeNode(children[i]); node.add(childTreeNode); // If the classifier node has children (expands further), make // it a folder by adding in a null child to allow lazy tree- // expansion as when required if(children[i].hasChildren) childTreeNode.add(new DefaultMutableTreeNode(null)); } } else { // if instanceof DocumentNodeData DocumentNodeData docNode = (DocumentNodeData)nodeData; if(docNode.nodeType.equals(GSXML.NODE_TYPE_ROOT)) { this.client.retrieveTitledStructureFor(docNode); Displays.createNodesForChildren(docNode, node); } //else { document can't be a leaf node *and* expand at the // same time. So shouldn't ever be here! // After all, this is the method that's called when a node // is expanding, and leaf nodes should not expand. //} } } /** Part of the TreeWillExpandListener interface. Nothing to do here. */ public void treeWillCollapse(TreeExpansionEvent e) {} /* THE FOLLOWING DOES NOT APPLY ANYMORE: * In this method, we are not dealing with expandable nodes * (we check for whether the clicked node is a leaf), but with document- * NodeData objects that are leaves: for these, we display the document. */ /** * Whenever an item is clicked in the browsingTree, this method is called. * We display the document associated with a documentNode that is clicked, * or "" for classifierNodes. */ public void valueChanged(TreeSelectionEvent e) { // remove any menuItems in the popup from the previously // selected docNode popup.removeAll(); DefaultMutableTreeNode node = null; node = (DefaultMutableTreeNode) browsingTree.getLastSelectedPathComponent(); if(node == null) return; Object nodeInfo = node.getUserObject(); // after construction there's nothing in the trees, so check for that: if(nodeInfo == null) return; // We need to change to a Wait cursor while we load the documentNode Container c = client.getContentPane(); c.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); if(nodeInfo instanceof DocumentNodeData) { //whether leaf or folder, the document element may contain text DocumentNodeData docNode = (DocumentNodeData)nodeInfo; client.retrieveContentFor(docNode); // for docNodes, there's more than title metadata, so ensure // all metadata has been retrieved client.retrieveAllMetadataFor(docNode); if(docNode.hasNoText()) { //NoText field is 1, meaning it's an img // now display the image this.htmlPane.setText( Displays.getImgUrlEnclosedInHtml(docNode.getImgURL())); } else { // it has text // Java's htmlpane does not recognise justified alignment. // It displays them all centred. // So here we replace all ALIGN="JUSTIFY" with ALIGN="LEFT" // (we don't do this in the DocumentNodeData class itself, // because our nodeContent should not be transformed: it will // display properly in browsers and other html displays). //System.err.println("docNode can not be an image"); String docContent = docNode.getContent().replaceAll( "ALIGN=\"JUSTIFY\"", "ALIGN=\"LEFT\""); String baseURL = client.getBaseURL(); // TODO: make the docNode itself work out its URL by passing // baseURL to the docNode? if(!baseURL.equals("") && docNode.getRoot() != null) { // "" is the case where dlAPIA = gs3 // where the _httpdocimg_ macro will deal with // resolving the relative urls into their full ones // We don't want to set the base and meddle with macro // for those cases. // For Fedora's case: HTMLDocument doc = (HTMLDocument)this.htmlPane.getDocument(); try{ URL url = new URL(baseURL+docNode.getRoot().nodeID+"/"); //System.err.println("url: " + url.toString()); doc.setBase(url); //docContent = docContent.replaceAll("_httpdocimg_/", ""); LOG.debug(docContent); }catch(MalformedURLException mex) { ; //nothing to be done, leave the base as it is } } this.htmlPane.setText(docContent); } this.htmlPane.setCaretPosition(0); } else // treenode is a ClassifierNodeData, clear the html Pane this.htmlPane.setText(""); // In any case -- whatever kind of nodedata it may be -- we display // the metadata for this NodeData: Displays.showMeta((NodeData)nodeInfo, metanames, metavalues); c.setCursor(Cursor.getDefaultCursor()); // set the cursor back to normal } /** Inner class (not static, as it needs access to outerclass' this object. * This class represents a button that encapsulates a ClassifierData * object and sets its own name and tooltip text based on the displayName * and displayDescription of that ClassifierData object. */ public class ClassifierButton extends JButton implements ActionListener { /** Encapsulated classifierData obj */ public ClassifierData classifier; /** Constructor that creates a button to visually represent * the classifier. * @param classifier - the ClassifierData object for which to * create a button, using its display data. */ public ClassifierButton(ClassifierData classifier) { super(classifier.displayName); this.classifier = classifier; this.setToolTipText(classifier.displayDescription); this.addActionListener(this); } /** Called when someone presses the ClassifierButton: when pressed, * perform the browse request associated with the classifier. */ public void actionPerformed(ActionEvent e) { Container c = client.getContentPane(); c.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); // empty the previous classification's/document's metadata // and any text in the html pane final String[] empty = {}; BrowseDisplay.this.metanames.setListData(empty); BrowseDisplay.this.metavalues.setListData(empty); BrowseDisplay.this.htmlPane.setText(""); // Now use the outerclass' MessagerClient object to perform the // browse request: BrowseDisplay.this.client.doBrowse(this.classifier); BrowseDisplay.this.browseLabel.setText("Browsing by " + classifier.displayName); c.setCursor(Cursor.getDefaultCursor()); } } /** Static inner class that represents the data in a <classifier> * element - itself nested inside a list (<classifierList>) of them. * These elements are to be found in the response returned for a describe * request sent to a collection's BrowseService. */ public static class ClassifierData { /** The content attribute of the <classifier> */ public final String content; /** The name attribute of the <classifier> */ public final String name; /** The display name attribute of the <classifier> */ public final String displayName; /** The description attribute of the <classifier> */ public final String displayDescription; /** Constructor. * @param classifierTag - creates a ClassifierData object from the * data stored in a <classifier> element */ public ClassifierData(Element classifierTag) { content = classifierTag.hasAttribute(GSXML.CLASSIFIER_CONTENT_ATT) ? classifierTag.getAttribute(GSXML.CLASSIFIER_CONTENT_ATT) : ""; name = classifierTag.hasAttribute(GSXML.NAME_ATT) ? classifierTag.getAttribute(GSXML.NAME_ATT) : ""; // now get value // and descr HashMap displayData = new HashMap(2); //size = 2, because // generally expecting only display Name and Description NodeList nl = classifierTag.getElementsByTagName( GSXML.DISPLAY_TEXT_ELEM); for(int i = 0; i < nl.getLength(); i++) { Element displayItem = (Element)nl.item(0); if(displayItem.hasAttribute(GSXML.NAME_ATT)) { String nameAtt = displayItem.getAttribute(GSXML.NAME_ATT); String value = ParseUtil.getBodyTextValue(displayItem); displayData.put(nameAtt, value); } } String dName = (String)displayData.get(GSXML.DISPLAY_TEXT_NAME); String dDescr = (String)displayData.get( GSXML.DISPLAY_TEXT_DESCRIPTION); this.displayName = (dName == null) ? "" : dName; this.displayDescription = (dDescr == null) ? "" : dDescr; // Can get rid of HashMap now: displayData.clear(); displayData = null; } /** The displayName can be used as a label in any widget. * @return the displayName of this ClassifierData object */ public String toString() { return this.displayName; } /** @return a display String with the data stored in this ClassifierData * object. Useful for debugging purposes. */ public String show() { StringBuffer buf = new StringBuffer(this.name); buf.append(" "); buf.append(this.content); buf.append(" "); buf.append(this.displayName); buf.append(" "); buf.append(this.displayDescription); return buf.toString(); } } }