/*
* The MIT License
*
* Copyright (c) 2016, 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 com.cloudbees.plugins.credentials;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Functions;
import hudson.model.Action;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import jenkins.model.ModelObjectWithContextMenu;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.Icon;
import org.jenkins.ui.icon.IconSet;
import org.jenkins.ui.icon.IconSpec;
import org.jenkins.ui.icon.IconType;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
/**
* Some utility methods to help working with {@link Icon}, {@link IconSet} and
* {@link ModelObjectWithContextMenu.ContextMenu}.
*
* @since 2.0
*/
@Restricted(NoExternalUse.class)
public class ContextMenuIconUtils {
/**
* Prevent instantiation.
*/
private ContextMenuIconUtils() {
throw new IllegalAccessError("Utility class");
}
/**
* Combine segments of an URL in the style expected by {@link ModelObjectWithContextMenu.MenuItem}.
*
* @param segments the segments.
* @return the combined URL.
*/
@NonNull
public static String buildUrl(String... segments) {
StringBuilder result = new StringBuilder();
boolean first = true;
for (String segment : segments) {
if (segment == null) {
continue;
}
String str = StringUtils.removeEnd(StringUtils.removeStart(segment, "/"), "/");
if (str.isEmpty()) {
continue;
}
if (first) {
first = false;
} else {
result.append('/');
}
result.append(str);
}
return result.toString();
}
/**
* Adds a menu item for the specified action with the supplied prefix offset and optional sub menu.
*
* @param menu the menu to add to.
* @param prefix the prefix offset of the action urls.
* @param action the action.
* @param subMenu the sub menu.
*/
public static void addMenuItem(@NonNull ModelObjectWithContextMenu.ContextMenu menu,
@CheckForNull String prefix, @NonNull Action action,
@CheckForNull ModelObjectWithContextMenu.ContextMenu subMenu) {
if (Functions.isContextMenuVisible(action) && action.getIconFileName() != null) {
Icon icon = action instanceof IconSpec ? ContextMenuIconUtils.getIcon(action) : null;
String base = icon != null ? ContextMenuIconUtils.getQualifiedUrl(icon) : Functions.getIconFilePath(action);
ModelObjectWithContextMenu.MenuItem item = new ModelObjectWithContextMenu.MenuItem(
ContextMenuIconUtils.buildUrl(prefix, action.getUrlName()),
ContextMenuIconUtils.getMenuItemIconUrl(base),
action.getDisplayName()
);
item.subMenu = subMenu;
menu.add(item);
}
}
/**
* Adds a menu item for the specified action with the supplied prefix offset.
*
* @param menu the menu to add to.
* @param prefix the prefix offset of the action urls.
* @param action the action.
*/
public static void addMenuItem(@NonNull ModelObjectWithContextMenu.ContextMenu menu, @CheckForNull String prefix,
@NonNull Action action) {
addMenuItem(menu, null, action, null);
}
/**
* Adds a menu item for the specified action.
*
* @param menu the menu to add to.
* @param action the action.
*/
public static void addMenuItem(@NonNull ModelObjectWithContextMenu.ContextMenu menu, @NonNull Action action) {
addMenuItem(menu, null, action, null);
}
/**
* Gets the qualified URL for an icon spec.
*
* @param spec the icon spec.
* @return the qualified URL for a menu item
*/
@CheckForNull
public static String getMenuItemIconUrlByClassSpec(@CheckForNull String spec) {
return getMenuItemIconUrl(getQualifiedUrl(getIconByClassSpec(spec)));
}
/**
* Converts a regular icon url into a menu item icon url
*
* @param url the "regular" icon url.
* @return the url for a menu item.
*/
public static String getMenuItemIconUrl(String url) {
if (url == null) {
return null;
}
String contextPath = Stapler.getCurrentRequest().getContextPath();
return (StringUtils.isBlank(contextPath)? "" : contextPath) + (url.startsWith("images/")
? Functions.getResourcePath()
: "") + (url.startsWith("/") ? url : '/' + url);
}
/**
* Navigate the signature changes in different versions of {@link IconSet}.
*
* @param spec the spec to look up.
* @return the {@link Icon} or {@code null}
*/
@CheckForNull
public static Icon getIconByClassSpec(String spec) {
try {
Method getIconByClassSpec = IconSet.class.getMethod("getIconByClassSpec", Object.class);
return (Icon) getIconByClassSpec.invoke(IconSet.icons, spec);
} catch (NoSuchMethodException e) {
// ignore
} catch (IllegalAccessException e) {
// ignore
} catch (InvocationTargetException e) {
// ignore
}
return IconSet.icons.getIconByClassSpec(spec);
}
/**
* {@link IconSet} support has not been implemented for {@link ModelObjectWithContextMenu.ContextMenu} as of
* Jenkins 2.5. Hacks to work around
* some of the fun method signature migrations in {@link IconSet}.
*
* @param action the action.
* @return the {@link Icon} or {@code null}
*/
@CheckForNull
public static Icon getIcon(@NonNull Action action) {
if (action.getIconFileName() == null) {
return null;
}
Icon icon;
try {
Method getIconByClassSpec = IconSet.class.getMethod("getIconByClassSpec", Object.class);
icon = action instanceof IconSpec
? (Icon) getIconByClassSpec.invoke(IconSet.icons, ((IconSpec) action).getIconClassName())
: null;
if (icon != null) {
return icon;
}
Method toNormalizedIconNameClass = IconSet.class.getMethod("toNormalizedIconNameClass", Object.class);
icon = (Icon) getIconByClassSpec
.invoke(IconSet.icons, toNormalizedIconNameClass.invoke(null, action.getIconFileName()));
if (icon != null) {
return icon;
}
Method getIconByUrl = IconSet.class.getMethod("getIconByUrl", Object.class);
return (Icon) getIconByUrl.invoke(IconSet.icons, action.getIconFileName());
} catch (NoSuchMethodException e) {
icon = action instanceof IconSpec
? IconSet.icons.getIconByClassSpec(((IconSpec) action).getIconClassName())
: null;
if (icon == null) {
icon = IconSet.icons.getIconByClassSpec(IconSet.toNormalizedIconNameClass(action.getIconFileName()));
}
if (icon == null) {
icon = IconSet.icons.getIconByUrl(action.getIconFileName());
}
} catch (InvocationTargetException e) {
icon = null;
} catch (IllegalAccessException e) {
icon = null;
}
return icon;
}
/**
* Gets the qualified URL of the specified icon.
*
* @param icon the icon.
* @return the qualified URL of the icon.
*/
@CheckForNull
public static String getQualifiedUrl(@CheckForNull Icon icon) {
if (icon == null) {
return null;
}
try {
Field iconType = Icon.class.getDeclaredField("iconType");
iconType.setAccessible(true);
IconType type = (IconType) iconType.get(icon);
switch (type) {
case CORE: {
return Functions.getResourcePath() + "/images/" + icon.getUrl();
}
case PLUGIN: {
return Functions.getResourcePath() + "/plugin/" + icon.getUrl();
}
}
return null;
} catch (NoSuchFieldException e) {
// ignore we'll use a JellyContext
} catch (IllegalAccessException e) {
// ignore we'll use a JellyContext
}
JellyContext ctx = new JellyContext();
ctx.setVariable("resURL", Functions.getResourcePath());
return icon.getQualifiedUrl(ctx);
}
}