001    /*
002     Copyright (C) 2003 Adam Olsen
003    
004     This program is free software; you can redistribute it and/or modify
005     it under the terms of the GNU General Public License as published by
006     the Free Software Foundation; either version 1, or (at your option)
007     any later version.
008    
009     This program is distributed in the hope that it will be useful,
010     but WITHOUT ANY WARRANTY; without even the implied warranty of
011     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     GNU General Public License for more details.
013    
014     You should have received a copy of the GNU General Public License
015     along with this program; if not, write to the Free Software
016     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
017     */
018    package com.valhalla.jbother;
019    
020    import com.valhalla.gui.*;
021    import org.jivesoftware.smack.PacketCollector;
022    import org.jivesoftware.smack.SmackConfiguration;
023    import org.jivesoftware.smack.filter.AndFilter;
024    import org.jivesoftware.smack.filter.PacketFilter;
025    import org.jivesoftware.smack.filter.PacketIDFilter;
026    import org.jivesoftware.smack.filter.PacketTypeFilter;
027    import org.jivesoftware.smack.packet.IQ;
028    import org.jivesoftware.smack.packet.Packet;
029    import com.valhalla.jbother.jabber.smack.*;
030    
031    import javax.swing.*;
032    import javax.swing.event.ListSelectionEvent;
033    import javax.swing.event.ListSelectionListener;
034    import javax.swing.table.AbstractTableModel;
035    import java.awt.*;
036    import java.awt.event.ActionEvent;
037    import java.awt.event.ActionListener;
038    import java.util.*;
039    
040    /**
041     * dialog used in searching using Jabber Search (eg. Jabber User Directories)
042     * exploits JEP-0055's 'jabber:iq:search' namespace
043     * handles dynamic number of search fields
044     *
045     * @author     Lukasz Wiechec
046     * @created    Apr 1, 2005 1:36:43 PM
047     */
048    
049    public class SearchDialog extends JDialog implements WaitDialogListener
050    {
051        private static String fId = "$Id$";
052    
053        private String service;
054        private ResourceBundle resources = ResourceBundle.getBundle("JBotherBundle", Locale.getDefault());
055    
056        // this will hold the mappings: <searchFieldName> -> textfield that contains it
057        private HashMap searchTextFields = new HashMap();
058    
059        // all data for displaying the inside of this dialog is taken from here:
060        private Search search;
061    
062        private JPanel container = new JPanel();
063        private JPanel inputPanel = new JPanel();
064        private JPanel resultsPanel = new JPanel();
065        private JButton closeButton = new JButton("Close");
066        private JTextArea instructionsArea = new JTextArea();
067        private JButton searchButton = new JButton("Search");
068        private JButton stopSearchButton = new JButton("Stop");
069        private JTable resultsTable = new JTable();
070        private SearchResultsTableModel resultsTableModel = new SearchResultsTableModel();
071        private JButton addContactButton = new JButton("Add contact");
072        private JButton userInfoButton = new JButton("User info");
073        private WaitDialog wait = new WaitDialog(this,null,"Gathering search information...");
074        // create panel with search fields
075        private JPanel searchFieldsPanel = new JPanel();
076        private GridBagLayout gridbag = new GridBagLayout();
077        private GridBagConstraints c = new GridBagConstraints();
078    
079    
080        private int selectedRow = -1;
081        private boolean cancelled = false;
082    
083        /**
084         *Constructor for the SearchDialog object
085         *
086         * @param  service                Description of the Parameter
087         * @exception  HeadlessException  Description of the Exception
088         */
089        public SearchDialog(String service)
090            throws HeadlessException
091        {
092            super(BuddyList.getInstance().getContainerFrame(), "Search", true);
093            setModal(false);
094            setTitle(resources.getString("search") + ": " + service);
095            this.service = service;
096            wait.setWaitListener(this);
097    
098            getRootPane().setDefaultButton(searchButton);
099    
100            instructionsArea.setLineWrap(true);
101            instructionsArea.setWrapStyleWord(true);
102            initialize();
103            DialogTracker.addDialog(this, false, true);
104            collectInformation();
105    
106        }
107    
108        public void cancel() { this.cancelled = true; dispose(); }
109    
110        public void collectInformation()
111        {
112            wait.setVisible(true);
113    
114            new Thread(new QuerySearch()).start();
115        }
116    
117    
118        /**
119         *  Description of the Class
120         *
121         * @author     synic
122         * @created    April 14, 2005
123         */
124        class QuerySearch implements Runnable
125        {
126            /**
127             *  Main processing method for the QuerySearch object
128             */
129            public void run()
130            {
131                if(querySearchService(service) == true)
132                {
133                    if( cancelled ) { return; }
134                    SwingUtilities.invokeLater(
135                        new Runnable()
136                        {
137                            public void run()
138                            {
139                                instructionsArea.setText(search.getInstructions());
140                                Iterator iSearchFields = search.getSearchFields().keySet().iterator();
141                                while(iSearchFields.hasNext())
142                                {
143                                    String searchFieldName = (String) iSearchFields.next();
144                                    JLabel searchFieldNameLabel = new JLabel(capitalizeString(searchFieldName) + ": ");
145                                    gridbag.setConstraints(searchFieldNameLabel, c);
146                                    searchFieldsPanel.add(searchFieldNameLabel, c);
147                                    c.gridx++;
148                                    c.gridwidth = GridBagConstraints.REMAINDER;
149                                    c.weightx = 1.0;
150                                    c.fill = GridBagConstraints.HORIZONTAL;
151                                    JTextField inputSearchField = new JTextField();
152                                    inputSearchField.setColumns(30);
153                                    searchTextFields.put(searchFieldName, inputSearchField);
154                                    gridbag.setConstraints(inputSearchField, c);
155                                    searchFieldsPanel.add(inputSearchField, c);
156                                    c.gridx = 0;
157                                    c.gridwidth = 1;
158                                    c.weightx = 0;
159                                    c.fill = GridBagConstraints.NONE;
160                                    c.gridy++;
161                                }
162    
163                                searchFieldsPanel.validate();
164                                searchFieldsPanel.repaint();
165                                wait.setVisible(false);
166                                wait = new WaitDialog(SearchDialog.this,null,"Searching...");
167                                setVisible(true);
168                            }
169                        });
170                }
171            }
172        }
173    
174        /**
175         * initialize the frame
176         */
177        private void initialize()
178        {
179            inputPanel = initializeInputPanel();
180            resultsPanel = initializeResultsPanel();
181            initializeListeners();
182            container.setLayout(new BorderLayout());
183            JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, inputPanel, resultsPanel);
184            splitPane.setDividerLocation(200);
185            container.add(splitPane, BorderLayout.CENTER);
186            JPanel bottomButtonPanel = new JPanel(new BorderLayout());
187            bottomButtonPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 10));
188            bottomButtonPanel.add(closeButton, BorderLayout.EAST);
189            container.add(bottomButtonPanel, BorderLayout.SOUTH);
190            addContactButton.setEnabled(false);
191            userInfoButton.setEnabled(false);
192            setContentPane(container);
193            pack();
194            setLocationRelativeTo(null);
195        }
196    
197        /**
198         * @return    JPanel containing instructions, search fields and start/stop searching buttons
199         */
200        private JPanel initializeInputPanel()
201        {
202            JPanel panel = new JPanel();
203            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
204            panel.setBorder(BorderFactory.createTitledBorder(resources.getString("searchFields")));
205    
206            // add text area with instructions
207            instructionsArea.setEditable(false);
208            instructionsArea.setBorder(BorderFactory.createEtchedBorder());
209            instructionsArea.setPreferredSize(new Dimension(300, 200));
210            panel.add(instructionsArea);
211            panel.add(Box.createVerticalStrut(5));
212    
213            searchFieldsPanel.setLayout(gridbag);
214            c.gridx = 0;
215            c.gridy = 0;
216            c.gridwidth = 1;
217            c.gridheight = 1;
218            c.weightx = 0.0;
219            c.weighty = 0.0;
220            c.anchor = GridBagConstraints.NORTHWEST;
221    
222            panel.add(searchFieldsPanel);
223            panel.add(Box.createVerticalStrut(5));
224    
225            // create a panel with search/stop search buttons
226            JPanel buttonsPanel = new JPanel();
227            buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
228            buttonsPanel.add(Box.createHorizontalGlue());
229            buttonsPanel.add(searchButton);
230            buttonsPanel.add(Box.createHorizontalStrut(10));
231            buttonsPanel.add(stopSearchButton);
232            buttonsPanel.add(Box.createHorizontalGlue());
233            panel.add(buttonsPanel);
234            panel.add(Box.createVerticalGlue());
235    
236            JPanel ueberPanel = new JPanel(new BorderLayout());
237            ueberPanel.add(panel, BorderLayout.NORTH);
238    
239            return ueberPanel;
240        }
241    
242        /**
243         * @return    panel containing table with search results
244         */
245        private JPanel initializeResultsPanel()
246        {
247            JPanel panel = new JPanel(new BorderLayout());
248            panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
249            // table
250            resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
251            ListSelectionModel rowSL = resultsTable.getSelectionModel();
252            rowSL.addListSelectionListener(
253                new ListSelectionListener()
254                {
255                    public void valueChanged(ListSelectionEvent e)
256                    {
257                        // ignora extra values
258                        if(e.getValueIsAdjusting())
259                        {
260                            return;
261                        }
262    
263                        ListSelectionModel lsm = (ListSelectionModel) e.getSource();
264                        if(lsm.isSelectionEmpty())
265                        {
266                            // no rows selected
267                            selectedRow = -1;
268                            addContactButton.setEnabled(false);
269                            userInfoButton.setEnabled(false);
270                        }
271                        else
272                        {
273                            addContactButton.setEnabled(true);
274                            userInfoButton.setEnabled(true);
275                            selectedRow = lsm.getMinSelectionIndex();
276                        }
277                    }
278                });
279            resultsTable.setModel(resultsTableModel);
280            JScrollPane scrollPane = new JScrollPane(resultsTable);
281            scrollPane.setBorder(BorderFactory.createTitledBorder("Search results"));
282            panel.add(scrollPane, BorderLayout.CENTER);
283            // buttons
284            JPanel buttonPanel = new JPanel();
285            buttonPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
286            buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
287            buttonPanel.add(Box.createHorizontalGlue());
288            buttonPanel.add(addContactButton);
289            buttonPanel.add(Box.createHorizontalStrut(5));
290            buttonPanel.add(userInfoButton);
291            panel.add(buttonPanel, BorderLayout.SOUTH);
292    
293            return panel;
294        }
295    
296        class SearchThread implements Runnable
297        {
298            public void run()
299            {
300                doSearch();
301            }
302        }
303    
304        /**
305         * sets up action listeners for buttons
306         */
307        private void initializeListeners()
308        {
309            closeButton.addActionListener(
310                new ActionListener()
311                {
312                    public void actionPerformed(ActionEvent e)
313                    {
314                        dispose();
315                    }
316                });
317    
318            searchButton.addActionListener(
319                new ActionListener()
320                {
321                    public void actionPerformed(ActionEvent e)
322                    {
323                        wait.setVisible( true );
324                        new Thread( new SearchThread()).start();
325                    }
326                });
327    
328            stopSearchButton.addActionListener(
329                new ActionListener()
330                {
331                    public void actionPerformed(ActionEvent e)
332                    {
333    
334                    }
335                });
336            addContactButton.addActionListener(
337                new ActionListener()
338                {
339                    public void actionPerformed(ActionEvent e)
340                    {
341                        AddBuddyDialog addBuddyDialog = new AddBuddyDialog();
342                        addBuddyDialog.setBuddyId(
343                            ((Search.Item) resultsTableModel.getItems().get(selectedRow)).getJid()
344                            );
345                        addBuddyDialog.setVisible(true);
346                    }
347                });
348            userInfoButton.addActionListener(
349                new ActionListener()
350                {
351                    public void actionPerformed(ActionEvent e)
352                    {
353                        new InformationViewerDialog(
354                            ((Search.Item) resultsTableModel.getItems().get(selectedRow)).getJid());
355                    }
356                });
357    
358        }
359    
360        /**
361         *  Gets the search attribute of the SearchDialog object
362         *
363         * @return    The search value
364         */
365        public Search getSearch()
366        {
367            return search;
368        }
369    
370        /**
371         *  Sets the search attribute of the SearchDialog object
372         *
373         * @param  search  The new search value
374         */
375        public void setSearch(Search search)
376        {
377            this.search = search;
378        }
379    
380        /**
381         * return string with first letter in uppercase
382         *
383         * @param  aStr  string to capitalize
384         * @return       capitalized string
385         */
386        private String capitalizeString(String aStr)
387        {
388            return aStr.substring(0, 1).toUpperCase() + aStr.substring(1);
389        }
390    
391        /**
392         * send the empty "jabber:iq:search" query request to the service in order to receive
393         * the list of search field and instructions
394         * also the results table can be initialized, as now we know the columns
395         *
396         * @param  service  to query
397         * @return          true if service provides Jabber search service
398         */
399        private boolean querySearchService(String service)
400        {
401            boolean returnVal = false;
402            Search searchQuery = new Search();
403            searchQuery.setTo(service);
404            searchQuery.setType(IQ.Type.GET);
405    
406            sendPacket(searchQuery);
407    
408            PacketFilter filter = new AndFilter(new PacketIDFilter(searchQuery.getPacketID()),
409                new PacketTypeFilter(IQ.class));
410    
411            PacketCollector packetCollector =
412                BuddyList.getInstance().getConnection().createPacketCollector(filter);
413    
414            IQ reply = (IQ) packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
415            if(reply == null)
416            {
417                SwingUtilities.invokeLater( new Runnable()
418                {
419                    public void run()
420                    {
421                        JOptionPane.showMessageDialog(SearchDialog.this,
422                            resources.getString("searchRequestTimeout"),
423                            resources.getString("searchError"),
424                            JOptionPane.OK_OPTION);
425                    }
426                } );
427                returnVal = false;
428            }
429            else
430            {
431                if(reply.getType() == IQ.Type.ERROR)
432                {
433                    SwingUtilities.invokeLater( new Runnable()
434                    {
435                        public void run()
436                        {
437                            JOptionPane.showMessageDialog(SearchDialog.this,
438                                resources.getString("serviceDoesNotSupportJaberSearch"),
439                                resources.getString("searchError"),
440                                JOptionPane.OK_OPTION
441                                );
442                            wait.setVisible(false);
443                        }
444                    });
445    
446                    returnVal = false;
447                }
448                else if(reply instanceof Search)
449                {
450                    search = (Search) reply;
451                    returnVal = true;
452                }
453            }
454            packetCollector.cancel();
455            return returnVal;
456        }
457    
458        /**
459         * "Search" button handler
460         * todo: maybe it would be wise to put his code in separate thread?
461         */
462        private void doSearch()
463        {
464            if( cancelled ) { return; }
465            Search srch = new Search();
466            srch.setTo(service);
467            srch.setType(IQ.Type.SET);
468            HashMap searchCriteria = new HashMap();
469            // get the input from all the fields, build Search message and send it
470            Iterator iSearchFields = searchTextFields.keySet().iterator();
471            while(iSearchFields.hasNext())
472            {
473                String searchFieldName = (String) iSearchFields.next();
474                String searchFieldValue = ((JTextField) searchTextFields.get(searchFieldName)).getText();
475                searchCriteria.put(searchFieldName, searchFieldValue);
476            }
477            srch.setSearchFields(searchCriteria);
478    
479            sendPacket(srch);
480    
481            PacketFilter filter = new AndFilter(new PacketIDFilter(srch.getPacketID()),
482                new PacketTypeFilter(IQ.class));
483            PacketCollector packetCollector =
484                BuddyList.getInstance().getConnection().createPacketCollector(filter);
485    
486            final IQ reply = (IQ) packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
487            if(reply == null)
488            {
489                SwingUtilities.invokeLater( new Runnable()
490                {
491                    public void run()
492                    {
493                        wait.setVisible( false );
494                        JOptionPane.showMessageDialog(SearchDialog.this,
495                            resources.getString("searchRequestTimeout"),
496                            resources.getString("searchError"),
497                            JOptionPane.OK_OPTION);
498                    }
499                });
500            }
501            else
502            {
503                SwingUtilities.invokeLater(new Runnable()
504                {
505                    public void run()
506                    {
507                        wait.setVisible( false );
508                       if( cancelled ) { return; }
509    
510                        // update results table with new data
511                        resultsTableModel.setItems(((Search) reply).getItems());
512                        resultsTableModel.fireTableStructureChanged();
513                        resultsTableModel.fireTableDataChanged();
514                    }
515                });
516            }
517            packetCollector.cancel();
518        }
519    
520        /**
521         *  Description of the Method
522         *
523         * @param  packet  Description of the Parameter
524         */
525        private void sendPacket(Packet packet)
526        {
527            if(BuddyList.getInstance().getConnection().isConnected())
528            {
529                BuddyList.getInstance().getConnection().sendPacket(packet);
530            }
531        }
532    
533        /**
534         * table model for the results of the search
535         * the number of columns will be know *after* querying the JUD. This
536         *
537         * @author     synic
538         * @created    April 14, 2005
539         */
540        class SearchResultsTableModel extends AbstractTableModel
541        {
542            // list of Search.Items
543            private ArrayList items = new ArrayList();
544    
545    
546            /**
547             *  Sets the items attribute of the SearchResultsTableModel object
548             *
549             * @param  aItems  The new items value
550             */
551            private void setItems(ArrayList aItems)
552            {
553                items = aItems;
554            }
555    
556            /**
557             *  Gets the items attribute of the SearchResultsTableModel object
558             *
559             * @return    The items value
560             */
561            public ArrayList getItems()
562            {
563                return items;
564            }
565    
566            /**
567             *  Gets the rowCount attribute of the SearchResultsTableModel object
568             *
569             * @return    The rowCount value
570             */
571            public int getRowCount()
572            {
573                if(items == null)
574                {
575                    return 0;
576                }
577                return items.size();
578            }
579    
580            /**
581             *  Gets the columnCount attribute of the SearchResultsTableModel object
582             *
583             * @return    The columnCount value
584             */
585            public int getColumnCount()
586            {
587    
588                if(items == null || items.size() == 0)
589                {
590                    return 0;
591                }
592                else
593                {
594                    return ((Search.Item) items.get(0)).getAttributes().size() + 1;
595                }
596            }
597    
598            /**
599             *  Gets the valueAt attribute of the SearchResultsTableModel object
600             *
601             * @param  rowIndex     Description of the Parameter
602             * @param  columnIndex  Description of the Parameter
603             * @return              The valueAt value
604             */
605            public Object getValueAt(int rowIndex, int columnIndex)
606            {
607                String out = "";
608                Search.Item item = (Search.Item) items.get(rowIndex);
609                if(columnIndex == item.getAttributes().size())
610                {
611                    out = item.getJid();
612                }
613                else
614                {
615                    Object[] keys = item.getAttributes().keySet().toArray();
616                    out = (String) item.getAttributes().get(keys[columnIndex]);
617                }
618                return out;
619            }
620    
621            /**
622             *  Gets the columnName attribute of the SearchResultsTableModel object
623             *
624             * @param  col  Description of the Parameter
625             * @return      The columnName value
626             */
627            public String getColumnName(int col)
628            {
629                if(col == search.getSearchFields().keySet().size())
630                {
631                    return "JID";
632                }
633                else
634                {
635                    Object[] searchFields = search.getSearchFields().keySet().toArray();
636                    return capitalizeString((String) searchFields[col]);
637                }
638            }
639        }
640    }
641