/v1/query endpoint

- Requires 'q' parameter which is URI encoded JSON array
- The array contains FilterQuery objects
- Right now the list of UUIDs and path is returned

Up next /v1/filters endpoint that returns list of filter kinds and what their default options.
This commit is contained in:
Risto Lahtela 2020-03-26 16:22:04 +02:00
parent aac7bdc632
commit a14d7d4769
11 changed files with 191 additions and 24 deletions

View File

@ -0,0 +1,78 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import com.djrapitops.plan.storage.database.queries.filter.QueryFilters;
import com.djrapitops.plan.utilities.java.Maps;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.List;
import java.util.Optional;
@Singleton
public class QueryJSONResolver implements Resolver {
private QueryFilters filters;
@Inject
public QueryJSONResolver(
QueryFilters filters
) {
this.filters = filters;
}
@Override
public boolean canAccess(Request request) {
WebUser user = request.getUser().orElse(new WebUser(""));
return user.hasPermission("page.players");
}
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));
}
private Response getResponse(Request request) {
String q = request.getQuery().get("q").orElseThrow(() -> new BadRequestException("'q' parameter not set (expecting json array)"));
try {
q = URLDecoder.decode(q, "UTF-8");
List<FilterQuery> queries = FilterQuery.parse(q);
Filter.Result result = filters.apply(queries);
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(Maps.builder(String.class, Object.class)
.put("path", result.getResultPath(","))
.put("uuids", result.getResultUUIDs())
.build())
.build();
} catch (IOException e) {
throw new BadRequestException("Failed to parse json: '" + q + "'" + e.getMessage());
}
}
}

View File

@ -52,7 +52,8 @@ public class RootJSONResolver {
PerformanceJSONCreator performanceJSONCreator,
PlayerJSONResolver playerJSONResolver,
NetworkJSONResolver networkJSONResolver
NetworkJSONResolver networkJSONResolver,
QueryJSONResolver queryJSONResolver
) {
this.identifiers = identifiers;
@ -70,6 +71,7 @@ public class RootJSONResolver {
.add("performanceOverview", forJSON(DataID.PERFORMANCE_OVERVIEW, performanceJSONCreator))
.add("player", playerJSONResolver)
.add("network", networkJSONResolver.getResolver())
.add("query", queryJSONResolver)
.build();
}

View File

@ -17,7 +17,7 @@
package com.djrapitops.plan.storage.database.queries.filter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -32,8 +32,8 @@ public interface Filter {
String[] getExpectedParameters();
default List<String> getOptions() {
return Collections.emptyList();
default Map<String, Object> getOptions() {
return Collections.emptyMap();
}
/**
@ -46,4 +46,51 @@ public interface Filter {
*/
Set<UUID> getMatchingUUIDs(FilterQuery query);
default Result apply(FilterQuery query) {
return new Result(null, getKind(), getMatchingUUIDs(query));
}
class Result {
private Result previous;
private String filterKind;
private int resultSize;
private Set<UUID> currentUUIDs;
private Result(Result previous, String filterKind, Set<UUID> currentUUIDs) {
this.previous = previous;
this.filterKind = filterKind;
this.resultSize = currentUUIDs.size();
this.currentUUIDs = currentUUIDs;
}
public Result apply(Filter filter, FilterQuery query) {
Set<UUID> got = filter.getMatchingUUIDs(query);
currentUUIDs.retainAll(got);
return new Result(this, filter.getKind(), currentUUIDs);
}
public Result notApplied(Filter filter) {
return new Result(this, filter.getKind(), currentUUIDs);
}
public boolean isEmpty() {
return resultSize <= 0;
}
public Set<UUID> getResultUUIDs() {
return currentUUIDs;
}
public StringBuilder getResultPath(String separator) {
StringBuilder builder;
if (previous == null) {
// First Result in chain
builder = new StringBuilder();
} else {
builder = previous.getResultPath(separator);
}
return builder.append(separator).append("-> ").append(filterKind).append(": ").append(resultSize);
}
}
}

View File

@ -23,6 +23,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Represents parameters for a single {@link Filter} parsed from the query json.
@ -48,6 +49,12 @@ public class FilterQuery {
}
public Optional<String> get(String key) {
if (parameters == null) return Optional.empty();
return Optional.ofNullable(parameters.get(key));
}
public Set<String> getSetParameters() {
if (parameters == null) return null;
return parameters.keySet();
}
}

View File

@ -16,12 +16,11 @@
*/
package com.djrapitops.plan.storage.database.queries.filter;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
/**
* Contains a single instance of each filter kind.
@ -50,4 +49,39 @@ public class QueryFilters {
return Optional.ofNullable(filters.get(kind));
}
/**
* Apply queries to get a {@link com.djrapitops.plan.storage.database.queries.filter.Filter.Result}.
*
* @param filterQueries FilterQueries to use as filter parameters.
* @return the result object or null if none of the filterQueries could be applied.
* @throws BadRequestException If the request kind is not supported or if filter was given bad options.
*/
public Filter.Result apply(List<FilterQuery> filterQueries) {
Filter.Result current = null;
for (FilterQuery filterQuery : filterQueries) {
current = apply(current, filterQuery);
if (current != null && current.isEmpty()) break;
}
return current;
}
private Filter.Result apply(Filter.Result current, FilterQuery filterQuery) {
String kind = filterQuery.getKind();
Filter filter = getFilter(kind).orElseThrow(() -> new BadRequestException("Filter kind not supported: '" + kind + "'"));
current = getResult(current, filter, filterQuery);
return current;
}
private Filter.Result getResult(Filter.Result current, Filter filter, FilterQuery query) {
try {
return current == null ? filter.apply(query) : current.apply(filter, query);
} catch (IllegalArgumentException badOptions) {
throw new BadRequestException("Bad parameters for filter '" + filter.getKind() +
"': expecting " + Arrays.asList(filter.getExpectedParameters()) +
", but was given " + query.getSetParameters());
} catch (CompleteSetException complete) {
return current == null ? null : current.notApplied(filter);
}
}
}

View File

@ -58,8 +58,8 @@ public class ActivityIndexFilter extends MultiOptionFilter {
}
@Override
public List<String> getOptions() {
return Collections.singletonList(serializeOptions(getOptionsArray()));
public Map<String, Object> getOptions() {
return Collections.singletonMap("options", getOptionsArray());
}
@Override

View File

@ -52,8 +52,8 @@ public class BannedFilter extends MultiOptionFilter {
}
@Override
public List<String> getOptions() {
return Collections.singletonList(serializeOptions(getOptionsArray()));
public Map<String, Object> getOptions() {
return Collections.singletonMap("options", getOptionsArray());
}
@Override

View File

@ -20,12 +20,12 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries;
import com.djrapitops.plan.utilities.java.Maps;
import org.apache.commons.lang3.StringUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public abstract class DateRangeFilter implements Filter {
@ -48,13 +48,16 @@ public abstract class DateRangeFilter implements Filter {
}
@Override
public List<String> getOptions() {
public Map<String, Object> getOptions() {
long earliestData = dbSystem.getDatabase().query(BaseUserQueries.minimumRegisterDate());
long now = System.currentTimeMillis();
if (earliestData == -1) earliestData = now;
String[] afterDate = StringUtils.split(dateFormat.format(earliestData), ' ');
String[] beforeDate = StringUtils.split(dateFormat.format(now), ' ');
return Arrays.asList(afterDate[0], afterDate[1], beforeDate[0], beforeDate[1]);
return Maps.builder(String.class, Object.class)
.put("after", afterDate)
.put("before", beforeDate)
.build();
}
protected long getAfter(FilterQuery query) {

View File

@ -19,7 +19,6 @@ package com.djrapitops.plan.storage.database.queries.filter.filters;
import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.TextStringBuilder;
import java.util.Arrays;
import java.util.List;
@ -36,10 +35,6 @@ public abstract class MultiOptionFilter implements Filter {
return Arrays.asList(deserializeOptions(selected));
}
protected String serializeOptions(String... options) {
return new TextStringBuilder().appendWithSeparators(options, ",").build();
}
private String[] deserializeOptions(String selected) {
return StringUtils.split(selected, ',');
}

View File

@ -52,8 +52,8 @@ public class OperatorsFilter extends MultiOptionFilter {
}
@Override
public List<String> getOptions() {
return Collections.singletonList(serializeOptions(getOptionsArray()));
public Map<String, Object> getOptions() {
return Collections.singletonMap("options", getOptionsArray());
}
@Override

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -45,7 +46,7 @@ public class PluginGroupsFilter extends MultiOptionFilter {
}
@Override
public List<String> getOptions() {
public Map<String, Object> getOptions() {
return null;
}