blob: ab4d48ffaccfca62d5897e5d7e4cff20d0acd67a [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// 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 com.googlesource.gerrit.plugins.task;
import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.task.TaskConfig.External;
import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
/**
* Add structure to access the task definitions from the config as a tree.
*
* <p>This class is a "middle" representation of the task tree. The task config is represented as a
* lazily loaded tree, and much of the tree validity is enforced at this layer.
*/
public class TaskTree {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
@FunctionalInterface
public interface NodeFactory {
Node create(NodeList parent, Task definition) throws Exception;
}
protected static final String TASK_DIR = "task";
protected final AccountResolver accountResolver;
protected final AllUsersNameProvider allUsers;
protected final CurrentUser user;
protected final Preloader preloader;
protected final NodeList root = new NodeList();
protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
protected final Map<SubSectionKey, List<Task>> definitionsBySubSection = new HashMap<>();
protected ChangeData changeData;
@Inject
public TaskTree(
AccountResolver accountResolver,
AllUsersNameProvider allUsers,
AnonymousUser anonymousUser,
CurrentUser user,
Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
Preloader preloader) {
this.accountResolver = accountResolver;
this.allUsers = allUsers;
this.user = user != null ? user : anonymousUser;
this.changeQueryProcessorProvider = changeQueryProcessorProvider;
this.changeQueryBuilderProvider = changeQueryBuilderProvider;
this.preloader = preloader;
}
public void masquerade(PatchSetArgument psa) {
preloader.masquerade(psa);
}
public List<Node> getRootNodes(ChangeData changeData)
throws ConfigInvalidException, IOException, OrmException {
this.changeData = changeData;
root.path = Collections.emptyList();
root.duplicateKeys = Collections.emptyList();
return root.getSubNodes();
}
protected class NodeList {
protected NodeList parent = null;
protected Collection<String> path;
protected Collection<String> duplicateKeys;
protected Map<TaskKey, Node> cachedNodeByTask = new HashMap<>();
protected List<Node> cachedNodes;
public List<Node> getSubNodes() throws ConfigInvalidException, IOException, OrmException {
if (cachedNodes != null) {
return refresh(cachedNodes);
}
return cachedNodes = loadSubNodes();
}
protected List<Node> loadSubNodes() throws ConfigInvalidException, IOException, OrmException {
return new SubNodeFactory().createFromPreloaded(preloader.getRootTasks());
}
public ChangeData getChangeData() {
return TaskTree.this.changeData;
}
protected boolean isTrusted() {
return true;
}
protected class SubNodeFactory {
protected Set<String> names = new HashSet<>();
public List<Node> createFromPreloaded(List<Task> defs)
throws ConfigInvalidException, OrmException {
List<Node> nodes = new ArrayList<>();
for (Task def : defs) {
nodes.add(createFromPreloaded(def));
}
return nodes;
}
public Node createFromPreloaded(Task def) throws ConfigInvalidException, OrmException {
return createFromPreloaded(def, (parent, definition) -> new Node(parent, definition));
}
public Node createFromPreloaded(Task def, ChangeData changeData)
throws ConfigInvalidException, OrmException {
return createFromPreloaded(
def,
(parent, definition) ->
new Node(parent, definition) {
@Override
public ChangeData getChangeData() {
return changeData;
}
@Override
public boolean isChange() {
return true;
}
});
}
protected Node createFromPreloaded(Task def, NodeFactory nodeFactory)
throws ConfigInvalidException, OrmException {
if (def != null) {
try {
Node node = cachedNodeByTask.get(def.key());
boolean isRefreshNeeded = node != null;
if (node == null) {
node = nodeFactory.create(NodeList.this, def);
}
if (names.add(def.name())) {
// names check above detects duplicate subtasks
if (isRefreshNeeded) {
node.refreshTask();
}
return node;
}
} catch (Exception e) {
}
}
return createInvalid();
}
protected Node createInvalid() {
return new Node().new Invalid();
}
}
}
public class Node extends NodeList {
public class Invalid extends Node {
@Override
public void refreshTask() throws ConfigInvalidException, OrmException {}
@Override
public Task getDefinition() {
return null;
}
}
public Task task;
public boolean isDuplicate;
protected final Properties properties;
protected final TaskKey taskKey;
protected Map<Branch.NameKey, List<Node>> nodesByBranch;
protected boolean hasUnfilterableSubNodes = false;
protected Node() { // Only for Invalid
taskKey = null;
properties = null;
}
public Node(NodeList parent, Task task) throws ConfigInvalidException, OrmException {
this.parent = parent;
taskKey = task.key();
properties = new Properties(this, task);
refreshTask();
}
public String key() {
return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey;
}
public List<Node> getSubNodes() throws ConfigInvalidException, IOException, OrmException {
if (cachedNodes != null) {
return refresh(cachedNodes);
}
List<Node> nodes = loadSubNodes();
if (!properties.isSubNodeReloadRequired()) {
if (!isChange()) {
return cachedNodes = nodes;
}
definitionsBySubSection.computeIfAbsent(
task.key().subSection(),
k -> nodes.stream().map(n -> n.getDefinition()).collect(toList()));
} else {
hasUnfilterableSubNodes = true;
cachedNodeByTask.clear();
nodes.stream()
.filter(n -> !(n instanceof Invalid) && !n.isChange())
.forEach(n -> cachedNodeByTask.put(n.task.key(), n));
}
return nodes;
}
public List<Node> getSubNodes(MatchCache matchCache)
throws ConfigInvalidException, IOException, OrmException {
if (hasUnfilterableSubNodes) {
return getSubNodes();
}
return new ApplicableNodeFilter(matchCache).getSubNodes();
}
@Override
protected List<Node> loadSubNodes() throws ConfigInvalidException, IOException, OrmException {
List<Task> cachedDefinitions = definitionsBySubSection.get(task.key().subSection());
if (cachedDefinitions != null) {
return new SubNodeFactory().createFromPreloaded(cachedDefinitions);
}
List<Node> nodes = new SubNodeAdder().getSubNodes();
properties.expansionComplete();
return nodes;
}
/* The task needs to be refreshed before a node is used, however
subNode refreshing can wait until they are fetched since they may
not be needed. */
public void refreshTask() throws ConfigInvalidException, OrmException {
this.path = new LinkedList<>(parent.path);
String key = key();
isDuplicate = path.contains(key);
path.add(key);
this.task = properties.getTask(getChangeData());
this.duplicateKeys = new LinkedList<>(parent.duplicateKeys);
if (task.duplicateKey != null) {
isDuplicate |= duplicateKeys.contains(task.duplicateKey);
duplicateKeys.add(task.duplicateKey);
}
}
protected Properties getParentProperties() {
return (parent instanceof Node) ? ((Node) parent).properties : Properties.EMPTY;
}
@Override
protected boolean isTrusted() {
return parent.isTrusted() && !task.isMasqueraded;
}
@Override
public ChangeData getChangeData() {
return parent.getChangeData();
}
public Task getDefinition() {
return properties.origTask;
}
public boolean isChange() {
return false;
}
protected class SubNodeAdder {
protected List<Node> nodes = new ArrayList<>();
protected SubNodeFactory factory = new SubNodeFactory();
public List<Node> getSubNodes() throws ConfigInvalidException, IOException, OrmException {
addSubTasks();
addSubTasksFactoryTasks();
addSubTasksFiles();
addSubTasksExternals();
return nodes;
}
protected void addSubTasks() throws ConfigInvalidException, IOException, OrmException {
for (String expression : task.subTasks) {
try {
Optional<Task> def =
preloader.getOptionalTask(new TaskExpression(task.file(), expression));
if (def.isPresent()) {
addPreloaded(def.get());
}
} catch (ConfigInvalidException e) {
addInvalidNode();
}
}
}
protected void addSubTasksFiles() throws ConfigInvalidException, OrmException {
for (String file : task.subTasksFiles) {
try {
addPreloaded(
preloader.getTasks(FileKey.create(task.key().branch(), resolveTaskFileName(file))));
} catch (ConfigInvalidException | IOException e) {
addInvalidNode();
}
}
}
protected void addSubTasksExternals() throws ConfigInvalidException, OrmException {
for (String external : task.subTasksExternals) {
try {
External ext = task.config.getExternal(external);
if (ext == null) {
addInvalidNode();
} else {
addPreloaded(getPreloadedTasks(ext));
}
} catch (ConfigInvalidException | IOException e) {
addInvalidNode();
}
}
}
protected void addSubTasksFactoryTasks()
throws ConfigInvalidException, IOException, OrmException {
for (String tasksFactoryName : task.subTasksFactories) {
TasksFactory tasksFactory = task.config.getTasksFactory(tasksFactoryName);
if (tasksFactory != null) {
NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
if (namesFactory != null && namesFactory.type != null) {
namesFactory = properties.getNamesFactory(namesFactory);
switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
case STATIC:
addStaticTypeTasks(tasksFactory, namesFactory);
continue;
case CHANGE:
addChangeTypeTasks(tasksFactory, namesFactory);
continue;
}
}
}
addInvalidNode();
}
}
protected void addStaticTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
throws ConfigInvalidException, IOException, OrmException {
for (String name : namesFactory.names) {
addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
}
}
protected void addChangeTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
throws ConfigInvalidException, IOException, OrmException {
try {
if (namesFactory.changes != null) {
List<ChangeData> changeDataList =
changeQueryProcessorProvider
.get()
.query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
.entities();
for (ChangeData changeData : changeDataList) {
addPreloaded(
preloader.preload(
task.config.new Task(tasksFactory, changeData.getId().toString())),
changeData);
}
return;
}
} catch (OrmException e) {
log.atSevere().withCause(e).log("ERROR: running changes query: " + namesFactory.changes);
} catch (QueryParseException e) {
}
addInvalidNode();
}
public void addPreloaded(List<Task> defs) throws ConfigInvalidException, OrmException {
nodes.addAll(factory.createFromPreloaded(defs));
}
public void addPreloaded(Task def, ChangeData changeData)
throws ConfigInvalidException, OrmException {
nodes.add(factory.createFromPreloaded(def, changeData));
}
public void addPreloaded(Task def) throws ConfigInvalidException, OrmException {
nodes.add(factory.createFromPreloaded(def));
}
public void addInvalidNode() {
nodes.add(factory.createInvalid());
}
protected List<Task> getPreloadedTasks(External external)
throws ConfigInvalidException, IOException, OrmException {
return preloader.getTasks(
FileKey.create(resolveUserBranch(external.user), resolveTaskFileName(external.file)));
}
}
public class ApplicableNodeFilter {
protected MatchCache matchCache;
protected PredicateCache pcache;
protected Branch.NameKey branch = getChangeData().change().getDest();
public ApplicableNodeFilter(MatchCache matchCache)
throws ConfigInvalidException, IOException, OrmException {
this.matchCache = matchCache;
this.pcache = matchCache.predicateCache;
}
public List<Node> getSubNodes() throws ConfigInvalidException, IOException, OrmException {
if (nodesByBranch != null) {
List<Node> nodes = nodesByBranch.get(branch);
if (nodes != null) {
return refresh(nodes);
}
}
List<Node> nodes = Node.this.getSubNodes();
if (!hasUnfilterableSubNodes && !nodes.isEmpty()) {
Optional<List<Node>> filterable = getOptionalApplicableForBranch(nodes);
if (filterable.isPresent()) {
if (nodesByBranch == null) {
nodesByBranch = new HashMap<>();
}
nodesByBranch.put(branch, filterable.get());
return filterable.get();
}
hasUnfilterableSubNodes = true;
}
return nodes;
}
protected Optional<List<Node>> getOptionalApplicableForBranch(List<Node> nodes)
throws ConfigInvalidException, IOException, OrmException {
int filterable = 0;
List<Node> applicableNodes = new ArrayList<>();
for (Node node : nodes) {
if (node instanceof Invalid) {
filterable++;
} else if (isApplicableCacheableByBranch(node)) {
filterable++;
try {
if (!matchCache.match(node.task.applicable)) {
// Correctness will not be affected if more nodes are added than necessary
// (i.e. if isApplicableCacheableByBranch() does not realize a Node is cacheable
// based on its Branch), but it is incorrect to filter out a Node now that could
// later be applicable when a property, other than its Change's destination, is
// altered.
continue;
}
} catch (QueryParseException e) {
}
}
applicableNodes.add(node);
}
// Simple heuristic to determine whether storing the filtered nodes is worth it. There
// is minor evidence to suggest that storing a large list actually hurts performance.
return (filterable > nodes.size() / 2) ? Optional.of(applicableNodes) : Optional.empty();
}
protected boolean isApplicableCacheableByBranch(Node node) {
String applicable = node.task.applicable;
if (node.properties.isApplicableRefreshRequired()) {
return false;
}
try {
return pcache.isCacheableByBranch(applicable);
} catch (QueryParseException e) {
return false;
}
}
}
}
protected String resolveTaskFileName(String file) throws ConfigInvalidException {
if (file == null) {
throw new ConfigInvalidException("External file not defined");
}
Path p = Paths.get(TASK_DIR, file);
if (!p.startsWith(TASK_DIR)) {
throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
}
return p.toString();
}
protected Branch.NameKey resolveUserBranch(String user)
throws ConfigInvalidException, IOException, OrmException {
if (user == null) {
throw new ConfigInvalidException("External user not defined");
}
Account acct = accountResolver.find(user);
if (acct == null) {
throw new ConfigInvalidException("Cannot resolve user: " + user);
}
return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct.getId()));
}
protected static List<Node> refresh(List<Node> nodes)
throws ConfigInvalidException, OrmException {
for (Node node : nodes) {
node.refreshTask();
}
return nodes;
}
}