/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, CloudBees, Inc.
*
*
*******************************************************************************/
package hudson.model;
import hudson.Util;
import static hudson.model.Hudson.JOB_NAME_LIMIT_NO_TEAM;
import hudson.model.listeners.ItemListener;
import hudson.security.AccessControlled;
import hudson.util.CopyOnWriteMap;
import hudson.util.FormValidation;
import hudson.util.Function1;
import hudson.util.IOUtils;
import hudson.util.XmlUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import org.eclipse.hudson.security.team.Team;
import org.eclipse.hudson.security.team.TeamManager;
import org.eclipse.hudson.security.team.TeamManager.TeamNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Defines a bunch of static methods to be used as a "mix-in" for
* {@link ItemGroup} implementations. Not meant for a consumption from outside
* {@link ItemGroup}s.
*
* @author Kohsuke Kawaguchi
*/
public abstract class ItemGroupMixIn {
private transient Logger logger = LoggerFactory.getLogger(ItemGroupMixIn.class);
/**
* {@link ItemGroup} for which we are working.
*/
private final ItemGroup parent;
private final AccessControlled acl;
protected ItemGroupMixIn(ItemGroup parent, AccessControlled acl) {
this.parent = parent;
this.acl = acl;
}
/*
* Callback methods to be implemented by the ItemGroup implementation.
*/
/**
* Adds a newly created item to the parent.
*/
protected abstract void add(TopLevelItem item);
/**
* Assigns the root directory for a prospective item.
*/
protected abstract File getRootDirFor(String name);
/*
* The rest is the methods that provide meat.
*/
/**
* Loads all the child {@link Item}s.
*
* @param modulesDir Directory that contains sub-directories for each child
* item.
*/
public static <K, V extends Item> Map<K, V> loadChildren(ItemGroup parent, File modulesDir, Function1<? extends K, ? super V> key) {
modulesDir.mkdirs(); // make sure it exists
File[] subdirs = modulesDir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory();
}
});
CopyOnWriteMap.Tree<K, V> configurations = new CopyOnWriteMap.Tree<K, V>();
for (File subdir : subdirs) {
try {
V item = (V) Items.load(parent, subdir, false);
configurations.put(key.call(item), item);
} catch (IOException e) {
e.printStackTrace(); // TODO: logging
}
}
return configurations;
}
/**
* {@link Item} -> name function.
*/
public static final Function1<String, Item> KEYED_BY_NAME = new Function1<String, Item>() {
public String call(Item item) {
return item.getName();
}
};
/**
* Creates a {@link TopLevelItem} from the submission of the
* '/lib/hudson/newFromList/formList' or throws an exception if it fails.
*/
public synchronized TopLevelItem createTopLevelItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
acl.checkPermission(Job.CREATE);
TopLevelItem result;
String requestContentType = req.getContentType();
if (requestContentType == null) {
throw new Failure("No Content-Type header set");
}
boolean isXmlSubmission = requestContentType.startsWith("application/xml") || requestContentType.startsWith("text/xml");
String name = req.getParameter("name");
if (name == null) {
throw new Failure("Query parameter 'name' is required");
}
String team = null;
{ // check if the name looks good
name = Hudson.checkGoodJobName(name);
Hudson hudson = Hudson.getInstance();
// see if team requested
Team requestedTeam = null;
if (hudson.isTeamManagementEnabled()) {
team = req.getParameter("team");
if (team != null){
String teamName = team.trim();
if (teamName.length() > 0) {
try {
requestedTeam = hudson.getTeamManager().findTeam(teamName);
team = teamName;
} catch (TeamManager.TeamNotFoundException ex) {
throw new Failure("Requested team " + teamName + " not found");
}
} else {
team = null;
}
}
}
String existingJobName = name;
if (hudson.isTeamManagementEnabled()){
existingJobName = requestedTeam == null
? hudson.getTeamManager().getRawTeamQualifiedJobName(name)
: hudson.getTeamManager().getRawTeamQualifiedJobName(requestedTeam, name);
}
if (parent.getItem(existingJobName) != null) {
throw new Failure(Messages.Hudson_JobAlreadyExists(existingJobName));
}
}
String mode = req.getParameter("mode");
if (mode != null && mode.equals("copy")) {
String from = req.getParameter("from");
// resolve a name to Item
Item src = parent.getItem(from);
if (src == null) {
src = Hudson.getInstance().getItemByFullName(from);
}
if (src == null) {
if (Util.fixEmpty(from) == null) {
throw new Failure("Specify which job to copy");
} else {
throw new Failure("No such job: " + from);
}
}
if (!(src instanceof TopLevelItem)) {
throw new Failure(from + " cannot be copied");
}
result = copy((TopLevelItem) src, name, team);
} else {
if (isXmlSubmission) {
result = createProjectFromXML(name, team, req.getInputStream());
rsp.setStatus(HttpServletResponse.SC_OK);
return result;
} else {
if (mode == null) {
throw new Failure("No mode given");
}
// create empty job and redirect to the project config screen
result = createProject(Items.getDescriptor(mode), name, team, true);
}
}
rsp.sendRedirect2(redirectAfterCreateItem(req, result));
return result;
}
/**
* Computes the redirection target URL for the newly created
* {@link TopLevelItem}.
*/
protected String redirectAfterCreateItem(StaplerRequest req, TopLevelItem result) throws IOException {
return req.getContextPath() + '/' + result.getUrl() + "configure";
}
/**
* Copies an existing {@link TopLevelItem} to a new name.
*
* The caller is responsible for calling
* {@link ItemListener#fireOnCopied(Item, Item)}. This method cannot do that
* because it doesn't know how to make the newly added item reachable from
* the parent.
*/
@SuppressWarnings({"unchecked" })
public synchronized <T extends TopLevelItem> T copy(T src, String name) throws IOException {
return copy(src, name, null);
}
/**
* Copies an existing {@link TopLevelItem} to a new name.
*
* The caller is responsible for calling
* {@link ItemListener#fireOnCopied(Item, Item)}. This method cannot do that
* because it doesn't know how to make the newly added item reachable from
* the parent.
*/
@SuppressWarnings({"unchecked" })
public synchronized <T extends TopLevelItem> T copy(T src, String name, String teamName) throws IOException {
acl.checkPermission(Job.CREATE);
T result = (T) createProject(src.getDescriptor(), name, teamName, false);
File jobConfigFile = Items.getConfigFile(result).getFile();
// copy config
Util.copyFile(Items.getConfigFile(src).getFile(), jobConfigFile);
try {
Document jobConfigDoc = XmlUtils.parseXmlFile(jobConfigFile);
if (XmlUtils.hasElement(jobConfigDoc, "cascadingChildrenNames")) {
XmlUtils.deleteElement(jobConfigDoc, "cascadingChildrenNames");
XmlUtils.writeXmlFile(jobConfigDoc, jobConfigFile);
}
} catch (ParserConfigurationException ex) {
logger.warn(ex.getLocalizedMessage());
} catch (SAXException ex) {
logger.warn(ex.getLocalizedMessage());
} catch (TransformerException ex) {
logger.warn(ex.getLocalizedMessage());
}
// reload from the new config
result = (T) Items.load(parent, result.getRootDir());
result.onCopiedFrom(src);
add(result);
ItemListener.fireOnCopied(src, Hudson.getInstance().getItem(result.getName()));
return result;
}
private String createInTeam(String name, String teamName) throws IOException {
name = Hudson.checkGoodJobName(name);
// To be created in a specific team, a job must first be added
// to the team, ensuring that Hudson will find the correct rootDir.
TeamManager teamManager = Hudson.getInstance().getTeamManager();
if (!teamManager.isTeamManagementEnabled()) {
if (teamName != null) {
throw new IOException("Team management is not enabled");
}
return name;
}
Team team;
if (teamName == null) {
try {
team = teamManager.findCurrentUserTeamForNewJob();
} catch (TeamNotFoundException ex) {
// Shouldn't happen, as user is already confirmed for Job.CREATE
return name;
}
} else {
try {
team = teamManager.findTeam(teamName);
} catch (TeamNotFoundException e) {
throw new IOException("Team "+teamName+" does not exist");
}
}
// addJob does the necessary name assembly and returns qualified job name
return teamManager.addJob(name, team);
}
public synchronized TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException {
return createProjectFromXML(name, null, xml);
}
public synchronized TopLevelItem createProjectFromXML(String name, String teamName, InputStream xml) throws IOException {
acl.checkPermission(Job.CREATE);
String jobName = createInTeam(name, teamName);
// place it as config.xml
File configXml = Items.getConfigFile(getRootDirFor(jobName)).getFile();
configXml.getParentFile().mkdirs();
try {
IOUtils.copy(xml, configXml);
// load it
TopLevelItem result = (TopLevelItem) Items.load(parent, configXml.getParentFile(), false);
add(result);
assert(result.getName().equals(jobName));
ItemListener.fireOnCreated(Hudson.getInstance().getItem(jobName));
Hudson.getInstance().rebuildDependencyGraph();
return result;
} catch (IOException e) {
// if anything fails, delete the config file to avoid further confusion
Hudson.getInstance().getTeamManager().removeJob(jobName);
Util.deleteRecursive(configXml.getParentFile());
throw e;
}
}
public synchronized TopLevelItem createProject(TopLevelItemDescriptor type, String name, boolean notify) throws IOException {
return createProject(type, name, null, notify);
}
public synchronized TopLevelItem createProject(TopLevelItemDescriptor type, String name, String teamName, boolean notify)
throws IOException {
acl.checkPermission(Job.CREATE);
name = createInTeam(name, teamName);
Hudson hudson = Hudson.getInstance();
String existingJobName = name;
if (hudson.isTeamManagementEnabled() && teamName == null) {
existingJobName = hudson.getTeamManager().getTeamQualifiedJobName(name);
}
if (parent.getItem(existingJobName) != null) {
throw new IllegalArgumentException("Job with name " + name + " already exists");
}
TopLevelItem item;
try {
item = type.newInstance(parent, name);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
// If root dir already exists on disk, save will just save to it,
// potentially re-using partially deleted project.
File rootDir = item.getRootDir();
if (rootDir.exists()) {
try {
Util.deleteRecursive(rootDir);
logger.info("Previously existing project dir deleted "+rootDir.getAbsolutePath());
} catch (IOException e) {
logger.warn("New project re-using previously deleted project's dir "+rootDir.getAbsolutePath()+" because "+e.getMessage());
}
}
item.onCreatedFromScratch();
item.save();
add(item);
if (notify) {
ItemListener.fireOnCreated(item);
}
return item;
}
}