All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.model.AbstractItem Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *
 * Copyright (c) 2004-2012 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, Winston Prakash,  Daniel Dyer, Tom Huybrechts
 *
 *******************************************************************************/ 

package hudson.model;

import hudson.BulkChange;
import hudson.Functions;
import hudson.Util;
import hudson.XmlFile;
import hudson.cli.declarative.CLIMethod;
import hudson.cli.declarative.CLIResolver;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.util.AtomicFileWriter;
import hudson.util.IOException2;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.Random;
import javax.servlet.ServletException;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder;
import org.eclipse.hudson.security.team.TeamManager;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.stapler.HttpDeletable;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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 {

    static final Logger LOGGER = LoggerFactory.getLogger(AbstractItem.class);
    /**
     * Project name.
     */
    protected /*final*/ transient String name;
    /**
     * Project description. Can be HTML.
     */
    protected volatile String description;
    private transient ItemGroup parent;
    
    protected AbstractItem(ItemGroup parent, String name) {
        this.parent = parent;
        if ((parent instanceof Hudson) && (Hudson.getInstance() != null) && (Hudson.getInstance().isTeamManagementEnabled())) {
            // A job created by itemGroupMixin with an explicit team already
            // has a qualified name and has been added to the team
            TeamManager teamManager = Hudson.getInstance().getTeamManager();
            if (teamManager.findJobOwnerTeam(name) == null)
                name = teamManager.getTeamQualifiedJobName(name);
        }
        doSetName(name);
    }

    public void onCreatedFromScratch() {
        // noop
    }
    
    @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 Messages.AbstractItem_Pronoun();
    }

    @Exported
    public String getDisplayName() {
        return getName();
    }

    public File getRootDir() {
        return (parent != null ? parent.getRootDirFor(this) : Hudson.getInstance().getRootDir());
    }

    public ItemGroup getParent() {
        assert parent != null;
        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();
    }

    /**
     * Just update {@link #name} without performing the rename operation, which
     * would involve copying files and etc.
     */
    protected void doSetName(String name) {
        this.name = name;
    }

    /**
     * Ad additional action which should be performed before the item will be
     * renamed. It's possible to add some logic in the subclasses.
     *
     * @param oldName old item name.
     * @param newName new item name.
     * @throws java.io.IOException if item couldn't be saved.
     */
    protected void performBeforeItemRenaming(String oldName, String newName) throws IOException {
    }

    /**
     * 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(String newName) throws IOException {
        // always synchronize from bigger objects first
        final ItemGroup parent = getParent();
        synchronized (parent) {
            synchronized (this) {
                // sanity check
                if (newName == null) {
                    throw new IllegalArgumentException("New name is not given");
                }

                // noop?
                if (this.name.equals(newName)) {
                    return;
                }

                Item existing = parent.getItem(newName);
                if (existing != null && existing != this) // the look up is case insensitive, so we need "existing!=this"
                // to allow people to rename "Foo" to "foo", for example.
                // see http://www.nabble.com/error-on-renaming-project-tt18061629.html
                {
                    throw new IllegalArgumentException("Job " + newName
                            + " already exists");
                }

                String oldName = this.name;

                performBeforeItemRenaming(oldName, newName);

                File oldRoot = this.getRootDir();

                doSetName(newName);
                File newRoot = this.getRootDir();

                boolean success = false;

                try {// rename data files
                    try {
                        Util.moveDirectory(oldRoot, newRoot);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    success = true;
                } finally {
                    // if failed, back out the rename.
                    if (!success) {
                        doSetName(oldName);
                    }
                }

                callOnRenamed(newName, parent, oldName);

                for (ItemListener l : ItemListener.all()) {
                    try {
                        l.onRenamed(this, oldName, newName);
                    } catch (Exception e) {
                        LOGGER.warn("Exception in ItemListener.onRename", e);
                    }
                }
                save();
            }
        }
    }

    /**
     * A pointless function to work around what appears to be a HotSpot problem.
     * See HUDSON-5756 and bug 6933067 on BugParade for more details.
     */
    private void callOnRenamed(String newName, ItemGroup parent, String oldName) throws IOException {
        try {
            parent.onRenamed(this, oldName, newName);
        } catch (AbstractMethodError _) {
            // ignore
        }
    }

    /**
     * Gets all the jobs that this {@link Item} contains as descendants.
     */
    public abstract Collection getAllJobs();

    public final String getFullName() {
        String n = getParent().getFullName();
        if (n.length() == 0) {
            return getName();
        } else {
            return n + '/' + getName();
        }
    }

    public final String getFullDisplayName() {
        String n = getParent().getFullDisplayName();
        if (n.length() == 0) {
            return getDisplayName();
        } else {
            return n + " \u00BB " + getDisplayName();
        }
    }

    /**
     * Called right after when a {@link Item} is loaded from disk. This is an
     * opporunity to do a post load processing.
     */
    public void onLoad(ItemGroup parent, String name) throws IOException {
        this.parent = parent;
        doSetName(name);
        deleteLock = new Object();
    }

    /**
     * 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.
     */
    public void onCopiedFrom(Item src) {
    }

    public final String getUrl() {
        // try to stick to the current view if possible
        StaplerRequest req = Stapler.getCurrentRequest();
        if (req != null) {
            String seed = Functions.getNearestAncestorUrl(req, this);
            if (seed != null) {
                // trim off the context path portion and leading '/', but add trailing '/'
                return seed.substring(Functions.getRequestRootPath(req).length() + 1) + '/';
            }
        }

        // otherwise compute the path normally
        return getParent().getUrl() + getShortUrl();
    }

    public String getShortUrl() {
        return getParent().getUrlChildPrefix() + '/' + Util.rawEncode(getName()) + '/';
    }

    public String getSearchUrl() {
        return getShortUrl();
    }

    @Exported(visibility = 999, name = "url")
    public final String getAbsoluteUrl() {
        StaplerRequest request = Stapler.getCurrentRequest();
        if (request == null) {
            throw new IllegalStateException("Not processing a HTTP request");
        }
        return Util.encode(Hudson.getInstance().getRootUrl() + getUrl());
    }

    /**
     * Remote API access.
     */
    public final Api getApi() {
        return new Api(this);
    }

    /**
     * Returns the {@link ACL} for this object.
     */
    public ACL getACL() {
        return HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getAuthorizationStrategy().getACL(this);
    }

    /**
     * Short for {@code getACL().checkPermission(p)}
     */
    public void checkPermission(Permission p) {
        getACL().checkPermission(p);
    }

    /**
     * Short for {@code getACL().hasPermission(p)}
     */
    public boolean hasPermission(Permission p) {
        return getACL().hasPermission(p);
    }

    /**
     * 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);
    }

    public Descriptor getDescriptorByName(String className) {
        return Hudson.getInstance().getDescriptorByName(className);
    }

    /**
     * Accepts the new description.
     */
    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.
     */
    @CLIMethod(name = "delete-job")
    public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
        requirePOST();
        delete();
        if (rsp != null) // null for CLI
        {
            rsp.sendRedirect2(req.getContextPath() + "/" + getParent().getUrl());
        }
    }

    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);
        }
    }
    
    private transient Object deleteLock = new Object();
    
    /**
     * Get delete lock. Used as follows:
     *
     * 
     *   synchronized(project.getDeleteLock()) {
     *     if (!project.isDeleted()) {
     *       // do something with project root folder
     *     }
     *   }
     * 
* @return delete lock that can be used to prevent deletion while performing * operations requiring access to item (job) folder. Should be obtained * before any other lock/synchronized to avoid deadlock. * @since 3.3.0 * @see #isDeleted */ public Object getDeleteLock() { return deleteLock; } /** * Deletes this item. */ public void delete() throws IOException, InterruptedException { final ItemGroup group = getParent(); // Obtain delete lock synchronized (getDeleteLock()) { // Lock parent, and then 'this' before deleting. synchronized (group) { synchronized (this) { checkPermission(DELETE); performDelete(); try { invokeOnDeleted(); } catch (AbstractMethodError e) { // ignore } Hudson.getInstance().rebuildDependencyGraph(); } } } } /** * A pointless function to work around what appears to be a HotSpot problem. * See HUDSON-5756 and bug 6933067 on BugParade for more details. */ private void invokeOnDeleted() throws IOException { getParent().onDeleted(this); } /** * Attempt to move root directory out of the way of new job creation * but within the same parent dir. Leaves a better diagnostic trace * and stays within current file system. * Must not throw. * @return File; moved root dir if successful, otherwise original root dir */ private void trySidelineJobDir(File rootDir) { File newDir = null; for (int retry = 0; retry < 5; retry++) { Random r = new Random(); int n = r.nextInt(); StringBuilder sb = new StringBuilder("tmp#"); // Can't be a job name sb.append(n); sb.append('_'); sb.append(rootDir.getName()); if (sb.length() > 255) { sb.setLength(255); } File dir = new File(rootDir.getParentFile(), sb.toString()); if (!dir.exists()) { newDir = dir; break; } } if (newDir != null) { try { Files.move(rootDir.toPath(), newDir.toPath(), StandardCopyOption.ATOMIC_MOVE ,StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { LOGGER.warn("Move job folder unsuccessful "+rootDir.getAbsolutePath(), e); return; } LOGGER.info("Job folder successfully moved from "+rootDir.getAbsolutePath()+" to "+newDir.getAbsolutePath()); } else { LOGGER.warn("Move job folder unsuccessful "+rootDir.getAbsolutePath()); } } private transient volatile boolean deleted; public boolean isDeleted() { return deleted; } /** * Does the real job of deleting the item. */ protected void performDelete() throws IOException, InterruptedException { if (!getConfigFile().doDelete()) { throw new IOException(getRootDir().getAbsolutePath()+"/config.xml can't be deleted"); } // delete must succeed beyond this point final File rootDir = getRootDir(); deleted = true; new Thread(new Runnable() { @Override public void run() { try { Util.deleteRecursive(rootDir); LOGGER.info("Job is deleted at "+rootDir.getAbsolutePath()); } catch (Exception e) { LOGGER.warn("Delete job folder failed "+rootDir.getAbsolutePath()+" because "+e.getMessage()); // Bug 432569 - If folder can't be deleted, leaves job in half-deleted state trySidelineJobDir(rootDir); } } }, "Deleting "+getName()).start(); } /** * Accepts 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 checkPermission(EXTENDED_READ); rsp.setContentType("application/xml"); getConfigFile().writeRawTo(rsp.getOutputStream()); return; } if (req.getMethod().equals("POST")) { // submission checkPermission(CONFIGURE); XmlFile configXmlFile = getConfigFile(); AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile()); try { try { // this allows us to use UTF-8 for storing data, // plus it checks any well-formedness issue in the submitted // data Transformer t = TransformerFactory.newInstance() .newTransformer(); t.transform(new StreamSource(req.getReader()), new StreamResult(out)); out.close(); } catch (TransformerException e) { throw new IOException2("Failed to persist configuration.xml", e); } // try to reflect the changes by reloading new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); onLoad(getParent(), getRootDir().getName()); // if everything went well, commit this new version out.commit(); } finally { out.abort(); // don't leave anything behind } return; } // huh? rsp.sendError(SC_BAD_REQUEST); } public String toString() { return getClass().getSimpleName() + '[' + getFullName() + ']'; } /** * Used for CLI binding. */ @CLIResolver public static AbstractItem resolveForCLI( @Argument(required = true, metaVar = "NAME", usage = "Job name") String name) throws CmdLineException { AbstractItem item = Hudson.getInstance().getItemByFullName(name, AbstractItem.class); if (item == null) { if (AbstractProject.findNearest(name) != null) { throw new CmdLineException(null, Messages.AbstractItem_NoSuchJobExists2(name, AbstractProject.findNearest(name).getFullName())); } else { throw new CmdLineException(null, Messages.AbstractItem_NoSuchJobExists(name)); } } return item; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy