2
0

Implement sites list and copy result.

This commit is contained in:
Maarten Billemont 2018-07-19 13:56:26 -04:00
parent 476a4046e7
commit 400ebe59db
7 changed files with 280 additions and 93 deletions

View File

@ -0,0 +1,88 @@
package com.lyndir.masterpassword.gui.util;
import com.google.common.collect.ImmutableList;
import java.util.*;
import javax.annotation.Nullable;
import javax.swing.*;
/**
* @author lhunath, 2018-07-19
*/
@SuppressWarnings("serial")
public class CollectionListModel<E> extends AbstractListModel<E> implements ComboBoxModel<E> {
private final List<E> model = new LinkedList<>();
@Nullable
private E selectedItem;
public CollectionListModel() {
}
public CollectionListModel(final Collection<E> model) {
this.model.addAll( model );
fireIntervalAdded( this, 0, model.size() );
}
@Override
public synchronized int getSize() {
return model.size();
}
@Override
public synchronized E getElementAt(final int index) {
return model.get( index );
}
/**
* Replace this model's contents with the objects from the new model collection.
*
* This operation will mutate the internal model to reflect the given model.
* The given model will remain untouched and independent from this object.
*/
@SuppressWarnings("AssignmentToForLoopParameter")
public synchronized void set(final Collection<? extends E> newModel) {
ImmutableList<? extends E> newModelList = ImmutableList.copyOf( newModel );
ListIterator<E> oldIt = model.listIterator();
for (int from = 0; oldIt.hasNext(); ++from) {
int to = newModelList.indexOf( oldIt.next() );
if (to != from) {
oldIt.remove();
fireIntervalRemoved( this, from, from );
--from;
}
}
Iterator<? extends E> newIt = newModelList.iterator();
for (int to = 0; newIt.hasNext(); ++to) {
E newSite = newIt.next();
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
model.add( to, newSite );
fireIntervalAdded( this, to, to );
}
}
if ((selectedItem == null) && !model.isEmpty())
setSelectedItem( model.get( 0 ) );
else if (!model.contains( selectedItem ))
setSelectedItem( null );
}
@Override
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) {
if (!Objects.equals( selectedItem, newSelectedItem ) && model.contains( newSelectedItem )) {
selectedItem = (E) newSelectedItem;
fireContentsChanged( this, -1, -1 );
}
}
@Nullable
@Override
public synchronized E getSelectedItem() {
return selectedItem;
}
}

View File

@ -20,6 +20,7 @@ package com.lyndir.masterpassword.gui.util;
import com.lyndir.masterpassword.gui.Res; import com.lyndir.masterpassword.gui.Res;
import java.awt.*; import java.awt.*;
import java.util.function.Function;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border; import javax.swing.border.Border;
@ -34,9 +35,8 @@ public abstract class Components {
private static final float HEADING_TEXT_SIZE = 19f; private static final float HEADING_TEXT_SIZE = 19f;
private static final float CONTROL_TEXT_SIZE = 13f; private static final float CONTROL_TEXT_SIZE = 13f;
public static GradientPanel boxLayout(final int axis, final Component... components) { public static GradientPanel boxPanel(final int axis, final Component... components) {
GradientPanel container = gradientPanel( null, null ); GradientPanel container = gradientPanel( null, null );
// container.setBackground( Color.red );
container.setLayout( new BoxLayout( container, axis ) ); container.setLayout( new BoxLayout( container, axis ) );
for (final Component component : components) for (final Component component : components)
container.add( component ); container.add( component );
@ -44,12 +44,13 @@ public abstract class Components {
return container; return container;
} }
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border) { public static GradientPanel borderPanel(@Nullable final Border border, final Component... components) {
return borderPanel( component, border, null ); return borderPanel( border, null, components );
} }
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border, @Nullable final Color background) { public static GradientPanel borderPanel(@Nullable final Border border, @Nullable final Color background,
GradientPanel box = boxLayout( BoxLayout.LINE_AXIS, component ); final Component... components) {
GradientPanel box = boxPanel( BoxLayout.LINE_AXIS, components );
if (border != null) if (border != null)
box.setBorder( border ); box.setBorder( border );
@ -60,11 +61,11 @@ public abstract class Components {
return box; return box;
} }
public static GradientPanel gradientPanel(@Nullable final LayoutManager layout, @Nullable final Color color) { public static GradientPanel gradientPanel(@Nullable final Color color, @Nullable final LayoutManager layout) {
return new GradientPanel( layout, color ) { return new GradientPanel( layout, color ) {
{ {
setOpaque( color != null ); setOpaque( color != null );
setBackground( null ); setBackground( color );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT );
} }
@ -104,6 +105,45 @@ public abstract class Components {
}; };
} }
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
return new JList<E>( model ) {
{
setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) );
setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) );
setCellRenderer( new DefaultListCellRenderer() {
{
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
}
@Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
return super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
}
} );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
public static JScrollPane scrollPane(final Component child) {
return new JScrollPane( child ) {
{
setBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ) );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
};
}
public static JButton button(final String label) { public static JButton button(final String label) {
return new JButton( label ) { return new JButton( label ) {
{ {
@ -219,14 +259,28 @@ public abstract class Components {
} }
@SafeVarargs @SafeVarargs
public static <V> JComboBox<V> comboBox(final V... values) { public static <E> JComboBox<E> comboBox(final Function<E, String> valueTransformer, final E... values) {
return comboBox( new DefaultComboBoxModel<>( values ) ); return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer );
} }
public static <M> JComboBox<M> comboBox(final ComboBoxModel<M> model) { public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) {
return new JComboBox<M>( model ) { return new JComboBox<E>( model ) {
{ {
setFont( Res.fonts().controlFont().deriveFont( CONTROL_TEXT_SIZE ) ); setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) );
setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) );
setRenderer( new DefaultListCellRenderer() {
{
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
}
@Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
return super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
}
} );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT );
} }

View File

@ -1,28 +1,30 @@
package com.lyndir.masterpassword.gui.view; package com.lyndir.masterpassword.gui.view;
import com.lyndir.masterpassword.gui.Res; import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.CollectionListModel;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.ItemEvent;
import java.util.Set; import java.awt.event.ItemListener;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.plaf.metal.MetalComboBoxEditor;
/** /**
* @author lhunath, 2018-07-14 * @author lhunath, 2018-07-14
*/ */
public class FilesPanel extends JPanel implements ActionListener { public class FilesPanel extends JPanel implements ItemListener {
private final Set<Listener> listeners = new CopyOnWriteArraySet<>(); private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
private final JLabel avatarLabel = new JLabel(); private final JLabel avatarLabel = new JLabel();
private final JComboBox<MPFileUser> userField = Components.comboBox(); private final CollectionListModel<MPUser<?>> usersModel = new CollectionListModel<>();
private final JComboBox<MPUser<?>> userField =
Components.comboBox( usersModel, user -> (user != null)? user.getFullName(): null );
protected FilesPanel() { protected FilesPanel() {
setOpaque( false ); setOpaque( false );
@ -43,25 +45,7 @@ public class FilesPanel extends JPanel implements ActionListener {
// User Selection // User Selection
add( userField ); add( userField );
userField.addActionListener( this ); userField.addItemListener( this );
userField.setFont( Res.fonts().valueFont().deriveFont( userField.getFont().getSize2D() ) );
userField.setRenderer( new DefaultListCellRenderer() {
@Override
@SuppressWarnings("unchecked")
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String userValue = (value == null)? null: ((MPFileUser) value).getFullName();
return super.getListCellRendererComponent( list, userValue, index, isSelected, cellHasFocus );
}
} );
userField.setEditor( new MetalComboBoxEditor() {
@Override
protected JTextField createEditorComponent() {
JTextField editorComponents = Components.textField();
editorComponents.setForeground( Color.red );
return editorComponents;
}
} );
// - // -
add( Box.createVerticalGlue() ); add( Box.createVerticalGlue() );
@ -69,36 +53,25 @@ public class FilesPanel extends JPanel implements ActionListener {
public void reload() { public void reload() {
MPFileUserManager.get().reload(); MPFileUserManager.get().reload();
userField.setModel( new DefaultComboBoxModel<>( MPFileUserManager.get().getFiles().toArray( new MPFileUser[0] ) ) ); usersModel.set( MPFileUserManager.get().getFiles() );
updateFile();
}
@Override
public void actionPerformed(final ActionEvent e) {
updateFile();
}
@Nullable
private MPFileUser getSelectedUser() {
int selectedIndex = userField.getSelectedIndex();
if (selectedIndex < 0)
return null;
return userField.getModel().getElementAt( selectedIndex );
}
private void updateFile() {
MPFileUser selectedFile = getSelectedUser();
avatarLabel.setIcon( Res.avatar( (selectedFile == null)? 0: selectedFile.getAvatar() ) );
for (final Listener listener : listeners)
listener.onUserSelected( selectedFile );
} }
public boolean addListener(final Listener listener) { public boolean addListener(final Listener listener) {
return listeners.add( listener ); return listeners.add( listener );
} }
@Override
public void itemStateChanged(final ItemEvent e) {
if (e.getStateChange() != ItemEvent.SELECTED)
return;
MPUser<?> selectedUser = usersModel.getSelectedItem();
avatarLabel.setIcon( Res.avatar( (selectedUser == null)? 0: selectedUser.getAvatar() ) );
for (final Listener listener : listeners)
listener.onUserSelected( selectedUser );
}
public interface Listener { public interface Listener {
void onUserSelected(@Nullable MPUser<?> selectedUser); void onUserSelected(@Nullable MPUser<?> selectedUser);

View File

@ -23,12 +23,14 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener {
super( "Master Password" ); super( "Master Password" );
setDefaultCloseOperation( DISPOSE_ON_CLOSE ); setDefaultCloseOperation( DISPOSE_ON_CLOSE );
setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) ); setContentPane( root = Components.gradientPanel( Res.colors().frameBg(), new FlowLayout() ) );
root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) ); root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) );
root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
root.add( filesPanel ); root.add( filesPanel );
root.add( Components.borderPanel( userPanel, BorderFactory.createRaisedBevelBorder(), Res.colors().controlBg() ) ); root.add( new JSeparator( SwingConstants.HORIZONTAL ) );
root.add( Components.strut() );
root.add( Components.borderPanel( BorderFactory.createRaisedBevelBorder(), Res.colors().controlBg(), userPanel ) );
filesPanel.addListener( this ); filesPanel.addListener( this );
filesPanel.reload(); filesPanel.reload();

View File

@ -2,22 +2,25 @@ package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.ImmutableCollection;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.Res; import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.CollectionListModel;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.MPUser;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionListener; import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.DocumentEvent; import javax.swing.event.*;
import javax.swing.event.DocumentListener;
/** /**
@ -128,23 +131,24 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
} }
@Override @Override
public void insertUpdate(final DocumentEvent e) { public void insertUpdate(final DocumentEvent event) {
errorLabel.setText( null ); errorLabel.setText( null );
} }
@Override @Override
public void removeUpdate(final DocumentEvent e) { public void removeUpdate(final DocumentEvent event) {
errorLabel.setText( null ); errorLabel.setText( null );
} }
@Override @Override
public void changedUpdate(final DocumentEvent e) { public void changedUpdate(final DocumentEvent event) {
errorLabel.setText( null ); errorLabel.setText( null );
} }
} }
private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener { private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener, ListSelectionListener,
KeyListener {
@Nonnull @Nonnull
private final MPUser<?> user; private final MPUser<?> user;
@ -152,6 +156,10 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
private final JLabel passwordField = Components.heading( " ", SwingConstants.CENTER ); private final JLabel passwordField = Components.heading( " ", SwingConstants.CENTER );
private final JLabel queryLabel = Components.label( " " ); private final JLabel queryLabel = Components.label( " " );
private final JTextField queryField = Components.textField(); private final JTextField queryField = Components.textField();
private final CollectionListModel<MPSite<?>> sitesModel = new CollectionListModel<>();
private final JList<MPSite<?>> sitesList = Components.list( sitesModel,
value -> (value != null)? value.getName(): null );
private Future<?> updateSitesJob;
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) { private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
@ -166,12 +174,17 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont().deriveFont( Font.BOLD, 48 ) ); passwordField.setFont( Res.fonts().bigValueFont().deriveFont( Font.BOLD, 48 ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.strut() );
add( queryLabel ); add( queryLabel );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField ); add( queryField );
queryField.addActionListener( this ); queryField.addActionListener( this );
queryField.addKeyListener( this );
queryField.getDocument().addDocumentListener( this ); queryField.getDocument().addDocumentListener( this );
add( Components.strut() );
add( Components.scrollPane( sitesList ) );
sitesList.addListSelectionListener( this );
add( Box.createGlue() ); add( Box.createGlue() );
Res.ui( false, queryField::requestFocusInWindow ); Res.ui( false, queryField::requestFocusInWindow );
@ -179,12 +192,59 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
@Override @Override
public void actionPerformed(final ActionEvent event) { public void actionPerformed(final ActionEvent event) {
String siteName = queryField.getText(); showSiteResult( sitesList.getSelectedValue(), result -> {
if (result == null)
return;
Transferable clipboardContents = new StringSelection( result );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
Res.ui( () -> {
Window window = SwingUtilities.windowForComponent( this );
window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) );
} );
} );
}
@Override
public void insertUpdate(final DocumentEvent event) {
updateSites();
}
@Override
public void removeUpdate(final DocumentEvent event) {
updateSites();
}
@Override
public void changedUpdate(final DocumentEvent event) {
updateSites();
}
@Override
public void valueChanged(final ListSelectionEvent event) {
showSiteResult( event.getValueIsAdjusting()? null: sitesList.getSelectedValue(), null );
}
private void showSiteResult(@Nullable final MPSite<?> site, @Nullable final Consumer<String> resultCallback) {
if (site == null) {
if (resultCallback != null)
resultCallback.accept( null );
Res.ui( () -> {
passwordLabel.setText( " " );
passwordField.setText( " " );
} );
return;
}
String siteName = site.getName();
Res.job( () -> { Res.job( () -> {
try { try {
String result = user.getMasterKey().siteResult( String result = user.getMasterKey().siteResult(
siteName, user.getAlgorithm(), UnsignedInteger.ONE, siteName, user.getAlgorithm(), UnsignedInteger.ONE,
MPKeyPurpose.Authentication, null, MPResultType.GeneratedLong, null ); MPKeyPurpose.Authentication, null, MPResultType.GeneratedLong, null );
if (resultCallback != null)
resultCallback.accept( result );
Res.ui( () -> { Res.ui( () -> {
passwordLabel.setText( strf( "Your password for %s:", siteName ) ); passwordLabel.setText( strf( "Your password for %s:", siteName ) );
@ -192,27 +252,35 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
} ); } );
} }
catch (final MPKeyUnavailableException | MPAlgorithmException e) { catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving password for: %s", siteName ); logger.err( e, "While resolving password for: %s", site );
} }
} ); } );
} }
@Override @Override
public void insertUpdate(final DocumentEvent e) { public void keyTyped(final KeyEvent event) {
// TODO
} }
@Override @Override
public void removeUpdate(final DocumentEvent e) { public void keyPressed(final KeyEvent event) {
// TODO if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
sitesList.dispatchEvent( event );
} }
@Override @Override
public void changedUpdate(final DocumentEvent e) { public void keyReleased(final KeyEvent event) {
// TODO if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
sitesList.dispatchEvent( event );
}
private synchronized void updateSites() {
if (updateSitesJob != null)
updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> {
ImmutableCollection<? extends MPSite<?>> sites = user.findSites( queryField.getText() );
Res.ui( () -> sitesModel.set( sites ) );
} );
} }
} }
} }

View File

@ -18,6 +18,7 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.impl.MPBasicSite; import com.lyndir.masterpassword.model.impl.MPBasicSite;
import com.lyndir.masterpassword.model.impl.MPBasicUser; import com.lyndir.masterpassword.model.impl.MPBasicUser;
@ -94,7 +95,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
Collection<S> getSites(); Collection<S> getSites();
@Nonnull @Nonnull
Collection<S> findSites(String query); ImmutableCollection<S> findSites(String query);
boolean addListener(Listener listener); boolean addListener(Listener listener);

View File

@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
@ -172,7 +173,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Nonnull @Nonnull
@Override @Override
public Collection<S> findSites(final String query) { public ImmutableCollection<S> findSites(final String query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
for (final S site : getSites()) for (final S site : getSites())
if (site.getName().startsWith( query )) if (site.getName().startsWith( query ))