/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* 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.model;
import com.thoughtworks.xstream.XStream;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.XmlFile;
import hudson.model.listeners.ItemListener;
import hudson.remoting.Callable;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.triggers.Trigger;
import hudson.util.DescriptorList;
import hudson.util.EditDistance;
import hudson.util.XStream2;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.lang.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import org.apache.commons.io.FileUtils;
/**
* Convenience methods related to {@link Item}.
*
* @author Kohsuke Kawaguchi
*/
public class Items {
/**
* List of all installed {@link TopLevelItem} types.
*
* @deprecated as of 1.286
* Use {@link #all()} for read access and {@link Extension} for registration.
*/
@Deprecated
public static final List<TopLevelItemDescriptor> LIST = (List)new DescriptorList<TopLevelItem>(TopLevelItem.class);
/**
* Used to behave differently when loading posted configuration as opposed to persisted configuration.
* @see Trigger#start
* @since 1.482
*/
private static final ThreadLocal<Boolean> updatingByXml = new ThreadLocal<Boolean>() {
@Override protected Boolean initialValue() {
return false;
}
};
/**
* Runs a block while making {@link #currentlyUpdatingByXml} be temporarily true.
* Use this when you are creating or changing an item.
* @param <V> a return value type (may be {@link Void})
* @param <T> an error type (may be {@link Error})
* @param callable a block, typically running {@link #load} or {@link Item#onLoad}
* @return whatever {@code callable} returned
* @throws T anything {@code callable} throws
* @since 1.546
*/
public static <V,T extends Throwable> V whileUpdatingByXml(Callable<V,T> callable) throws T {
updatingByXml.set(true);
try {
return callable.call();
} finally {
updatingByXml.set(false);
}
}
/**
* Checks whether we are in the middle of creating or configuring an item via XML.
* Used to determine the {@code newInstance} parameter for {@link Trigger#start}.
* @return true if {@link #whileUpdatingByXml} is currently being called, false for example when merely starting Jenkins or reloading from disk
* @since 1.546
*/
public static boolean currentlyUpdatingByXml() {
return updatingByXml.get();
}
/**
* Returns all the registered {@link TopLevelItemDescriptor}s.
*/
public static DescriptorExtensionList<TopLevelItem,TopLevelItemDescriptor> all() {
return Jenkins.getInstance().<TopLevelItem,TopLevelItemDescriptor>getDescriptorList(TopLevelItem.class);
}
/**
* Returns all the registered {@link TopLevelItemDescriptor}s that the current security principal is allowed to
* create within the specified item group.
*
* @since TODO
*/
public static List<TopLevelItemDescriptor> all(ItemGroup c) {
return all(Jenkins.getAuthentication(), c);
}
/**
* Returns all the registered {@link TopLevelItemDescriptor}s that the specified security principal is allowed to
* create within the specified item group.
*
* @since TODO
*/
public static List<TopLevelItemDescriptor> all(Authentication a, ItemGroup c) {
List<TopLevelItemDescriptor> result = new ArrayList<TopLevelItemDescriptor>();
ACL acl;
if (c instanceof AccessControlled) {
acl = ((AccessControlled) c).getACL();
} else {
// fall back to root
acl = Jenkins.getInstance().getACL();
}
for (TopLevelItemDescriptor d: all()) {
if (acl.hasCreatePermission(a, c, d) && d.isApplicableIn(c)) {
result.add(d);
}
}
return result;
}
/**
* @deprecated Underspecified what the parameter is. {@link Descriptor#getId}? A {@link Describable} class name?
*/
public static TopLevelItemDescriptor getDescriptor(String fqcn) {
return Descriptor.find(all(), fqcn);
}
/**
* Converts a list of items into a comma-separated list of full names.
*/
public static String toNameList(Collection<? extends Item> items) {
StringBuilder buf = new StringBuilder();
for (Item item : items) {
if(buf.length()>0)
buf.append(", ");
buf.append(item.getFullName());
}
return buf.toString();
}
/**
* @deprecated as of 1.406
* Use {@link #fromNameList(ItemGroup, String, Class)}
*/
@Deprecated
public static <T extends Item> List<T> fromNameList(String list, Class<T> type) {
return fromNameList(null,list,type);
}
/**
* Does the opposite of {@link #toNameList(Collection)}.
*/
public static <T extends Item> List<T> fromNameList(ItemGroup context, @Nonnull String list, @Nonnull Class<T> type) {
final Jenkins jenkins = Jenkins.getInstance();
List<T> r = new ArrayList<T>();
if (jenkins == null) {
return r;
}
StringTokenizer tokens = new StringTokenizer(list,",");
while(tokens.hasMoreTokens()) {
String fullName = tokens.nextToken().trim();
if (StringUtils.isNotEmpty(fullName)) {
T item = jenkins.getItem(fullName, context, type);
if(item!=null)
r.add(item);
}
}
return r;
}
/**
* Computes the canonical full name of a relative path in an {@link ItemGroup} context, handling relative
* positions ".." and "." as absolute path starting with "/". The resulting name is the item fullName from Jenkins
* root.
*/
public static String getCanonicalName(ItemGroup context, String path) {
String[] c = context.getFullName().split("/");
String[] p = path.split("/");
Stack<String> name = new Stack<String>();
for (int i=0; i<c.length;i++) {
if (i==0 && c[i].equals("")) continue;
name.push(c[i]);
}
for (int i=0; i<p.length;i++) {
if (i==0 && p[i].equals("")) {
// Absolute path starting with a "/"
name.clear();
continue;
}
if (p[i].equals("..")) {
if (name.size() == 0) {
throw new IllegalArgumentException(String.format(
"Illegal relative path '%s' within context '%s'", path, context.getFullName()
));
}
name.pop();
continue;
}
if (p[i].equals(".")) {
continue;
}
name.push(p[i]);
}
return StringUtils.join(name, '/');
}
/**
* Computes the relative name of list of items after a rename or move occurred.
* Used to manage job references as names in plugins to support {@link hudson.model.listeners.ItemListener#onLocationChanged}.
* <p>
* In a hierarchical context, when a plugin has a reference to a job as <code>../foo/bar</code> this method will
* handle the relative path as "foo" is renamed to "zot" to compute <code>../zot/bar</code>
*
* @param oldFullName the old full name of the item
* @param newFullName the new full name of the item
* @param relativeNames coma separated list of Item relative names
* @param context the {link ItemGroup} relative names refer to
* @return relative name for the renamed item, based on the same ItemGroup context
*/
public static String computeRelativeNamesAfterRenaming(String oldFullName, String newFullName, String relativeNames, ItemGroup context) {
StringTokenizer tokens = new StringTokenizer(relativeNames,",");
List<String> newValue = new ArrayList<String>();
while(tokens.hasMoreTokens()) {
String relativeName = tokens.nextToken().trim();
String canonicalName = getCanonicalName(context, relativeName);
if (canonicalName.equals(oldFullName) || canonicalName.startsWith(oldFullName+'/')) {
String newCanonicalName = newFullName + canonicalName.substring(oldFullName.length());
if (relativeName.startsWith("/")) {
newValue.add("/" + newCanonicalName);
} else {
newValue.add(getRelativeNameFrom(newCanonicalName, context.getFullName()));
}
} else {
newValue.add(relativeName);
}
}
return StringUtils.join(newValue, ",");
}
// Had difficulty adapting the version in Functions to use no live items, so rewrote it:
static String getRelativeNameFrom(String itemFullName, String groupFullName) {
String[] itemFullNameA = itemFullName.isEmpty() ? new String[0] : itemFullName.split("/");
String[] groupFullNameA = groupFullName.isEmpty() ? new String[0] : groupFullName.split("/");
for (int i = 0; ; i++) {
if (i == itemFullNameA.length) {
if (i == groupFullNameA.length) {
// itemFullName and groupFullName are identical
return ".";
} else {
// itemFullName is an ancestor of groupFullName; insert ../ for rest of groupFullName
StringBuilder b = new StringBuilder();
for (int j = 0; j < groupFullNameA.length - itemFullNameA.length; j++) {
if (j > 0) {
b.append('/');
}
b.append("..");
}
return b.toString();
}
} else if (i == groupFullNameA.length) {
// groupFullName is an ancestor of itemFullName; insert rest of itemFullName
StringBuilder b = new StringBuilder();
for (int j = i; j < itemFullNameA.length; j++) {
if (j > i) {
b.append('/');
}
b.append(itemFullNameA[j]);
}
return b.toString();
} else if (itemFullNameA[i].equals(groupFullNameA[i])) {
// identical up to this point
continue;
} else {
// first mismatch; insert ../ for rest of groupFullName, then rest of itemFullName
StringBuilder b = new StringBuilder();
for (int j = i; j < groupFullNameA.length; j++) {
if (j > i) {
b.append('/');
}
b.append("..");
}
for (int j = i; j < itemFullNameA.length; j++) {
b.append('/').append(itemFullNameA[j]);
}
return b.toString();
}
}
}
/**
* Loads a {@link Item} from a config file.
*
* @param dir
* The directory that contains the config file, not the config file itself.
*/
public static Item load(ItemGroup parent, File dir) throws IOException {
Item item = (Item)getConfigFile(dir).read();
item.onLoad(parent,dir.getName());
return item;
}
/**
* The file we save our configuration.
*/
public static XmlFile getConfigFile(File dir) {
return new XmlFile(XSTREAM,new File(dir,"config.xml"));
}
/**
* The file we save our configuration.
*/
public static XmlFile getConfigFile(Item item) {
return getConfigFile(item.getRootDir());
}
/**
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
* and filter them by the given type.
*
* @since 1.512
*/
public static <T extends Item> List<T> getAllItems(final ItemGroup root, Class<T> type) {
List<T> r = new ArrayList<T>();
getAllItems(root, type, r);
return r;
}
private static <T extends Item> void getAllItems(final ItemGroup root, Class<T> type, List<T> r) {
List<Item> items = new ArrayList<Item>(((ItemGroup<?>) root).getItems());
Collections.sort(items, new Comparator<Item>() {
@Override public int compare(Item i1, Item i2) {
return name(i1).compareToIgnoreCase(name(i2));
}
String name(Item i) {
String n = i.getName();
if (i instanceof ItemGroup) {
n += '/';
}
return n;
}
});
for (Item i : items) {
if (type.isInstance(i)) {
if (i.hasPermission(Item.READ)) {
r.add(type.cast(i));
}
}
if (i instanceof ItemGroup) {
getAllItems((ItemGroup) i, type, r);
}
}
}
/**
* Finds an item whose name (when referenced from the specified context) is closest to the given name.
* @param <T> the type of item being considered
* @param type same as {@code T}
* @param name the supplied name
* @param context a context to start from (used to compute relative names)
* @return the closest available item
* @since 1.538
*/
public static @CheckForNull <T extends Item> T findNearest(Class<T> type, String name, ItemGroup context) {
List<T> projects = Jenkins.getInstance().getAllItems(type);
String[] names = new String[projects.size()];
for (int i = 0; i < projects.size(); i++) {
names[i] = projects.get(i).getRelativeNameFrom(context);
}
String nearest = EditDistance.findNearest(name, names);
return Jenkins.getInstance().getItem(nearest, context, type);
}
/**
* Moves an item between folders (or top level).
* Fires all relevant events but does not verify that the item’s directory is not currently being used in some way (for example by a running build).
* Does not check any permissions.
* @param item some item (job or folder)
* @param destination the destination of the move (a folder or {@link Jenkins}); not the current parent (or you could just call {@link AbstractItem#renameTo})
* @return the new item (usually the same object as {@code item})
* @throws IOException if the move fails, or some subsequent step fails (directory might have already been moved)
* @throws IllegalArgumentException if the move would really be a rename, or the destination cannot accept the item, or the destination already has an item of that name
* @since 1.548
*/
public static <I extends AbstractItem & TopLevelItem> I move(I item, DirectlyModifiableTopLevelItemGroup destination) throws IOException, IllegalArgumentException {
DirectlyModifiableTopLevelItemGroup oldParent = (DirectlyModifiableTopLevelItemGroup) item.getParent();
if (oldParent == destination) {
throw new IllegalArgumentException();
}
// TODO verify that destination is to not equal to, or inside, item
if (!destination.canAdd(item)) {
throw new IllegalArgumentException();
}
String name = item.getName();
if (destination.getItem(name) != null) {
throw new IllegalArgumentException(name + " already exists");
}
String oldFullName = item.getFullName();
// TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps?
File destDir = destination.getRootDirFor(item);
FileUtils.forceMkdir(destDir.getParentFile());
FileUtils.moveDirectory(item.getRootDir(), destDir);
oldParent.remove(item);
I newItem = destination.add(item, name);
item.movedTo(destination, newItem, destDir);
ItemListener.fireLocationChange(newItem, oldFullName);
return newItem;
}
/**
* Used to load/save job configuration.
*
* When you extend {@link Job} in a plugin, try to put the alias so
* that it produces a reasonable XML.
*/
public static final XStream XSTREAM = new XStream2();
/**
* Alias to {@link #XSTREAM} so that one can access additional methods on {@link XStream2} more easily.
*/
public static final XStream2 XSTREAM2 = (XStream2)XSTREAM;
static {
XSTREAM.alias("project",FreeStyleProject.class);
}
}