2
0

Improved search query support.

This commit is contained in:
Maarten Billemont 2018-09-12 13:12:10 -04:00
parent ac5286853a
commit a1eee88a54
10 changed files with 298 additions and 108 deletions

View File

@ -27,6 +27,13 @@ public final class Utilities {
return value; return value;
} }
public static String ifNotNullOrEmptyElse(@Nullable final String value, @Nonnull final String emptyValue) {
if ((value == null) || value.isEmpty())
return emptyValue;
return value;
}
@Nonnull @Nonnull
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) { public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
if (value == null) if (value == null)

View File

@ -1,10 +1,11 @@
package com.lyndir.masterpassword.gui.util; package com.lyndir.masterpassword.gui.util;
import static com.google.common.base.Preconditions.*;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -62,7 +63,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
public synchronized void set(final Iterable<? extends E> elements) { public synchronized void set(final Iterable<? extends E> elements) {
ListIterator<E> oldIt = model.listIterator(); ListIterator<E> oldIt = model.listIterator();
for (int from = 0; oldIt.hasNext(); ++from) { for (int from = 0; oldIt.hasNext(); ++from) {
int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) ); int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
if (to != from) { if (to != from) {
oldIt.remove(); oldIt.remove();
@ -82,7 +83,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
} }
if ((selectedItem == null) || !model.contains( selectedItem )) if ((selectedItem == null) || !model.contains( selectedItem ))
setSelectedItem( getElementAt( 0 ) ); selectItem( getElementAt( 0 ) );
} }
@SafeVarargs @SafeVarargs
@ -91,19 +92,26 @@ public class CollectionListModel<E> extends AbstractListModel<E>
} }
@Override @Override
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" }) @Deprecated
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) { @SuppressWarnings("unchecked")
if (!Objects.equals( selectedItem, newSelectedItem )) { public synchronized void setSelectedItem(@Nullable final Object/* E */ newSelectedItem) {
selectedItem = (E) newSelectedItem; selectItem( (E) newSelectedItem );
}
fireContentsChanged( this, -1, -1 ); public synchronized CollectionListModel<E> selectItem(@Nullable final E newSelectedItem) {
//noinspection ObjectEquality if (Objects.equals( selectedItem, newSelectedItem ))
if ((list != null) && (list.getModel() == this)) return this;
list.setSelectedValue( selectedItem, true );
if (selectionConsumer != null) selectedItem = newSelectedItem;
selectionConsumer.accept( selectedItem );
} fireContentsChanged( this, -1, -1 );
//noinspection ObjectEquality
if ((list != null) && (list.getModel() == this))
list.setSelectedValue( selectedItem, true );
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this;
} }
@Nullable @Nullable
@ -112,11 +120,6 @@ public class CollectionListModel<E> extends AbstractListModel<E>
return selectedItem; return selectedItem;
} }
public CollectionListModel<E> select(final E selectedItem) {
setSelectedItem( selectedItem );
return this;
}
public synchronized void registerList(final JList<E> list) { public synchronized void registerList(final JList<E> list) {
// TODO: This class should probably implement ListSelectionModel instead. // TODO: This class should probably implement ListSelectionModel instead.
if (this.list != null) if (this.list != null)
@ -139,7 +142,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
@Override @Override
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) { public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = null; this.selectionConsumer = null;
setSelectedItem( selectedItem ); selectItem( selectedItem );
return selection( selectionConsumer ); return selection( selectionConsumer );
} }
@ -147,7 +150,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
@Override @Override
public synchronized void valueChanged(final ListSelectionEvent event) { public synchronized void valueChanged(final ListSelectionEvent event) {
//noinspection ObjectEquality //noinspection ObjectEquality
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) { if (!event.getValueIsAdjusting() && (event.getSource() == list) && (checkNotNull( list ).getModel() == this)) {
selectedItem = list.getSelectedValue(); selectedItem = list.getSelectedValue();
if (selectionConsumer != null) if (selectionConsumer != null)

View File

@ -64,7 +64,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
@Override @Override
public void onUserSelected(@Nullable final MPUser<?> user) { public void onUserSelected(@Nullable final MPUser<?> user) {
usersModel.setSelectedItem( user ); usersModel.selectItem( user );
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
} }
} }

View File

@ -15,7 +15,6 @@ import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*; import com.lyndir.masterpassword.model.impl.*;
import com.lyndir.masterpassword.util.Utilities;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
@ -479,15 +478,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"Delete the site from the user." ); "Delete the site from the user." );
@Nonnull @Nonnull
private final MPUser<?> user; private final MPUser<?> user;
private final JLabel passwordLabel; private final JLabel passwordLabel;
private final JLabel passwordField; private final JLabel passwordField;
private final JLabel answerLabel; private final JLabel answerLabel;
private final JLabel answerField; private final JLabel answerField;
private final JLabel queryLabel; private final JLabel queryLabel;
private final JTextField queryField; private final JTextField queryField;
private final CollectionListModel<MPSite<?>> sitesModel; private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
private final JList<MPSite<?>> sitesList; private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
private Future<?> updateSitesJob; private Future<?> updateSitesJob;
@ -517,6 +517,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
answerField = Components.heading( SwingConstants.CENTER ); answerField = Components.heading( SwingConstants.CENTER );
answerField.setForeground( Res.colors().highlightFg() ); answerField.setForeground( Res.colors().highlightFg() );
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
@ -538,7 +539,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
add( Components.strut() ); add( Components.strut() );
add( Components.scrollPane( sitesList = Components.list( add( Components.scrollPane( sitesList = Components.list(
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ), sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
this::getSiteDescription ) ) ); this::getSiteDescription ) ) );
add( Components.strut() ); add( Components.strut() );
@ -580,7 +581,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteSettings() { public void showSiteSettings() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@ -627,22 +628,22 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteQuestions() { public void showSiteQuestions() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult ); JList<MPQuery.Result<? extends MPQuestion>> questionsList =
JList<MPQuestion> questionsList = Components.list( Components.list( questionsModel, this::getQuestionDescription );
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() ); JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
JTextField queryField = Components.textField( null, query -> Res.job( () -> { MPQuery query = new MPQuery( queryText );
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) ); Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<>( site.findQuestions( query ) );
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) )) if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) ); questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
Res.ui( () -> questionsModel.set( questions ) ); Res.ui( () -> questionsModel.set( questionItems ) );
} ) ); } ) );
queryField.putClientProperty( "JTextField.variant", "search" ); queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) ); queryField.addActionListener( this::useQuestion );
queryField.addKeyListener( new KeyAdapter() { queryField.addKeyListener( new KeyAdapter() {
@Override @Override
public void keyPressed(final KeyEvent event) { public void keyPressed(final KeyEvent event) {
@ -672,7 +673,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteValues() { public void showSiteValues() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@ -717,7 +718,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteKeys() { public void showSiteKeys() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@ -787,7 +788,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void deleteSite() { public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@ -797,55 +798,62 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
user.deleteSite( site ); user.deleteSite( site );
} }
private String getSiteDescription(@Nonnull final MPSite<?> site) { private String getSiteDescription(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getOption(): null;
if (site == null)
return " ";
if (site instanceof MPNewSite) if (site instanceof MPNewSite)
return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", queryField.getText() ); return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", item.getKeyAsHTML() );
ImmutableList.Builder<Object> parameters = ImmutableList.builder(); ImmutableList.Builder<Object> parameters = ImmutableList.builder();
try { MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null; if (fileSite != null)
if (fileSite != null) parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( Res.format( fileSite.getLastUsed() ) ); parameters.add( site.getAlgorithm().version() );
parameters.add( site.getAlgorithm().version() ); parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "#%d", site.getCounter().longValue() ) ); if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( strf( "<em>%s</em>", site.getLogin() ) ); parameters.add( fileSite.getUrl() );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
}
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
logger.err( e, "While generating site description." );
parameters.add( e.getLocalizedMessage() );
}
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(), return strf( "<html><strong>%s</strong> (%s)</html>", item.getKeyAsHTML(),
Joiner.on( " - " ).skipNulls().join( parameters.build() ) ); Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
} }
private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPQuestion question = (item != null)? item.getOption(): null;
if (question == null)
return "<site>";
if (question instanceof MPNewQuestion)
return strf( "<html>%s &lt;Add new question&gt;</html>", item.getKeyAsHTML() );
return strf( "<html>%s</html>", item.getKeyAsHTML() );
}
private void useSite(final ActionEvent event) { private void useSite(final ActionEvent event) {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site instanceof MPNewSite) { if (site instanceof MPNewSite) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ), this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
"New Site", JOptionPane.YES_NO_OPTION )) { "New Site", JOptionPane.YES_NO_OPTION ))
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) ); return;
useSite( event );
} site = user.addSite( site.getSiteName() );
return;
} }
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0; boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
MPSite<?> fsite = site;
showSiteResult( site, loginResult, result -> { showSiteResult( site, loginResult, result -> {
if (result == null) if (result == null)
return; return;
if (site instanceof MPFileSite) if (fsite instanceof MPFileSite)
((MPFileSite) site).use(); ((MPFileSite) fsite).use();
copyResult( result ); copyResult( result );
} ); } );
} }
private void showSiteResult(@Nullable final MPSite<?> site) { private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getOption(): null;
showSiteResult( site, false, result -> { showSiteResult( site, false, result -> {
} ); } );
} }
@ -862,8 +870,11 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
return null; return null;
}, resultCallback.andThen( result -> Res.ui( () -> { }, resultCallback.andThen( result -> Res.ui( () -> {
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " ); if (!loginResult && (site != null)) {
passwordField.setText( (result != null)? result: " " ); passwordLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
passwordField.setText( (result != null)? result: " " );
}
settingsButton.setEnabled( result != null ); settingsButton.setEnabled( result != null );
questionsButton.setEnabled( result != null ); questionsButton.setEnabled( result != null );
editButton.setEnabled( result != null ); editButton.setEnabled( result != null );
@ -872,30 +883,33 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} ) ) ); } ) ) );
} }
private void useQuestion(@Nullable final MPQuestion question) { private void useQuestion(final ActionEvent event) {
MPQuestion question = getQuestion();
if (question instanceof MPNewQuestion) { if (question instanceof MPNewQuestion) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this, this,
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>", strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ), Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
"New Question", JOptionPane.YES_NO_OPTION )) { "New Question", JOptionPane.YES_NO_OPTION ))
useQuestion( question.getSite().addQuestion( question.getKeyword() ) ); return;
}
return; question = question.getSite().addQuestion( question.getKeyword() );
} }
MPQuestion fquestion = question;
showQuestionResult( question, result -> { showQuestionResult( question, result -> {
if (result == null) if (result == null)
return; return;
if (question instanceof MPFileQuestion) if (fquestion instanceof MPFileQuestion)
((MPFileQuestion) question).use(); ((MPFileQuestion) fquestion).use();
copyResult( result ); copyResult( result );
} ); } );
} }
private void showQuestionResult(@Nullable final MPQuestion question) { private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPQuestion question = (item != null)? item.getOption(): null;
showQuestionResult( question, answer -> { showQuestionResult( question, answer -> {
} ); } );
} }
@ -939,6 +953,24 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} ); } );
} }
@Nullable
private MPSite<?> getSite() {
MPQuery.Result<? extends MPSite<?>> selectedSite = sitesModel.getSelectedItem();
if (selectedSite == null)
return null;
return selectedSite.getOption();
}
@Nullable
private MPQuestion getQuestion() {
MPQuery.Result<? extends MPQuestion> selectedQuestion = questionsModel.getSelectedItem();
if (selectedQuestion == null)
return null;
return selectedQuestion.getOption();
}
@Override @Override
public void keyTyped(final KeyEvent event) { public void keyTyped(final KeyEvent event) {
} }
@ -955,25 +987,27 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
sitesList.dispatchEvent( event ); sitesList.dispatchEvent( event );
} }
private synchronized void updateSites(@Nullable final String query) { private synchronized void updateSites(@Nullable final String queryText) {
if (updateSitesJob != null) if (updateSitesJob != null)
updateSitesJob.cancel( true ); updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> { updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) ); MPQuery query = new MPQuery( queryText );
Collection<MPQuery.Result<? extends MPSite<?>>> siteItems =
new LinkedList<>( user.findSites( query ) );
if (!Strings.isNullOrEmpty( query )) if (!Strings.isNullOrEmpty( queryText ))
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) if (siteItems.stream().noneMatch( MPQuery.Result::isExact ))
sites.add( new MPNewSite( user, query ) ); siteItems.add( MPQuery.Result.allOf( new MPNewSite( user, query.getQuery() ), query.getQuery() ) );
Res.ui( () -> sitesModel.set( sites ) ); Res.ui( () -> sitesModel.set( siteItems ) );
} ); } );
} }
@Override @Override
public void onUserUpdated(final MPUser<?> user) { public void onUserUpdated(final MPUser<?> user) {
updateSites( queryField.getText() ); updateSites( queryField.getText() );
showSiteResult( sitesModel.getSelectedItem() ); showSiteItem( sitesModel.getSelectedItem() );
} }
@Override @Override

View File

@ -0,0 +1,150 @@
package com.lyndir.masterpassword.model;
import java.util.*;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
/**
* @author lhunath, 2018-09-11
*/
public class MPQuery {
@Nonnull
private final String query;
public MPQuery(@Nullable final String query) {
this.query = (query != null)? query: "";
}
@Nonnull
public String getQuery() {
return query;
}
/**
* @return {@code true} if this query is contained wholly inside the given {@code key}.
*/
@Nonnull
public <T extends Comparable<? super T>> Optional<Result<T>> find(final T option, final Function<T, CharSequence> keyForOption) {
CharSequence key = keyForOption.apply( option );
Result<T> result = Result.noneOf( option, key );
if (query.isEmpty())
return Optional.of( result );
if (key.length() == 0)
return Optional.empty();
// Consume query and key characters until one of them runs out, recording any matches against the result's key.
int q = 0, k = 0;
while ((q < query.length()) && (k < key.length())) {
if (query.charAt( q ) == key.charAt( k )) {
result.keyMatchedAt( k );
++q;
}
++k;
}
// If query is consumed, the result is a hit.
return (q >= query.length())? Optional.of( result ): Optional.empty();
}
public static class Result<T extends Comparable<? super T>> implements Comparable<Result<T>> {
private final T option;
private final CharSequence key;
private final boolean[] keyMatches;
Result(final T option, final CharSequence key) {
this.option = option;
this.key = key;
keyMatches = new boolean[key.length()];
}
public static <T extends Comparable<? super T>> Result<T> noneOf(final T option, final CharSequence key) {
return new Result<>( option, key );
}
public static <T extends Comparable<? super T>> Result<T> allOf(final T option, final CharSequence key) {
Result<T> result = noneOf( option, key );
Arrays.fill( result.keyMatches, true );
return result;
}
@Nonnull
public T getOption() {
return option;
}
@Nonnull
public CharSequence getKey() {
return key;
}
public String getKeyAsHTML() {
return getKeyAsHTML( "u" );
}
@SuppressWarnings({ "MagicCharacter", "HardcodedFileSeparator" })
public String getKeyAsHTML(final String mark) {
String closeMark = mark.contains( " " )? mark.substring( 0, mark.indexOf( ' ' ) ): mark;
StringBuilder html = new StringBuilder();
boolean marked = false;
for (int i = 0; i < key.length(); ++i) {
if (keyMatches[i] && !marked) {
html.append( '<' ).append( mark ).append( '>' );
marked = true;
} else if (!keyMatches[i] && marked) {
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
marked = false;
}
html.append( key.charAt( i ) );
}
if (marked)
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
return html.toString();
}
public boolean[] getKeyMatches() {
return keyMatches.clone();
}
public boolean isExact() {
for (final boolean keyMatch : keyMatches)
if (!keyMatch)
return false;
return true;
}
private void keyMatchedAt(final int k) {
keyMatches[k] = true;
}
@Override
public int compareTo(@NotNull final Result<T> o) {
return getOption().compareTo( o.getOption() );
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof Result))
return false;
Result<?> r = (Result<?>) o;
return Objects.equals( option, r.option ) && Objects.equals( key, r.key ) && Arrays.equals( keyMatches, r.keyMatches );
}
@Override
public int hashCode() {
return getOption().hashCode();
}
}
}

View File

@ -117,5 +117,5 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
Collection<Q> getQuestions(); Collection<Q> getQuestions();
@Nonnull @Nonnull
ImmutableCollection<Q> findQuestions(@Nullable String query); ImmutableCollection<MPQuery.Result<Q>> findQuestions(MPQuery query);
} }

View File

@ -112,7 +112,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
Collection<S> getSites(); Collection<S> getSites();
@Nonnull @Nonnull
ImmutableCollection<S> findSites(@Nullable String query); ImmutableCollection<MPQuery.Result<S>> findSites(MPQuery query);
void addListener(Listener listener); void addListener(Listener listener);

View File

@ -21,7 +21,6 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
@ -193,11 +192,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Nonnull @Nonnull
@Override @Override
public ImmutableCollection<Q> findQuestions(@Nullable final String query) { public ImmutableCollection<MPQuery.Result<Q>> findQuestions(final MPQuery query) {
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<MPQuery.Result<Q>> results = ImmutableSortedSet.naturalOrder();
for (final Q question : getQuestions()) for (final Q question : questions)
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query )) query.find( question, MPQuestion::getKeyword ).ifPresent( results::add );
results.add( question );
return results.build(); return results.build();
} }

View File

@ -20,7 +20,6 @@ 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.base.Strings;
import com.google.common.collect.ImmutableCollection; 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;
@ -201,11 +200,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull @Nonnull
@Override @Override
public ImmutableCollection<S> findSites(@Nullable final String query) { public ImmutableCollection<MPQuery.Result<S>> findSites(final MPQuery query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<MPQuery.Result<S>> results = ImmutableSortedSet.naturalOrder();
for (final S site : getSites()) for (final S site : sites.values())
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query )) query.find( site, MPSite::getSiteName ).ifPresent( results::add );
results.add( site );
return results.build(); return results.build();
} }

@ -1 +1 @@
Subproject commit ff77947d449b9d92bc254e04a0506f4b5badae62 Subproject commit 914a60cd25707f4ac456ad225580a86a5a95e637