/*
* The MIT License
*
* Copyright (c) 2015 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.plugins.promoted_builds.util;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.TopLevelItem;
import hudson.plugins.promoted_builds.parameters.PromotedBuildParameterDefinition;
import java.util.StringTokenizer;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
//TODO: Replace by methods in the Jenkins core when they become available
/**
* Implements an engine, which allows to resolve {@link Item}s by their paths.
* The engine supports both relative and absolute addressing.
* @author Oleg Nenashev
* @since 2.22
*/
public class ItemPathResolver {
/**
* Optional configuration, which enables the legacy behavior in
* {@link #getByPath(java.lang.String, hudson.model.Item, java.lang.Class)}
*/
@Restricted(NoExternalUse.class)
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL",
justification = "We want to allow modifying it from Groovy scripts as a last resort")
private static boolean ENABLE_LEGACY_RESOLUTION_AGAINST_ROOT =
Boolean.getBoolean(ItemPathResolver.class+".enableResolutionAgainstRoot");
/**
* Check if the legacy path resolution mode is enabled.
* The resolution uses available {@link ResolverManager}s and falls back to
* {@link #ENABLE_LEGACY_RESOLUTION_AGAINST_ROOT} if there is no decision.
* @return True if the legacy resolution engine is enabled
*/
public static boolean isEnableLegacyResolutionAgainstRoot() {
final Jenkins jenkins = Jenkins.getInstance();
if (jenkins != null) {
ExtensionList<ResolverManager> extensions = jenkins.getExtensionList(ResolverManager.class);
for (ResolverManager manager : extensions) {
Boolean enableLegacyItemPathResolutionMode = manager.isEnableLegacyItemPathResolutionMode();
if (enableLegacyItemPathResolutionMode != null) {
return enableLegacyItemPathResolutionMode;
}
}
}
return ENABLE_LEGACY_RESOLUTION_AGAINST_ROOT;
}
/**
* Gets an {@link Item} of the specified type by absolute or relative path.
* <p>
* The implementation retains the original behavior in {@link PromotedBuildParameterDefinition},
* but this method also provides a support of multi-level addressing including special markups
* for the relative addressing.
* </p>
* Effectively, the resolution order is following:
* <ul>
* <li><b>Optional</b> Legacy behavior, which can be enabled by {@link #ENABLE_LEGACY_RESOLUTION_AGAINST_ROOT}.
* If an item for the name exists on the top Jenkins level, it will be returned</li>
* <li>If the path starts with "/", a global addressing will be used</li>
* <li>If the path starts with "./" or "../", a relative addressing will be used</li>
* <li>If there is no prefix, a relative addressing will be tried. If it
* fails, the method falls back to a global one</li>
* </ul>
* For the relative and absolute addressing the engine supports "." and
* ".." markers within the path.
* The first one points to the current element, the second one - to the upper element.
* If the search cannot get a new top element (e.g. reached the root), the method returns {@code null}.
*
* @param <T> Type of the {@link Item} to be retrieved
* @param path Path string to the item.
* @param baseItem Base {@link Item} for the relative addressing. If null,
* this addressing approach will be skipped
* @param type Type of the {@link Item} to be retrieved
* @return Found {@link Item}. Null if it has not been found by all addressing modes
* or the type differs.
*/
@CheckForNull
@SuppressWarnings("unchecked")
@Restricted(NoExternalUse.class)
public static <T extends Item> T getByPath(@Nonnull String path,
@CheckForNull Item baseItem, @Nonnull Class<T> type) {
final Jenkins jenkins = Jenkins.getInstance();
if (jenkins == null) {
return null;
}
// Legacy behavior
if (isEnableLegacyResolutionAgainstRoot()) {
TopLevelItem topLevelItem = jenkins.getItem(path);
if (topLevelItem != null && type.isAssignableFrom(topLevelItem.getClass())) {
return (T)topLevelItem;
}
}
// Explicit global addressing
if (path.startsWith("/")) {
return findPath(jenkins, path.substring(1), type);
}
// Try the relative addressing if possible
if (baseItem != null) {
final ItemGroup<?> relativeRoot = baseItem instanceof ItemGroup<?>
? (ItemGroup<?>)baseItem : baseItem.getParent();
final T item = findPath(relativeRoot, path, type);
if (item != null) {
return item;
}
}
// Fallback to the default behavior (addressing from the Jenkins root)
return findPath(jenkins, path, type);
}
@CheckForNull
@SuppressWarnings("unchecked")
private static <T extends Item> T findPath(@CheckForNull ItemGroup base,
@Nonnull String path, @Nonnull Class<T> type) {
Item item = findPath(base, path);
if (item != null && type.isAssignableFrom(item.getClass())) {
return (T) item;
}
return null;
}
@CheckForNull
private static Item findPath(@CheckForNull ItemGroup base, @Nonnull String path) {
@CheckForNull ItemGroup<?> pointer = base;
final StringTokenizer t = new StringTokenizer(path, "/");
while(pointer != null && t.hasMoreTokens()) {
String current = t.nextToken();
if (current.equals("..")) {
if (pointer instanceof Item) {
Item currentItem = (Item)pointer;
pointer = currentItem.getParent();
} else {
pointer = null; // Cannot go upstairs
}
} else if (current.equals(".")) {
// Do nothing, we stay on the same level
} else {
// Resolve the level beneath
final Item item = pointer.getItem(current);
if (!t.hasMoreTokens()) {
// Last token => we consider it as a required item
return item;
}
if (item instanceof ItemGroup<?>) {
pointer = (ItemGroup<?>) item;
} else {
pointer = null; // Wrong path, we got to item before finishing the requested path
}
}
}
// Cannot retrieve the path => exit with null
if (pointer instanceof Item) {
return (Item)pointer;
}
return null;
}
/**
* External manager, which allows the alter behavior on-demand.
* Currently this {@link ExtensionPoint} is designed for the internal use only
* @since 2.22
*/
@Restricted(NoExternalUse.class)
public static abstract class ResolverManager implements ExtensionPoint {
/**
* Alters the item path resolution mode.
* @return true if the manager made a decision. null by default
*/
@CheckForNull
@SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "Third state for the logic")
public Boolean isEnableLegacyItemPathResolutionMode() {
return null;
}
}
}