/*
Copyright 2008-2012 Josh Drummond
This file is part of WebPasswordSafe.
WebPasswordSafe 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.
WebPasswordSafe 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.
You should have received a copy of the GNU General Public License
along with WebPasswordSafe; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.webpasswordsafe.client.ui;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.webpasswordsafe.client.WebPasswordSafe;
import net.webpasswordsafe.client.i18n.TextMessages;
import net.webpasswordsafe.client.remote.PasswordService;
import net.webpasswordsafe.common.model.Password;
import net.webpasswordsafe.common.model.Tag;
import net.webpasswordsafe.common.util.Constants;
import net.webpasswordsafe.common.util.Constants.Match;
import net.webpasswordsafe.common.util.Utils;
import com.extjs.gxt.ui.client.Style.LayoutRegion;
import com.extjs.gxt.ui.client.Style.Orientation;
import com.extjs.gxt.ui.client.Style.Scroll;
import com.extjs.gxt.ui.client.Style.SelectionMode;
import com.extjs.gxt.ui.client.data.BaseModel;
import com.extjs.gxt.ui.client.data.BaseTreeModel;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.KeyListener;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.event.TreePanelEvent;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.util.Format;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.util.Padding;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Dialog;
import com.extjs.gxt.ui.client.widget.Info;
import com.extjs.gxt.ui.client.widget.MessageBox;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.CheckBox;
import com.extjs.gxt.ui.client.widget.form.Radio;
import com.extjs.gxt.ui.client.widget.form.RadioGroup;
import com.extjs.gxt.ui.client.widget.form.TextField;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.extjs.gxt.ui.client.widget.layout.BorderLayout;
import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData;
import com.extjs.gxt.ui.client.widget.layout.FillLayout;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayout;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayoutData;
import com.extjs.gxt.ui.client.widget.layout.BoxLayout.BoxLayoutPack;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayout.HBoxLayoutAlign;
import com.extjs.gxt.ui.client.widget.layout.RowData;
import com.extjs.gxt.ui.client.widget.layout.RowLayout;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanel;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanel.CheckNodes;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanel.CheckCascade;
/**
* @author Josh Drummond
*
*/
public class PasswordSearchPanel extends ContentPanel implements TagLoadListener
{
private final static TextMessages textMessages = GWT.create(TextMessages.class);
private Grid<PasswordSearchData> passwordGrid;
private ListStore<PasswordSearchData> gridStore;
private TreeStore<TagData> treeStore;
private TreePanel<TagData> tagTree;
private RadioGroup tagMatchRG;
private Radio radioOR, radioAND;
private TextField<String> searchTextBox;
private CheckBox activeOnlyCheckBox;
//private static final String TOOLTIP_VIEW_PASSWORD_VALUE = "Click to view current password value.";
//private static final String TOOLTIP_EDIT_PASSWORD = "Click to edit password and/or permissions.";
public PasswordSearchPanel()
{
setLayout(new BorderLayout());
setHeaderVisible(false);
ContentPanel northPanel = new ContentPanel();
northPanel.setHeading(textMessages.passwordSearch());
ContentPanel westPanel = new ContentPanel(new FillLayout());
westPanel.setHeading(textMessages.tags());
ContentPanel centerPanel = new ContentPanel(new FillLayout());
centerPanel.setHeading(textMessages.passwords());
HBoxLayout northLayout = new HBoxLayout();
northLayout.setPadding(new Padding(5));
northLayout.setHBoxLayoutAlign(HBoxLayoutAlign.MIDDLE);
northLayout.setPack(BoxLayoutPack.CENTER);
northPanel.setLayout(northLayout);
searchTextBox = new TextField<String>();
searchTextBox.addKeyListener(new KeyListener() {
@Override
public void componentKeyPress(ComponentEvent event) {
if (event.getKeyCode() == KeyCodes.KEY_ENTER)
{
doSearch();
}
}
});
searchTextBox.setWidth(300);
searchTextBox.setMaxLength(1000);
searchTextBox.focus();
Button searchButton = new Button(textMessages.search(), new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
doSearch();
}
});
activeOnlyCheckBox = new CheckBox();
activeOnlyCheckBox.setBoxLabel(textMessages.activeOnly());
activeOnlyCheckBox.setValue(true);
northPanel.add(searchTextBox, new HBoxLayoutData(new Margins(0, 5, 0, 0)));
northPanel.add(searchButton, new HBoxLayoutData(new Margins(0, 5, 0, 0)));
northPanel.add(activeOnlyCheckBox, new HBoxLayoutData(new Margins(0, 5, 0, 5)));
treeStore = new TreeStore<TagData>();
tagTree = new TreePanel<TagData>(treeStore);
tagTree.setBorders(true);
tagTree.sinkEvents(Event.ONDBLCLICK);
tagTree.addListener(Events.OnDoubleClick, new Listener<TreePanelEvent<TagData>>() {
public void handleEvent(TreePanelEvent<TagData> be)
{
TagData clickedTag = be.getItem();
List<TagData> checkedTags = tagTree.getCheckedSelection();
for (TagData checkedTag : checkedTags)
{
tagTree.setChecked(checkedTag, false);
}
tagTree.setChecked(clickedTag, true);
doSearch();
}
});
tagTree.setCheckStyle(CheckCascade.NONE);
tagTree.setCheckNodes(CheckNodes.LEAF);
tagTree.setCheckable(true);
tagTree.setDisplayProperty(Constants.NAME);
tagTree.setWidth(250);
centerPanel.setScrollMode(Scroll.AUTOX);
List<ColumnConfig> configs = new ArrayList<ColumnConfig>(4);
ColumnConfig column = new ColumnConfig();
//column.setToolTip(TOOLTIP_EDIT_PASSWORD);
column.setId(Constants.TITLE);
column.setHeader(textMessages.title());
column.setWidth(200);
configs.add(column);
column = new ColumnConfig();
column.setId(Constants.USERNAME);
column.setHeader(textMessages.username());
column.setWidth(100);
configs.add(column);
column = new ColumnConfig();
//column.setToolTip(TOOLTIP_VIEW_PASSWORD_VALUE);
column.setId(Constants.PASSWORD);
column.setHeader(textMessages.password());
column.setWidth(100);
configs.add(column);
column = new ColumnConfig();
column.setId(Constants.TAGS);
column.setHeader(textMessages.tags());
column.setWidth(200);
configs.add(column);
column = new ColumnConfig();
column.setId(Constants.NOTES);
column.setHeader(textMessages.notes());
column.setWidth(300);
configs.add(column);
gridStore = new ListStore<PasswordSearchData>();
ColumnModel cm = new ColumnModel(configs);
passwordGrid = new Grid<PasswordSearchData>(gridStore, cm);
passwordGrid.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
passwordGrid.setStyleAttribute("borderTop", "none");
passwordGrid.setBorders(true);
passwordGrid.setStripeRows(true);
passwordGrid.addListener(Events.CellDoubleClick, new Listener<GridEvent<PasswordSearchData>>()
{
@Override
public void handleEvent(GridEvent<PasswordSearchData> ge)
{
if (2 == ge.getColIndex())
{
doShowPasswordPopup((Long)ge.getModel().get(Constants.ID));
}
else
{
doLoadPasswordDialog((Long)ge.getModel().get(Constants.ID));
}
}
});
centerPanel.add(passwordGrid);
BorderLayoutData northData = new BorderLayoutData(LayoutRegion.NORTH, 100);
northData.setCollapsible(true);
northData.setFloatable(false);
northData.setHideCollapseTool(false);
northData.setSplit(true);
northData.setMargins(new Margins(5, 5, 0, 5));
BorderLayoutData westData = new BorderLayoutData(LayoutRegion.WEST, 150);
westData.setSplit(true);
westData.setCollapsible(true);
westData.setMargins(new Margins(5));
radioOR = new Radio();
radioOR.setBoxLabel(textMessages.or());
radioOR.setValue(true);
radioAND = new Radio();
radioAND.setBoxLabel(textMessages.and());
tagMatchRG = new RadioGroup();
tagMatchRG.setBorders(true);
tagMatchRG.add(radioOR);
tagMatchRG.add(radioAND);
westPanel.setLayout(new RowLayout(Orientation.VERTICAL));
westPanel.add(tagTree, new RowData(1, 1));
westPanel.add(tagMatchRG, new RowData(1, -1));
BorderLayoutData centerData = new BorderLayoutData(LayoutRegion.CENTER);
centerData.setMargins(new Margins(5, 0, 5, 0));
add(northPanel, northData);
add(westPanel, westData);
add(centerPanel, centerData);
doLoadTags();
}
public void openSelectedPassword()
{
PasswordSearchData p = passwordGrid.getSelectionModel().getSelectedItem();
if (null != p)
{
doLoadPasswordDialog((Long)p.get(Constants.ID));
}
}
public void getSelectedCurrentPasswordData()
{
PasswordSearchData p = passwordGrid.getSelectionModel().getSelectedItem();
if (null != p)
{
doShowPasswordPopup((Long)p.get(Constants.ID));
}
}
private void doLoadTags()
{
AsyncCallback<List<Tag>> callback = new AsyncCallback<List<Tag>>()
{
@Override
public void onFailure(Throwable caught)
{
WebPasswordSafe.handleServerFailure(caught);
}
@Override
public void onSuccess(List<Tag> result)
{
refreshTags(result);
}
};
PasswordService.Util.getInstance().getAvailableTags(callback);
}
private void doLoadPasswordDialog(long passwordId)
{
AsyncCallback<Password> callback = new AsyncCallback<Password>()
{
@Override
public void onFailure(Throwable caught)
{
WebPasswordSafe.handleServerFailure(caught);
}
@Override
public void onSuccess(Password result)
{
if (null != result)
{
openPasswordDialog(result);
}
else
{
MessageBox.alert(textMessages.error(), textMessages.noAccessPasswordRead(), null);
}
}
};
PasswordService.Util.getInstance().getPassword(passwordId, callback);
}
private void openPasswordDialog(Password password)
{
new PasswordDialog(password, this).show();
}
private void refreshTags(List<Tag> tags)
{
treeStore.removeAll();
for (Tag tag : tags)
{
treeStore.add(new TagData(tag), true);
}
}
private void doSearch()
{
List<TagData> selectedTagData = tagTree.getCheckedSelection();
Set<Tag> selectedTags = new HashSet<Tag>(selectedTagData.size());
for (TagData selectedTag : selectedTagData)
{
selectedTags.add((Tag)selectedTag.get(Constants.TAG));
}
AsyncCallback<List<Password>> callback = new AsyncCallback<List<Password>>()
{
@Override
public void onFailure(Throwable caught)
{
WebPasswordSafe.handleServerFailure(caught);
}
@Override
public void onSuccess(List<Password> result)
{
Info.display(textMessages.status(), textMessages.foundPasswords(result.size()));
refreshTable(result);
}
};
Match match = tagMatchRG.getValue().equals(radioAND) ? Match.AND : Match.OR;
PasswordService.Util.getInstance().searchPassword(Utils.safeString(searchTextBox.getValue()), activeOnlyCheckBox.getValue(), selectedTags, match, callback);
}
private void refreshTable(List<Password> passwords)
{
gridStore.removeAll();
for (Password password : passwords)
{
gridStore.add(new PasswordSearchData(password.getId(), password.getName(), password.getUsername(), password.getTagsAsString(), password.getNotes()));
}
}
@Override
public void reloadTags()
{
doLoadTags();
}
private class PasswordSearchData extends BaseModel
{
private static final long serialVersionUID = 1L;
public PasswordSearchData(long id, String title, String username, String tags, String notes)
{
set(Constants.ID, id);
set(Constants.TITLE, Format.htmlEncode(title));
set(Constants.USERNAME, Format.htmlEncode(username));
set(Constants.PASSWORD, textMessages.displayCensored());
set(Constants.TAGS, Format.htmlEncode(tags));
set(Constants.NOTES, Format.htmlEncode(notes));
}
}
private void doShowPasswordPopup(long passwordId)
{
AsyncCallback<String> callback = new AsyncCallback<String>()
{
@Override
public void onFailure(Throwable caught)
{
WebPasswordSafe.handleServerFailure(caught);
}
@Override
public void onSuccess(String result)
{
Dialog popup = new Dialog();
popup.setHeading(textMessages.currentPassword());
popup.setButtons(Dialog.CLOSE);
popup.addText(Format.htmlEncode(result));
popup.setScrollMode(Scroll.AUTO);
popup.setHideOnButtonClick(true);
popup.show();
}
};
PasswordService.Util.getInstance().getCurrentPassword(passwordId, callback);
}
private class TagData extends BaseTreeModel
{
private static final long serialVersionUID = 1L;
public TagData(Tag tag)
{
set(Constants.ID, tag.getId());
set(Constants.NAME, Format.htmlEncode(tag.getName()));
set(Constants.TAG, tag);
}
}
}