io.gravitee.gateway.reactive.v4.flow.AbstractBestMatchFlowSelector Maven / Gradle / Ivy
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.gateway.reactive.v4.flow;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
* @author GraviteeSource Team
*/
public abstract class AbstractBestMatchFlowSelector {
private static final String PATH_PARAM_PREFIX = ":";
private static final Pattern SEPARATOR_SPLITTER = Pattern.compile("/");
/**
* Filters the flows to get the one best matching the request.
*
*
* We assume the {@code List} parameter is already filtered by the a previous resolver to be sure the Flows' path match the request according to Flows' operator.
*
*
* For each part of the flow path (split by {@link #SEPARATOR_SPLITTER}), a score is attributed:
* - 1 if the string strictly equals the same part of the request
* - 0.5 if the part is a path parameter (starting with {@link #PATH_PARAM_PREFIX})
* - else 0
*
* Then, for each flow, we compare the scores arrays to the current selected best matching flow, reading from left to right.
* As soon as a score is greater, then the flow becomes the best match.
*
*
* Here is an example with those flows configured:
*
* - /myPath/staticId
* - /:id/staticId
* - /myPath/:id
*
*
*
*
*
*
*
*
* Request path
* Flow path <> score
* Selected path
*
*
*
*
* /myPath/staticId
*
* - /myPath/staticId <> [1, 1]
* - /:id/staticId <> [0.5, 1]
* - /myPath/:id <> [1, 0.5]
*
* /myPath/staticId
*
*
* /myPath/553
*
* - /myPath/staticId <> [1, 0]
* - /:id/staticId <> [0.5, 0]
* - /myPath/:id <> [1, 0.5]
*
* /myPath/:id
*
*
* /random/staticId
*
* - /myPath/staticId <> [0, 1]
* - /:id/staticId <> [0.5, 1]
* - /myPath/:id <> [0, 0.5]
*
* /:id/staticId
*
*
*
*
* @param flows the flows already filtered (by a previous resolver).
* @param path the current execution request.
*
* @return a list containing the best matching flow.
*/
public T forPath(final List flows, final String path) {
// Do not process empty flows
if (flows == null || flows.isEmpty()) {
return null;
}
T selectedFlow = null;
Float[] selectedFlowScore = null;
for (T flow : flows) {
String[] splits = splitFlowPath(flow);
final String[] pathSplits = splitPath(path);
final Float[] scores = new Float[splits.length];
for (int i = 0; i < splits.length; i++) {
// First, compute a score foreach split
if (i >= pathSplits.length || splits[i].equals(pathSplits[i])) {
scores[i] = 1.0f;
} else if (splits[i].startsWith(PATH_PARAM_PREFIX)) {
scores[i] = 0.5f;
} else {
scores[i] = 0f;
}
if (selectedFlow == null) {
selectedFlow = flow;
selectedFlowScore = scores;
}
// Then, if current splits array is longer than selected flow one, the current flow is selected as best
if (i == selectedFlowScore.length) {
selectedFlow = flow;
selectedFlowScore = scores;
}
// Finally, if split score is fewer than selected, no need to continue, else we have a better matching, so we can select the flow
if (scores[i] < selectedFlowScore[i]) {
break;
} else if (scores[i] > selectedFlowScore[i]) {
selectedFlow = flow;
selectedFlowScore = scores;
}
}
}
return selectedFlow;
}
protected abstract Optional providePath(T flow);
private String[] splitFlowPath(T flow) {
return providePath(flow).map(AbstractBestMatchFlowSelector::splitPath).orElse(new String[0]);
}
/**
* Split string with "/" character. We use an already compiled Pattern to avoid using {@link String#split(String)} which is compiling a new one for each call.
*
* Also, we choose a negative limit to split the string, to avoid discarding trailing empty string.
*
* For more information, you can see {@link Pattern#split(CharSequence, int)}.
*
*
* If the limit is negative then the pattern will be applied as many times as possible and the array can have any length.
*
* @param path to split
* @return The array of strings computed by splitting the input path
*/
private static String[] splitPath(String path) {
return SEPARATOR_SPLITTER.split(path, -1);
}
}