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