| // Copyright (C) 2019 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 com.google.common.collect.Sets; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory; |
| import com.googlesource.gerrit.plugins.task.TaskConfig.Task; |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| 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; |
| |
| /** Use to expand properties like ${_name} in the text of various definitions. */ |
| public class Properties { |
| public static final Properties EMPTY_PARENT = new Properties(); |
| |
| protected final Properties parentProperties; |
| protected final Task origTask; |
| protected final CopyOnWrite<Task> task; |
| protected Expander expander; |
| protected Loader loader; |
| protected boolean init = true; |
| protected boolean isTaskRefreshNeeded; |
| protected boolean isSubNodeReloadRequired; |
| |
| public Properties() { |
| this(null, null); |
| expander = new Expander(n -> ""); |
| } |
| |
| public Properties(Task origTask, Properties parentProperties) { |
| this.origTask = origTask; |
| this.parentProperties = parentProperties; |
| task = new CopyOnWrite<>(origTask, t -> origTask.config.new Task(t)); |
| } |
| |
| /** Use to expand properties specifically for Tasks. */ |
| public Task getTask(ChangeData changeData) throws StorageException { |
| if (loader != null && loader.isNonTaskDefinedPropertyLoaded()) { |
| // To detect NamesFactories dependent on non task defined properties, the checking must be |
| // done after subnodes are fully loaded, which unfortunately happens after getTask() is |
| // called. However, these non task property uses from the last change are still detectable |
| // here before we replace the old Loader with a new one. |
| isSubNodeReloadRequired = true; |
| } |
| |
| loader = new Loader(changeData); |
| expander = new Expander(n -> loader.load(n)); |
| |
| if (isTaskRefreshNeeded || init) { |
| Map<String, String> exported = expander.expand(origTask.exported); |
| if (exported != origTask.exported) { |
| task.getForWrite().exported = exported; |
| } |
| |
| expander.expand(task, Collections.emptySet()); |
| |
| if (init) { |
| init = false; |
| isTaskRefreshNeeded = loader.isNonTaskDefinedPropertyLoaded(); |
| } |
| } |
| return task.getForRead(); |
| } |
| |
| public boolean isSubNodeReloadRequired() { |
| return isSubNodeReloadRequired; |
| } |
| |
| /** Use to expand properties specifically for NamesFactories. */ |
| public NamesFactory getNamesFactory(NamesFactory namesFactory) { |
| return expander.expand( |
| namesFactory, |
| nf -> namesFactory.config.new NamesFactory(nf), |
| Sets.newHashSet(TaskConfig.KEY_TYPE)); |
| } |
| |
| protected class Loader { |
| protected final ChangeData changeData; |
| protected final Function<String, String> inheritedMapper; |
| protected Change change; |
| protected boolean isInheritedPropertyLoaded; |
| |
| public Loader(ChangeData changeData) { |
| this.changeData = changeData; |
| if (parentProperties == null || parentProperties.expander == null) { |
| inheritedMapper = n -> ""; |
| } else { |
| inheritedMapper = n -> parentProperties.expander.getValueForName(n); |
| } |
| } |
| |
| public boolean isNonTaskDefinedPropertyLoaded() { |
| return change != null || isInheritedPropertyLoaded; |
| } |
| |
| public String load(String name) { |
| if (name.startsWith("_")) { |
| return internal(name); |
| } |
| String value = origTask.exported.get(name); |
| if (value == null) { |
| value = origTask.properties.get(name); |
| if (value == null) { |
| value = inheritedMapper.apply(name); |
| if (!value.isEmpty()) { |
| isInheritedPropertyLoaded = true; |
| } |
| } |
| } |
| return value; |
| } |
| |
| protected String internal(String name) { |
| if ("_name".equals(name)) { |
| return origTask.name(); |
| } |
| String changeProp = name.replace("_change_", ""); |
| if (changeProp != name) { |
| try { |
| return change(changeProp); |
| } catch (StorageException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| return ""; |
| } |
| |
| protected String change(String changeProp) throws StorageException { |
| switch (changeProp) { |
| case "number": |
| return String.valueOf(change().getId().get()); |
| case "id": |
| return change().getKey().get(); |
| case "project": |
| return change().getProject().get(); |
| case "branch": |
| return change().getDest().branch(); |
| case "status": |
| return change().getStatus().toString(); |
| case "topic": |
| return change().getTopic(); |
| default: |
| return ""; |
| } |
| } |
| |
| protected Change change() { |
| if (change == null) { |
| try { |
| change = changeData.change(); |
| } catch (StorageException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| return change; |
| } |
| } |
| |
| /** |
| * Use to expand properties whose values may contain other references to properties. |
| * |
| * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there |
| * are no looping definitions. |
| * |
| * <p>Given some property name/value asssociations defined like this: |
| * |
| * <p><code> |
| * valueByName.put("obstacle", "fence"); |
| * valueByName.put("action", "jumped over the ${obstacle}"); |
| * </code> |
| * |
| * <p>a String like: <code>"The brown fox ${action}."</code> |
| * |
| * <p>will expand to: <code>"The brown fox jumped over the fence."</code> |
| */ |
| protected static class Expander extends AbstractExpander { |
| protected final Function<String, String> loadingFunction; |
| protected final Map<String, String> valueByName = new HashMap<>(); |
| protected final Set<String> expanding = new HashSet<>(); |
| |
| public Expander(Function<String, String> loadingFunction) { |
| this.loadingFunction = loadingFunction; |
| } |
| |
| /** |
| * Expand all properties (${property_name} -> property_value) in the given text. Returns same |
| * object if no expansions occured. |
| */ |
| public Map<String, String> expand(Map<String, String> map) { |
| if (map != null) { |
| boolean hasProperty = false; |
| Map<String, String> expandedMap = new HashMap<>(map.size()); |
| for (Map.Entry<String, String> e : map.entrySet()) { |
| String name = e.getKey(); |
| String value = e.getValue(); |
| String expanded = getValueForName(name); |
| hasProperty = hasProperty || value != expanded; |
| expandedMap.put(name, expanded); |
| } |
| return hasProperty ? Collections.unmodifiableMap(expandedMap) : map; |
| } |
| return null; |
| } |
| |
| @Override |
| public String getValueForName(String name) { |
| String value = valueByName.get(name); |
| if (value != null) { |
| return value; |
| } |
| value = loadingFunction.apply(name); |
| if (value == null) { |
| value = ""; |
| } else if (!value.isEmpty()) { |
| if (!expanding.add(name)) { |
| throw new RuntimeException("Looping property definitions."); |
| } |
| value = expandText(value); |
| expanding.remove(name); |
| } |
| valueByName.put(name, value); |
| return value; |
| } |
| } |
| |
| /** |
| * Use to expand properties like ${property} in Strings into their values. |
| * |
| * <p>Given some property name/value associations like this: |
| * |
| * <p><code> |
| * "animal" -> "fox" |
| * "bar" -> "foo" |
| * "obstacle" -> "fence" |
| * </code> |
| * |
| * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code> |
| * |
| * <p>will expand to: <code>"The brown fox jumped over the fence."</code> This class is meant to |
| * be used as a building block for other full featured expanders and thus must be overriden to |
| * provide the name/value associations via the getValueForName() method. |
| */ |
| protected abstract static class AbstractExpander { |
| // "${_name}" -> group(1) = "_name" |
| protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); |
| |
| /** |
| * Returns expanded object if property found in the Strings in the object's Fields (except the |
| * excluded ones). Returns same object if no expansions occured. |
| */ |
| public <T> T expand(T object, Function<T, T> copier, Set<String> excludedFieldNames) { |
| return expand(new CopyOnWrite<>(object, copier), excludedFieldNames); |
| } |
| |
| /** |
| * Returns expanded object if property found in the Strings in the object's Fields (except the |
| * excluded ones). Returns same object if no expansions occured. |
| */ |
| public <T> T expand(CopyOnWrite<T> cow, Set<String> excludedFieldNames) { |
| for (Field field : cow.getOriginal().getClass().getFields()) { |
| try { |
| if (!excludedFieldNames.contains(field.getName())) { |
| field.setAccessible(true); |
| Object o = field.get(cow.getOriginal()); |
| if (o instanceof String) { |
| String expanded = expandText((String) o); |
| if (expanded != o) { |
| field.set(cow.getForWrite(), expanded); |
| } |
| } else if (o instanceof List) { |
| @SuppressWarnings("unchecked") |
| List<String> forceCheck = List.class.cast(o); |
| List<String> expanded = expand(forceCheck); |
| if (expanded != o) { |
| field.set(cow.getForWrite(), expanded); |
| } |
| } |
| } |
| } catch (IllegalAccessException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| return cow.getForRead(); |
| } |
| |
| /** |
| * Returns expanded unmodifiable List if property found. Returns same object if no expansions |
| * occured. |
| */ |
| public List<String> expand(List<String> list) { |
| if (list != null) { |
| boolean hasProperty = false; |
| List<String> expandedList = new ArrayList<>(list.size()); |
| for (String value : list) { |
| String expanded = expandText(value); |
| hasProperty = hasProperty || value != expanded; |
| expandedList.add(expanded); |
| } |
| return hasProperty ? Collections.unmodifiableList(expandedList) : list; |
| } |
| return null; |
| } |
| |
| /** |
| * Expand all properties (${property_name} -> property_value) in the given text . Returns same |
| * object if no expansions occured. |
| */ |
| public String expandText(String text) { |
| if (text == null) { |
| return null; |
| } |
| StringBuffer out = new StringBuffer(); |
| Matcher m = PATTERN.matcher(text); |
| if (!m.find()) { |
| return text; |
| } |
| do { |
| m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1)))); |
| } while (m.find()); |
| m.appendTail(out); |
| return out.toString(); |
| } |
| |
| /** |
| * Get the replacement value for the property identified by name |
| * |
| * @param name of the property to get the replacement value for |
| * @return the replacement value. Since the expandText() method alwyas needs a String to replace |
| * '${property-name}' reference with, even when the property does not exist, this will never |
| * return null, instead it will returns the empty string if the property is not found. |
| */ |
| protected abstract String getValueForName(String name); |
| } |
| } |