Java tutorial
/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Daniel Dyer, Tom Huybrechts, Yahoo!, 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.model; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.AbortException; import hudson.XmlFile; import hudson.Util; import hudson.Functions; import hudson.BulkChange; import hudson.cli.declarative.CLIResolver; import hudson.model.Queue.Executable; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; import hudson.model.queue.Tasks; import hudson.model.queue.WorkUnit; import hudson.security.ACLContext; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.ACL; import hudson.util.AlternativeUiTextProvider; import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.AtomicFileWriter; import hudson.util.FormValidation; import hudson.util.IOUtils; import hudson.util.Secret; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import jenkins.model.DirectlyModifiableTopLevelItemGroup; import jenkins.model.Jenkins; import jenkins.model.queue.ItemDeletion; import jenkins.security.NotReallyRoleSensitiveCallable; import jenkins.util.xml.XMLUtils; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.WebMethod; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import org.acegisecurity.AccessDeniedException; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.HttpDeletable; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; import org.xml.sax.SAXException; import javax.servlet.ServletException; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import static hudson.model.queue.Executables.getParentOf; import hudson.model.queue.SubTask; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import org.apache.commons.io.FileUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Ancestor; /** * Partial default implementation of {@link Item}. * * @author Kohsuke Kawaguchi */ // Item doesn't necessarily have to be Actionable, but // Java doesn't let multiple inheritance. @ExportedBean public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner, StaplerProxy { private static final Logger LOGGER = Logger.getLogger(AbstractItem.class.getName()); /** * Project name. */ protected /*final*/ transient String name; /** * Project description. Can be HTML. */ protected volatile String description; private transient ItemGroup parent; protected String displayName; protected AbstractItem(ItemGroup parent, String name) { this.parent = parent; doSetName(name); } @Exported(visibility = 999) public String getName() { return name; } /** * Get the term used in the UI to represent this kind of * {@link Item}. Must start with a capital letter. */ public String getPronoun() { return AlternativeUiTextProvider.get(PRONOUN, this, Messages.AbstractItem_Pronoun()); } /** * Gets the term used in the UI to represent the kind of {@link Queue.Task} associated with this kind of * {@link Item}. Must start with a capital letter. Defaults to "Build". * @since 2.50 */ public String getTaskNoun() { return AlternativeUiTextProvider.get(TASK_NOUN, this, Messages.AbstractItem_TaskNoun()); } @Exported /** * @return The display name of this object, or if it is not set, the name * of the object. */ public String getDisplayName() { if (null != displayName) { return displayName; } // if the displayName is not set, then return the name as we use to do return getName(); } @Exported /** * This is intended to be used by the Job configuration pages where * we want to return null if the display name is not set. * @return The display name of this object or null if the display name is not * set */ public String getDisplayNameOrNull() { return displayName; } /** * This method exists so that the Job configuration pages can use * getDisplayNameOrNull so that nothing is shown in the display name text * box if the display name is not set. * @param displayName * @throws IOException */ public void setDisplayNameOrNull(String displayName) throws IOException { setDisplayName(displayName); } public void setDisplayName(String displayName) throws IOException { this.displayName = Util.fixEmptyAndTrim(displayName); save(); } public File getRootDir() { return getParent().getRootDirFor(this); } /** * This bridge method is to maintain binary compatibility with {@link TopLevelItem#getParent()}. */ @WithBridgeMethods(value = Jenkins.class, castRequired = true) @Override public @Nonnull ItemGroup getParent() { if (parent == null) { throw new IllegalStateException("no parent set on " + getClass().getName() + "[" + name + "]"); } return parent; } /** * Gets the project description HTML. */ @Exported public String getDescription() { return description; } /** * Sets the project description HTML. */ public void setDescription(String description) throws IOException { this.description = description; save(); ItemListener.fireOnUpdated(this); } /** * Just update {@link #name} without performing the rename operation, * which would involve copying files and etc. */ protected void doSetName(String name) { this.name = name; } /** * Controls whether the default rename action is available for this item. * * @return whether {@link #name} can be modified by a user * @see #checkRename * @see #renameTo * @since 2.110 */ public boolean isNameEditable() { return false; } /** * Renames this item */ @RequirePOST @Restricted(NoExternalUse.class) public HttpResponse doConfirmRename(@QueryParameter String newName) throws IOException { newName = newName == null ? null : newName.trim(); FormValidation validationError = doCheckNewName(newName); if (validationError.kind != FormValidation.Kind.OK) { throw new Failure(validationError.getMessage()); } renameTo(newName); // send to the new job page // note we can't use getUrl() because that would pick up old name in the // Ancestor.getUrl() return HttpResponses.redirectTo("../" + newName); } /** * Called by {@link #doConfirmRename} and {@code rename.jelly} to validate renames. * @return {@link FormValidation#ok} if this item can be renamed as specified, otherwise * {@link FormValidation#error} with a message explaining the problem. */ @Restricted(NoExternalUse.class) public @Nonnull FormValidation doCheckNewName(@QueryParameter String newName) { // TODO: Create an Item.RENAME permission to use here, see JENKINS-18649. if (!hasPermission(Item.CONFIGURE)) { if (parent instanceof AccessControlled) { ((AccessControlled) parent).checkPermission(Item.CREATE); } checkPermission(Item.DELETE); } newName = newName == null ? null : newName.trim(); try { Jenkins.checkGoodName(newName); assert newName != null; // Would have thrown Failure if (newName.equals(name)) { return FormValidation.warning(Messages.AbstractItem_NewNameUnchanged()); } Jenkins.get().getProjectNamingStrategy().checkName(newName); checkIfNameIsUsed(newName); checkRename(newName); } catch (Failure e) { return FormValidation.error(e.getMessage()); } return FormValidation.ok(); } /** * Check new name for job * @param newName - New name for job. */ private void checkIfNameIsUsed(@Nonnull String newName) throws Failure { try { Item item = getParent().getItem(newName); if (item != null) { throw new Failure(Messages.AbstractItem_NewNameInUse(newName)); } try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { item = getParent().getItem(newName); if (item != null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Unable to rename the job {0}: name {1} is already in use. " + "User {2} has no {3} permission for existing job with the same name", new Object[] { this.getFullName(), newName, ctx.getPreviousContext().getAuthentication().getName(), Item.DISCOVER.name }); } // Don't explicitly mention that there is another item with the same name. throw new Failure(Messages.Jenkins_NotAllowedName(newName)); } } } catch (AccessDeniedException ex) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Unable to rename the job {0}: name {1} is already in use. " + "User {2} has {3} permission, but no {4} for existing job with the same name", new Object[] { this.getFullName(), newName, User.current(), Item.DISCOVER.name, Item.READ.name }); } throw new Failure(Messages.AbstractItem_NewNameInUse(newName)); } } /** * Allows subclasses to block renames for domain-specific reasons. Generic validation of the new name * (e.g., null checking, checking for illegal characters, and checking that the name is not in use) * always happens prior to calling this method. * * @param newName the new name for the item * @throws Failure if the rename should be blocked * @since 2.110 * @see Job#checkRename */ protected void checkRename(@Nonnull String newName) throws Failure { } /** * Renames this item. * Not all the Items need to support this operation, but if you decide to do so, * you can use this method. */ protected void renameTo(final String newName) throws IOException { // always synchronize from bigger objects first final ItemGroup parent = getParent(); String oldName = this.name; String oldFullName = getFullName(); synchronized (parent) { synchronized (this) { // sanity check if (newName == null) throw new IllegalArgumentException("New name is not given"); // noop? if (this.name.equals(newName)) return; // the lookup is case insensitive, so we should not fail if this item was the existing? one // to allow people to rename "Foo" to "foo", for example. // see http://www.nabble.com/error-on-renaming-project-tt18061629.html Items.verifyItemDoesNotAlreadyExist(parent, newName, this); File oldRoot = this.getRootDir(); doSetName(newName); File newRoot = this.getRootDir(); boolean success = false; try {// rename data files boolean interrupted = false; boolean renamed = false; // try to rename the job directory. // this may fail on Windows due to some other processes // accessing a file. // so retry few times before we fall back to copy. for (int retry = 0; retry < 5; retry++) { if (oldRoot.renameTo(newRoot)) { renamed = true; break; // succeeded } try { Thread.sleep(500); } catch (InterruptedException e) { // process the interruption later interrupted = true; } } if (interrupted) Thread.currentThread().interrupt(); if (!renamed) { // failed to rename. it must be that some lengthy // process is going on // to prevent a rename operation. So do a copy. Ideally // we'd like to // later delete the old copy, but we can't reliably do // so, as before the VM // shuts down there might be a new job created under the // old name. Copy cp = new Copy(); cp.setProject(new org.apache.tools.ant.Project()); cp.setTodir(newRoot); FileSet src = new FileSet(); src.setDir(oldRoot); cp.addFileset(src); cp.setOverwrite(true); cp.setPreserveLastModified(true); cp.setFailOnError(false); // keep going even if // there's an error cp.execute(); // try to delete as much as possible try { Util.deleteRecursive(oldRoot); } catch (IOException e) { // but ignore the error, since we expect that e.printStackTrace(); } } success = true; } finally { // if failed, back out the rename. if (!success) doSetName(oldName); } parent.onRenamed(this, oldName, newName); } } ItemListener.fireLocationChange(this, oldFullName); } /** * Notify this item it's been moved to another location, replaced by newItem (might be the same object, but not guaranteed). * This method is executed <em>after</em> the item root directory has been moved to it's new location. * <p> * Derived classes can override this method to add some specific behavior on move, but have to call parent method * so the item is actually setup within it's new parent. * * @see hudson.model.Items#move(AbstractItem, jenkins.model.DirectlyModifiableTopLevelItemGroup) */ public void movedTo(DirectlyModifiableTopLevelItemGroup destination, AbstractItem newItem, File destDir) throws IOException { newItem.onLoad(destination, name); } /** * Gets all the jobs that this {@link Item} contains as descendants. */ public abstract Collection<? extends Job> getAllJobs(); @Exported public final String getFullName() { String n = getParent().getFullName(); if (n.length() == 0) return getName(); else return n + '/' + getName(); } @Exported public final String getFullDisplayName() { String n = getParent().getFullDisplayName(); if (n.length() == 0) return getDisplayName(); else return n + " " + getDisplayName(); } /** * Gets the display name of the current item relative to the given group. * * @since 1.515 * @param p the ItemGroup used as point of reference for the item * @return * String like "foo bar" */ public String getRelativeDisplayNameFrom(ItemGroup p) { return Functions.getRelativeDisplayNameFrom(this, p); } /** * This method only exists to disambiguate {@link #getRelativeNameFrom(ItemGroup)} and {@link #getRelativeNameFrom(Item)} * @since 1.512 * @see #getRelativeNameFrom(ItemGroup) */ public String getRelativeNameFromGroup(ItemGroup p) { return getRelativeNameFrom(p); } /** * Called right after when a {@link Item} is loaded from disk. * This is an opportunity to do a post load processing. */ public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { this.parent = parent; doSetName(name); } /** * When a {@link Item} is copied from existing one, * the files are first copied on the file system, * then it will be loaded, then this method will be invoked * to perform any implementation-specific work. * * <p> * * * @param src * Item from which it's copied from. The same type as {@code this}. Never null. */ public void onCopiedFrom(Item src) { } public final String getUrl() { // try to stick to the current view if possible StaplerRequest req = Stapler.getCurrentRequest(); String shortUrl = getShortUrl(); String uri = req == null ? null : req.getRequestURI(); if (req != null) { String seed = Functions.getNearestAncestorUrl(req, this); LOGGER.log(Level.FINER, "seed={0} for {1} from {2}", new Object[] { seed, this, uri }); if (seed != null) { // trim off the context path portion and leading '/', but add trailing '/' return seed.substring(req.getContextPath().length() + 1) + '/'; } List<Ancestor> ancestors = req.getAncestors(); if (!ancestors.isEmpty()) { Ancestor last = ancestors.get(ancestors.size() - 1); if (last.getObject() instanceof View) { View view = (View) last.getObject(); if (view.getOwner().getItemGroup() == getParent() && !view.isDefault()) { // Showing something inside a view, so should use that as the base URL. String base = last.getUrl().substring(req.getContextPath().length() + 1) + '/'; LOGGER.log(Level.FINER, "using {0}{1} for {2} from {3}", new Object[] { base, shortUrl, this, uri }); return base + shortUrl; } else { LOGGER.log(Level.FINER, "irrelevant {0} for {1} from {2}", new Object[] { view.getViewName(), this, uri }); } } else { LOGGER.log(Level.FINER, "inapplicable {0} for {1} from {2}", new Object[] { last.getObject(), this, uri }); } } else { LOGGER.log(Level.FINER, "no ancestors for {0} from {1}", new Object[] { this, uri }); } } else { LOGGER.log(Level.FINER, "no current request for {0}", this); } // otherwise compute the path normally String base = getParent().getUrl(); LOGGER.log(Level.FINER, "falling back to {0}{1} for {2} from {3}", new Object[] { base, shortUrl, this, uri }); return base + shortUrl; } public String getShortUrl() { String prefix = getParent().getUrlChildPrefix(); String subdir = Util.rawEncode(getName()); return prefix.equals(".") ? subdir + '/' : prefix + '/' + subdir + '/'; } public String getSearchUrl() { return getShortUrl(); } @Override @Exported(visibility = 999, name = "url") public final String getAbsoluteUrl() { return Item.super.getAbsoluteUrl(); } /** * Remote API access. */ public final Api getApi() { return new Api(this); } /** * Returns the {@link ACL} for this object. */ public ACL getACL() { return Jenkins.getInstance().getAuthorizationStrategy().getACL(this); } /** * Save the settings to a file. */ public synchronized void save() throws IOException { if (BulkChange.contains(this)) return; getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } public final XmlFile getConfigFile() { return Items.getConfigFile(this); } private Object writeReplace() { return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this)); } private static class Replacer { private final String fullName; Replacer(AbstractItem i) { fullName = i.getFullName(); } private Object readResolve() { Jenkins j = Jenkins.getInstanceOrNull(); if (j == null) { return null; } // Will generally only work if called after job loading: return j.getItemByFullName(fullName); } } /** * Accepts the new description. */ @RequirePOST public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(CONFIGURE); setDescription(req.getParameter("description")); rsp.sendRedirect("."); // go to the top page } /** * Deletes this item. * Note on the funny name: for reasons of historical compatibility, this URL is {@code /doDelete} * since it predates {@code <l:confirmationLink>}. {@code /delete} goes to a Jelly page * which should now be unused by core but is left in case plugins are still using it. */ @RequirePOST public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { delete(); if (req == null || rsp == null) { // CLI return; } List<Ancestor> ancestors = req.getAncestors(); ListIterator<Ancestor> it = ancestors.listIterator(ancestors.size()); String url = getParent().getUrl(); // fallback but we ought to get to Jenkins.instance at the root while (it.hasPrevious()) { Object a = it.previous().getObject(); if (a instanceof View) { url = ((View) a).getUrl(); break; } else if (a instanceof ViewGroup && a != this) { url = ((ViewGroup) a).getUrl(); break; } } rsp.sendRedirect2(req.getContextPath() + '/' + url); } public void delete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { try { doDoDelete(req, rsp); } catch (InterruptedException e) { // TODO: allow this in Stapler throw new ServletException(e); } } /** * Deletes this item. * * <p> * Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller * from showing the stack trace. This */ public void delete() throws IOException, InterruptedException { checkPermission(DELETE); boolean responsibleForAbortingBuilds = !ItemDeletion.contains(this); boolean ownsRegistration = ItemDeletion.register(this); if (!ownsRegistration && ItemDeletion.isRegistered(this)) { // we are not the owning thread and somebody else is concurrently deleting this exact item throw new Failure(Messages.AbstractItem_BeingDeleted(getPronoun())); } try { // if a build is in progress. Cancel it. if (responsibleForAbortingBuilds || ownsRegistration) { Queue queue = Queue.getInstance(); if (this instanceof Queue.Task) { // clear any items in the queue so they do not get picked up queue.cancel((Queue.Task) this); } // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot for (Queue.Item i : queue.getItems()) { Item item = Tasks.getItemOf(i.task); while (item != null) { if (item == this) { queue.cancel(i); break; } if (item.getParent() instanceof Item) { item = (Item) item.getParent(); } else { break; } } } // interrupt any builds in progress (and this should be a recursive test so that folders do not pay // the 15 second delay for every child item). This happens after queue cancellation, so will be // a complete set of builds in flight Map<Executor, Queue.Executable> buildsInProgress = new LinkedHashMap<>(); for (Computer c : Jenkins.getInstance().getComputers()) { for (Executor e : c.getAllExecutors()) { final WorkUnit workUnit = e.getCurrentWorkUnit(); final Executable executable = workUnit != null ? workUnit.getExecutable() : null; final SubTask subtask = executable != null ? getParentOf(executable) : null; if (subtask != null) { Item item = Tasks.getItemOf(subtask); if (item != null) { while (item != null) { if (item == this) { buildsInProgress.put(e, e.getCurrentExecutable()); e.interrupt(Result.ABORTED); break; } if (item.getParent() instanceof Item) { item = (Item) item.getParent(); } else { break; } } } } } } if (!buildsInProgress.isEmpty()) { // give them 15 seconds or so to respond to the interrupt long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); // comparison with executor.getCurrentExecutable() == computation currently should always be true // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) { // we know that ItemDeletion will prevent any new builds in the queue // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear // Queue.cancel happens-before collecting the buildsInProgress list // thus buildsInProgress contains the complete set we need to interrupt and wait for for (Iterator<Map.Entry<Executor, Queue.Executable>> iterator = buildsInProgress.entrySet() .iterator(); iterator.hasNext();) { Map.Entry<Executor, Queue.Executable> entry = iterator.next(); // comparison with executor.getCurrentExecutable() == executable currently should always be // true as we no longer recycle Executors, but safer to future-proof in case we ever // revisit recycling. if (!entry.getKey().isAlive() || entry.getValue() != entry.getKey().getCurrentExecutable()) { iterator.remove(); } // I don't know why, but we have to keep interrupting entry.getKey().interrupt(Result.ABORTED); } Thread.sleep(50L); } if (!buildsInProgress.isEmpty()) { throw new Failure(Messages.AbstractItem_FailureToStopBuilds(buildsInProgress.size(), getFullDisplayName())); } } } synchronized (this) { // could just make performDelete synchronized but overriders might not honor that performDelete(); } // JENKINS-19446: leave synch block, but JENKINS-22001: still notify synchronously } finally { if (ownsRegistration) { ItemDeletion.deregister(this); } } getParent().onDeleted(AbstractItem.this); Jenkins.getInstance().rebuildDependencyGraphAsync(); } /** * Does the real job of deleting the item. */ protected void performDelete() throws IOException, InterruptedException { getConfigFile().delete(); Util.deleteRecursive(getRootDir()); } /** * Accepts {@code config.xml} submission, as well as serve it. */ @WebMethod(name = "config.xml") public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp) throws IOException { if (req.getMethod().equals("GET")) { // read rsp.setContentType("application/xml"); writeConfigDotXml(rsp.getOutputStream()); return; } if (req.getMethod().equals("POST")) { // submission updateByXml((Source) new StreamSource(req.getReader())); return; } // huh? rsp.sendError(SC_BAD_REQUEST); } static final Pattern SECRET_PATTERN = Pattern.compile(">(" + Secret.ENCRYPTED_VALUE_PATTERN + ")<"); /** * Writes {@code config.xml} to the specified output stream. * The user must have at least {@link #EXTENDED_READ}. * If he lacks {@link #CONFIGURE}, then any {@link Secret}s detected will be masked out. */ @Restricted(NoExternalUse.class) public void writeConfigDotXml(OutputStream os) throws IOException { checkPermission(EXTENDED_READ); XmlFile configFile = getConfigFile(); if (hasPermission(CONFIGURE)) { IOUtils.copy(configFile.getFile(), os); } else { String encoding = configFile.sniffEncoding(); String xml = FileUtils.readFileToString(configFile.getFile(), encoding); Matcher matcher = SECRET_PATTERN.matcher(xml); StringBuffer cleanXml = new StringBuffer(); while (matcher.find()) { if (Secret.decrypt(matcher.group(1)) != null) { matcher.appendReplacement(cleanXml, ">********<"); } } matcher.appendTail(cleanXml); org.apache.commons.io.IOUtils.write(cleanXml.toString(), os, encoding); } } /** * @deprecated as of 1.473 * Use {@link #updateByXml(Source)} */ @Deprecated public void updateByXml(StreamSource source) throws IOException { updateByXml((Source) source); } /** * Updates an Item by its XML definition. * @param source source of the Item's new definition. * The source should be either a <code>StreamSource</code> or a <code>SAXSource</code>, other * sources may not be handled. * @since 1.473 */ public void updateByXml(Source source) throws IOException { checkPermission(CONFIGURE); XmlFile configXmlFile = getConfigFile(); final AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile()); try { try { XMLUtils.safeTransform(source, new StreamResult(out)); out.close(); } catch (TransformerException | SAXException e) { throw new IOException("Failed to persist config.xml", e); } // try to reflect the changes by reloading Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshalNullingOut(this); if (o != this) { // ensure that we've got the same job type. extending this code to support updating // to different job type requires destroying & creating a new job type throw new IOException("Expecting " + this.getClass() + " but got " + o.getClass() + " instead"); } Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable<Void, IOException>() { @Override public Void call() throws IOException { onLoad(getParent(), getRootDir().getName()); return null; } }); Jenkins.getInstance().rebuildDependencyGraphAsync(); // if everything went well, commit this new version out.commit(); SaveableListener.fireOnChange(this, getConfigFile()); } finally { out.abort(); // don't leave anything behind } } /** * Reloads this job from the disk. * * Exposed through CLI as well. * * TODO: think about exposing this to UI * * @since 1.556 */ @RequirePOST public void doReload() throws IOException { checkPermission(CONFIGURE); // try to reflect the changes by reloading getConfigFile().unmarshal(this); Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable<Void, IOException>() { @Override public Void call() throws IOException { onLoad(getParent(), getRootDir().getName()); return null; } }); Jenkins.getInstance().rebuildDependencyGraphAsync(); SaveableListener.fireOnChange(this, getConfigFile()); } /** * {@inheritDoc} */ @Override public String getSearchName() { // the search name of abstract items should be the name and not display name. // this will make suggestions use the names and not the display name // so that the links will 302 directly to the thing the user was finding return getName(); } @Override public String toString() { return super.toString() + '[' + (parent != null ? getFullName() : "?/" + name) + ']'; } @Override @Restricted(NoExternalUse.class) public Object getTarget() { if (!SKIP_PERMISSION_CHECK) { if (!getACL().hasPermission(Item.DISCOVER)) { return null; } getACL().checkPermission(Item.READ); } return this; } /** * Escape hatch for StaplerProxy-based access control */ @Restricted(NoExternalUse.class) public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean .getBoolean(AbstractItem.class.getName() + ".skipPermissionCheck"); /** * Used for CLI binding. */ @CLIResolver public static AbstractItem resolveForCLI( @Argument(required = true, metaVar = "NAME", usage = "Item name") String name) throws CmdLineException { // TODO can this (and its pseudo-override in AbstractProject) share code with GenericItemOptionHandler, used for explicit CLICommands rather than CLIMethods? AbstractItem item = Jenkins.getInstance().getItemByFullName(name, AbstractItem.class); if (item == null) { AbstractItem project = Items.findNearest(AbstractItem.class, name, Jenkins.getInstance()); throw new CmdLineException(null, project == null ? Messages.AbstractItem_NoSuchJobExistsWithoutSuggestion(name) : Messages.AbstractItem_NoSuchJobExists(name, project.getFullName())); } return item; } /** * Replaceable pronoun of that points to a job. Defaults to "Job"/"Project" depending on the context. */ public static final Message<AbstractItem> PRONOUN = new Message<AbstractItem>(); /** * Replaceable noun for describing the kind of task that this item represents. Defaults to "Build". */ public static final Message<AbstractItem> TASK_NOUN = new Message<>(); }