blob: a7a339231669f8ad4f002caf57fb2d3e9b4a80d4 [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.sentry.provider.db.service.persistent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SentryUpgradeOrder {
private static final Logger LOGGER = LoggerFactory.getLogger(SentryUpgradeOrder.class);
/**
* Returns a directed graph in the form of a map structure that contains the allowed upgrade
* order schema versions read from a specific Reader.
* </p>
* The format of the upgrade order schema versions is as follows:
* SOURCE-to-TARGET
* </p>
* where SOURCE is the version from where to upgrade, and TARGET is the target version where
* to upgrade, i.e.
* 1.8.0-to-1.9.0
* </p>
* The reader may contain a list of upgrade path versions separated by a new line.
* 1.8.0-to-1.9.0
* 1.8.0-to-2.0.0
* 1.9.0-to-2.1.0
* </p>
* Lines starting with the # are considered comments and are ignored.
*
* @param reader A Reader object from where to read the list of upgrade path versions.
* @return A map structure that contains the directed graph.
* @throws IOException If an error occurs while reading data from the Reader object.
*/
public static Map<String, List<String>> readUpgradeGraph(Reader reader) throws IOException {
Map<String, List<String>> upgradeSchemaVersions = new HashMap<>();
try (BufferedReader bfReader = new BufferedReader(reader)) {
String schemaVersion;
while ((schemaVersion = bfReader.readLine()) != null) {
String[] versions = schemaVersion.toLowerCase().split("-to-");
// Valid upgrade versions has a source and a target version
if (versions.length != 2) {
LOGGER.warn("Ignoring unknown Sentry schema upgrade path: " + schemaVersion);
continue;
}
String source = versions[0].trim();
String target = versions[1].trim();
// Valid upgrade versions should have different source and target versions
if (source.isEmpty() || source.startsWith("#") || source.equals(target)) {
continue;
}
// There could be multiple source versions with different targets.
List<String> toVersions = upgradeSchemaVersions.getOrDefault(source, new LinkedList<>());
toVersions.add(target);
upgradeSchemaVersions.put(source, toVersions);
}
}
return upgradeSchemaVersions;
}
/**
* Returns a list of upgrade path versions sorted from lower version to higher version.
*
* @param upgrades The map structured that contains a graph of directed path upgrades.
* @param from A string of the source version to search.
* @param to A string of the target version to search.
* @return An ordered list from lower to higher versions of the upgrade path versions.
*/
public static List<String> getUpgradePath(Map<String, List<String>> upgrades, String from, String to) {
// LinkedList is used to keep the correct order in Java 8 (ArrayList is known to not preserve
// the order at the moment of insertion).
List<String> upgradeListPath = new LinkedList<>();
Stack<String> upgradePaths = new Stack<>();
Set<String> visited = new HashSet<>();
searchPath(upgrades, from, to, upgradePaths, visited);
// Reverse the stack to put the correct order in the list.
// To keep the behavior correctly, a list is returned instead of the stack to avoid the
// incorrect order obtained if a stack is used in the for() loop.
while (!upgradePaths.isEmpty()) {
upgradeListPath.add(upgradePaths.pop());
}
return upgradeListPath;
}
/**
* Searches the complete upgrade path from the list of valid upgrades.
*
* @param from The string version from where to upgrade.
* @param to The string version to upgrade.
* @param upgradePath A stack where to put the complete upgrade path. Empty if no upgrade path
* is found.
* @param visited A set where to track the visited upgrade paths.
*
* @return True if a complete upgrade path is found; False otherwise.
*/
private static boolean searchPath(Map<String, List<String>> graph, String from, String to, Stack<String> upgradePath, Set<String> visited) {
/*
* It is not necessary to find the shortest path to the upgrade as the number of upgrade
* scripts are small and do not cause too much overhead during an upgrade. So a simple depth-first
* search is done to find the requested path.
*/
if (from.equals(to)) {
return true;
}
if (!graph.containsKey(from)) {
return false;
}
boolean found = false;
for (String toVersion : graph.get(from)) {
String nextPath = generateUpgradeVersionString(from, toVersion);
boolean isVisited = visited.contains(nextPath);
if (!isVisited) {
visited.add(nextPath);
found = searchPath(graph, toVersion, to, upgradePath, visited);
if (found) {
upgradePath.push(nextPath);
break;
} else {
visited.remove(nextPath);
}
}
}
return found;
}
private static String generateUpgradeVersionString(String from, String to) {
StringBuilder sb = new StringBuilder();
sb.append(from).append("-to-").append(to);
return sb.toString();
}
}