Improved search query support.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,5 +117,5 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
||||
Collection<Q> getQuestions();
|
||||
|
||||
@Nonnull
|
||||
ImmutableCollection<Q> findQuestions(@Nullable String query);
|
||||
ImmutableCollection<MPQuery.Result<Q>> findQuestions(MPQuery query);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
||||
Collection<S> getSites();
|
||||
|
||||
@Nonnull
|
||||
ImmutableCollection<S> findSites(@Nullable String query);
|
||||
ImmutableCollection<MPQuery.Result<S>> findSites(MPQuery query);
|
||||
|
||||
void addListener(Listener listener);
|
||||
|
||||
|
||||
@@ -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.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
@@ -193,11 +192,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
|
||||
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final Q question : getQuestions())
|
||||
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
|
||||
results.add( question );
|
||||
public ImmutableCollection<MPQuery.Result<Q>> findQuestions(final MPQuery query) {
|
||||
ImmutableSortedSet.Builder<MPQuery.Result<Q>> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final Q question : questions)
|
||||
query.find( question, MPQuestion::getKeyword ).ifPresent( results::add );
|
||||
|
||||
return results.build();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package com.lyndir.masterpassword.model.impl;
|
||||
|
||||
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.ImmutableSortedSet;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
@@ -201,11 +200,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ImmutableCollection<S> findSites(@Nullable final String query) {
|
||||
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final S site : getSites())
|
||||
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
|
||||
results.add( site );
|
||||
public ImmutableCollection<MPQuery.Result<S>> findSites(final MPQuery query) {
|
||||
ImmutableSortedSet.Builder<MPQuery.Result<S>> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final S site : sites.values())
|
||||
query.find( site, MPSite::getSiteName ).ifPresent( results::add );
|
||||
|
||||
return results.build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user