blob: 499642ae1e69e17bbde5a33c836382c9c533c334 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.solr.cloud.api.collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.solr.client.solrj.RoutedAliasTypes;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.update.AddUpdateCommand;
import static org.apache.solr.client.solrj.request.CollectionAdminRequest.DimensionalRoutedAlias.addDimensionIndexIfRequired;
import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
public class DimensionalRoutedAlias extends RoutedAlias {
private final String name;
private List<RoutedAlias> dimensions;
// things we don't need to calc twice...
private Set<String> reqParams = new HashSet<>();
private Set<String> optParams = new HashSet<>();
private Map<String, String> aliasMetadata;
private static final Pattern SEP_MATCHER = Pattern.compile("("+
Arrays.stream(RoutedAliasTypes.values())
.filter(v -> v != RoutedAliasTypes.DIMENSIONAL)
.map(RoutedAliasTypes::getSeparatorPrefix)
.collect(Collectors.joining("|")) +
")");
DimensionalRoutedAlias(List<RoutedAlias> dimensions, String name, Map<String, String> props) {
this.dimensions = dimensions;
this.name = name;
this.aliasMetadata = props;
}
interface Deffered<T> {
T get();
}
static RoutedAlias dimensionForType(Map<String, String> props, RoutedAliasTypes type,
int index, Deffered<DimensionalRoutedAlias> dra) {
// this switch must have a case for every element of the RoutedAliasTypes enum EXCEPT DIMENSIONAL
switch (type) {
case TIME:
return new TimeRoutedAliasDimension(props, index, dra);
case CATEGORY:
return new CategoryRoutedAliasDimension(props, index, dra);
default:
// if we got a type not handled by the switch there's been a bogus implementation.
throw new SolrException(SERVER_ERROR, "Router " + type + " is not fully implemented. If you see this" +
"error in an official release please file a bug report. Available types were:"
+ Arrays.asList(RoutedAliasTypes.values()));
}
}
@Override
public boolean updateParsedCollectionAliases(ZkStateReader zkStateReader, boolean contextualize) {
boolean result = false;
for (RoutedAlias dimension : dimensions) {
result |= dimension.updateParsedCollectionAliases(zkStateReader, contextualize);
}
return result;
}
@Override
public String computeInitialCollectionName() {
StringBuilder sb = new StringBuilder(getAliasName());
for (RoutedAlias dimension : dimensions) {
// N. B. getAliasName is generally safe as a regex because it must conform to collection naming rules
// and those rules exclude regex special characters. A malicious request might do something expensive, but
// if you have malicious users able to run admin commands and create aliases, it is very likely that you have
// much bigger problems than an expensive regex.
String routeString = dimension.computeInitialCollectionName().replaceAll(dimension.getAliasName() , "");
sb.append(routeString);
}
return sb.toString();
}
@Override
String[] formattedRouteValues(SolrInputDocument doc) {
String[] result = new String[dimensions.size()];
for (int i = 0; i < dimensions.size(); i++) {
RoutedAlias dimension = dimensions.get(i);
result[i] = dimension.formattedRouteValues(doc)[0];
}
return result;
}
@Override
public String getAliasName() {
return name;
}
@Override
public String getRouteField() {
throw new UnsupportedOperationException("DRA's route via their dimensions, this method should not be called");
}
@Override
public RoutedAliasTypes getRoutedAliasType() {
return RoutedAliasTypes.DIMENSIONAL;
}
@Override
public void validateRouteValue(AddUpdateCommand cmd) throws SolrException {
for (RoutedAlias dimension : dimensions) {
dimension.validateRouteValue(cmd);
}
}
@Override
public Map<String, String> getAliasMetadata() {
return aliasMetadata;
}
@Override
public Set<String> getRequiredParams() {
if (reqParams.size() == 0) {
indexParams(reqParams, dimensions, RoutedAlias::getRequiredParams);
// the top level Dimensional[foo,bar] designation needs to be retained
reqParams.add(ROUTER_TYPE_NAME);
reqParams.add(ROUTER_FIELD);
}
return reqParams;
}
@Override
public Set<String> getOptionalParams() {
if (optParams.size() == 0) {
indexParams(optParams, dimensions, RoutedAlias::getOptionalParams);
}
return optParams;
}
@Override
public CandidateCollection findCandidateGivenValue(AddUpdateCommand cmd) {
contextualizeDimensions(formattedRouteValues(cmd.solrDoc));
List<CandidateCollection> subPartCandidates = new ArrayList<>();
for (RoutedAlias dimension : dimensions) {
subPartCandidates.add(dimension.findCandidateGivenValue(cmd));
}
StringBuilder col2Create = new StringBuilder(getAliasName());
StringBuilder destCol = new StringBuilder(getAliasName());
CreationType max = CreationType.NONE;
for (CandidateCollection subCol : subPartCandidates) {
col2Create.append(subCol.getCreationCollection());
destCol.append(subCol.getDestinationCollection());
if (subCol.getCreationType().ordinal() > max.ordinal()) {
max = subCol.getCreationType();
}
}
return new CandidateCollection(max,destCol.toString(),col2Create.toString());
}
@Override
protected String getHeadCollectionIfOrdered(AddUpdateCommand cmd) {
StringBuilder head = new StringBuilder(getAliasName());
for (RoutedAlias dimension : dimensions) {
head.append(dimension.getHeadCollectionIfOrdered(cmd).substring(getAliasName().length()));
}
return head.toString();
}
/**
* Determine the combination of adds/deletes implied by the arrival of a document destined for the
* specified collection.
*
* @param targetCol the collection for which a document is destined.
* @return A list of actions across the DRA.
*/
@Override
protected List<Action> calculateActions(String targetCol) {
String[] routeValues = SEP_MATCHER.split(targetCol);
// remove the alias name to avoid all manner of off by one errors...
routeValues = Arrays.copyOfRange(routeValues,1,routeValues.length);
List<List<Action>> dimActs = new ArrayList<>(routeValues.length);
contextualizeDimensions(routeValues);
for (int i = 0; i < routeValues.length; i++) {
String routeValue = routeValues[i];
RoutedAlias dim = dimensions.get(i);
dimActs.add(dim.calculateActions(dim.getAliasName() + getSeparatorPrefix(dim)+ routeValue) );
}
Set <Action> result = new LinkedHashSet<>();
StringBuilder currentSuffix = new StringBuilder();
for (int i = routeValues.length -1; i >=0 ; i--) { // also lowest up to match
String routeValue = routeValues[i];
RoutedAlias dim = dimensions.get(i);
String dimStr = dim.getRoutedAliasType().getSeparatorPrefix() + routeValue;
List<Action> actions = dimActs.get(i);
for (Iterator<Action> iterator = actions.iterator(); iterator.hasNext(); ) {
Action action = iterator.next();
iterator.remove();
result.add(new Action(action.sourceAlias, action.actionType, action.targetCollection + currentSuffix));
}
result.addAll(actions);
Set <Action> revisedResult = new LinkedHashSet<>();
for (Action action : result) {
if (action.sourceAlias == dim) {
revisedResult.add(action); // should already have the present value
continue;
}
// the rest are from lower dimensions and thus require a prefix.
revisedResult.add(new Action(action.sourceAlias, action.actionType,dimStr + action.targetCollection));
}
result = revisedResult;
currentSuffix.append(dimStr);
}
Set <Action> revisedResult = new LinkedHashSet<>();
for (Action action : result) {
revisedResult.add(new Action(action.sourceAlias, action.actionType,getAliasName() + action.targetCollection));
}
return new ArrayList<>(revisedResult);
}
private void contextualizeDimensions(String[] routeValues) {
for (RoutedAlias dimension : dimensions) {
((DraContextualized)dimension).setContext(routeValues);
}
}
private static String getSeparatorPrefix(RoutedAlias dim) {
return dim.getRoutedAliasType().getSeparatorPrefix();
}
private static void indexParams(Set<String> result, List<RoutedAlias> dimensions, Function<RoutedAlias, Set<String>> supplier) {
for (int i = 0; i < dimensions.size(); i++) {
RoutedAlias dimension = dimensions.get(i);
Set<String> params = supplier.apply(dimension);
for (String param : params) {
addDimensionIndexIfRequired(result, i, param);
}
}
}
private interface DraContextualized {
static List<String> dimensionCollectionListView(int index, Aliases aliases, Deffered<DimensionalRoutedAlias> dra, String[] context, boolean ordered) {
List<String> cols = aliases.getCollectionAliasListMap().get(dra.get().name);
LinkedHashSet<String> view = new LinkedHashSet<>(cols.size());
List<RoutedAlias> dimensions = dra.get().dimensions;
for (String col : cols) {
Matcher m = SEP_MATCHER.matcher(col);
if (!m.find()) {
throw new IllegalStateException("Invalid Dimensional Routed Alias name:" + col);
}
String[] split = SEP_MATCHER.split(col);
if (split.length != dimensions.size() + 1) {
throw new IllegalStateException("Dimension Routed Alias collection with wrong number of dimensions. (" +
col + ") expecting " + dimensions.stream().map(d ->
d.getRoutedAliasType().toString()).collect(Collectors.toList()));
}
boolean matchesAllHigherDims = index == 0;
boolean matchesAllLowerDims = context == null || index == context.length - 1;
if (context != null) {
for (int i = 0; i < context.length; i++) {
if (i == index) {
continue;
}
String s = split[i+1];
String ctx = context[i];
if (i <= index) {
matchesAllHigherDims |= s.equals(ctx);
} else {
matchesAllLowerDims |= s.equals(ctx);
}
}
} else {
matchesAllHigherDims = true;
matchesAllLowerDims = true;
}
// dimensions with an implicit order need to start from their initial configuration
// and count up to maintain order in the alias collection list with respect to that dimension
if (matchesAllHigherDims && !ordered || matchesAllHigherDims && matchesAllLowerDims) {
view.add("" + getSeparatorPrefix(dimensions.get(index)) + split[index + 1]);
}
}
return new ArrayList<>(view);
}
void setContext(String[] context);
}
private static class TimeRoutedAliasDimension extends TimeRoutedAlias implements DraContextualized {
private final int index;
private final Deffered<DimensionalRoutedAlias> dra;
private String[] context;
TimeRoutedAliasDimension(Map<String, String> props, int index, Deffered<DimensionalRoutedAlias> dra) throws SolrException {
super("", props);
this.index = index;
this.dra = dra;
}
@Override
List<String> getCollectionList(Aliases aliases) {
return DraContextualized.dimensionCollectionListView(index, aliases, dra, context, true);
}
@Override
public void setContext(String[] context) {
this.context = context;
}
}
private static class CategoryRoutedAliasDimension extends CategoryRoutedAlias implements DraContextualized {
private final int index;
private final Deffered<DimensionalRoutedAlias> dra;
private String[] context;
CategoryRoutedAliasDimension(Map<String, String> props, int index, Deffered<DimensionalRoutedAlias> dra) {
super("", props);
this.index = index;
this.dra = dra;
}
@Override
List<String> getCollectionList(Aliases aliases) {
return DraContextualized.dimensionCollectionListView(index, aliases, dra, context, false);
}
@Override
public void setContext(String[] context) {
this.context = context;
}
}
}