/*
* The MIT License
*
* Copyright 2013 CloudBees.
*
* 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.hudson.plugins.folder;
import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemGroupMixIn;
import hudson.model.Items;
import hudson.model.ListView;
import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.model.View;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.DescribableList;
import hudson.views.ListViewColumn;
import hudson.views.ViewJobFilter;
import org.jenkins.ui.icon.Icon;
import org.jenkins.ui.icon.IconSet;
import org.jenkins.ui.icon.IconSpec;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;
/**
* A mutable folder.
*/
public class Folder extends AbstractFolder<TopLevelItem> implements DirectlyModifiableTopLevelItemGroup {
/**
* @see #getNewPronoun
* @since 4.0
*/
public static final AlternativeUiTextProvider.Message<Folder> NEW_PRONOUN = new AlternativeUiTextProvider.Message<Folder>();
/**
* @deprecated as of 1.7
* Folder is no longer a view by itself.
*/
private transient DescribableList<ListViewColumn, Descriptor<ListViewColumn>> columns;
/**
* @deprecated as of 1.7
* Folder is no longer a view by itself.
*/
private transient DescribableList<ViewJobFilter, Descriptor<ViewJobFilter>> filters;
private transient /*final*/ ItemGroupMixIn mixin;
/**
* {@link Action}s contributed from subsidiary objects associated with
* {@link Folder}, such as from properties.
* <p>
* We don't want to persist them separately, and these actions
* come and go as configuration change, so it's kept separate.
*/
@CopyOnWrite
protected transient volatile List<Action> transientActions = new Vector<Action>();
public Folder(ItemGroup parent, String name) {
super(parent, name);
init();
}
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent, name);
updateTransientActions();
}
@Override
protected final void init() {
super.init();
mixin = new MixInImpl(this);
}
@Override
protected void initViews(List<View> views) throws IOException {
if (columns != null || filters != null) {
// we're loading an ancient config
if (columns == null) {
columns = new DescribableList<ListViewColumn, Descriptor<ListViewColumn>>(this,
ListViewColumn.createDefaultInitialColumnList());
}
if (filters == null) {
filters = new DescribableList<ViewJobFilter, Descriptor<ViewJobFilter>>(this);
}
ListView lv = new ListView("All", this);
views.add(lv);
lv.getColumns().replaceBy(columns.toList());
lv.getJobFilters().replaceBy(filters.toList());
lv.setIncludeRegex(".*");
lv.save();
} else {
super.initViews(views);
}
}
@Override
public void onCreatedFromScratch() {
updateTransientActions();
}
/**
* {@inheritDoc}
* <p>
* Note that this method returns a read-only view of {@link Action}s.
*
* @see TransientFolderActionFactory
* @see FolderProperty#getFolderActions
*/
@SuppressWarnings("deprecation")
@Override
public synchronized List<Action> getActions() {
// add all the transient actions, too
List<Action> actions = new Vector<Action>(super.getActions());
actions.addAll(transientActions);
// return the read only list to cause a failure on plugins who try to add an action here
return Collections.unmodifiableList(actions);
}
/**
* effectively deprecated. Since using updateTransientActions correctly
* under concurrent environment requires a lock that can too easily cause deadlocks.
*/
protected void updateTransientActions() {
transientActions = createTransientActions();
}
@SuppressWarnings("deprecation")
protected List<Action> createTransientActions() {
Vector<Action> ta = new Vector<Action>();
for (TransientFolderActionFactory tpaf : TransientFolderActionFactory.all()) {
ta.addAll(Util.fixNull(tpaf.createFor(this))); // be defensive against null
}
for (FolderProperty<?> p: getProperties().getAll(FolderProperty.class)) {
ta.addAll(Util.fixNull(p.getFolderActions()));
}
return ta;
}
/**
* Used in "New Job" side menu.
* @see #NEW_PRONOUN
*/
public String getNewPronoun() {
return AlternativeUiTextProvider.get(NEW_PRONOUN, this, Messages.Folder_DefaultPronoun());
}
/**
* @deprecated as of 1.7
* Folder is no longer a view by itself.
*/
public DescribableList<ListViewColumn, Descriptor<ListViewColumn>> getColumns() {
return new DescribableList<ListViewColumn, Descriptor<ListViewColumn>>(this,
ListViewColumn.createDefaultInitialColumnList());
}
/** @deprecated use {@link #addProperty(AbstractFolderProperty)} instead */
@Deprecated
public void addProperty(FolderProperty<?> p) throws IOException {
addProperty((AbstractFolderProperty) p);
}
/**
* If copied, copy folder contents.
*/
@Override
public void onCopiedFrom(Item _src) {
Folder src = (Folder) _src;
for (TopLevelItem item : src.getItems()) {
try {
copy(item, item.getName());
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to copy " + src + " into " + this, e);
}
}
}
public TopLevelItem doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
TopLevelItem nue = mixin.createTopLevelItem(req, rsp);
if (!isAllowedChild(nue)) {
// TODO would be better to intercept it before creation, if mode is set
try {
nue.delete();
} catch (InterruptedException x) {
throw (IOException) new IOException(x.toString()).initCause(x);
}
throw new IOException("forbidden child type");
}
return nue;
}
/**
* Copies an existing {@link TopLevelItem} to into this folder with a new name.
*/
public <T extends TopLevelItem> T copy(T src, String name) throws IOException {
if (!isAllowedChild(src)) {
throw new IOException("forbidden child type");
}
return mixin.copy(src, name);
}
public TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException {
TopLevelItem nue = mixin.createProjectFromXML(name, xml);
if (!isAllowedChild(nue)) {
try {
nue.delete();
} catch (InterruptedException x) {
throw (IOException) new IOException(x.toString()).initCause(x);
}
throw new IOException("forbidden child type");
}
return nue;
}
public <T extends TopLevelItem> T createProject(Class<T> type, String name) throws IOException {
return type.cast(createProject((TopLevelItemDescriptor) Jenkins.getActiveInstance().getDescriptor(type), name));
}
public TopLevelItem createProject(TopLevelItemDescriptor type, String name) throws IOException {
return createProject(type, name, true);
}
public TopLevelItem createProject(TopLevelItemDescriptor type, String name, boolean notify) throws IOException {
if (!isAllowedChildDescriptor(type)) {
throw new IOException("forbidden child type");
}
return mixin.createProject(type, name, notify);
}
@Override
protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
updateTransientActions();
}
/**
* Items that can be created in this {@link Folder}.
* @see FolderAddFilter
*/
public List<TopLevelItemDescriptor> getItemDescriptors() {
List<TopLevelItemDescriptor> r = new ArrayList<TopLevelItemDescriptor>();
for (TopLevelItemDescriptor tid : Items.all()) {
if (isAllowedChildDescriptor(tid)) {
r.add(tid);
}
}
return r;
}
/**
* Returns true if the specified descriptor type is allowed for this container.
*/
public boolean isAllowedChildDescriptor(TopLevelItemDescriptor tid) {
for (FolderProperty<?> p : getProperties().getAll(FolderProperty.class)) {
if (!p.allowsParentToCreate(tid)) {
return false;
}
}
if (!getACL().hasCreatePermission(Jenkins.getAuthentication(), this, tid)) {
return false;
}
return tid.isApplicableIn(this);
}
/** Historical synonym for {@link #canAdd}. */
public boolean isAllowedChild(TopLevelItem tid) {
for (FolderProperty<?> p : getProperties().getAll(FolderProperty.class)) {
if (!p.allowsParentToHave(tid)) {
return false;
}
}
return true;
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
@Override public boolean canAdd(TopLevelItem item) {
return isAllowedChild(item);
}
@Override public <I extends TopLevelItem> I add(I item, String name) throws IOException, IllegalArgumentException {
if (!canAdd(item)) {
throw new IllegalArgumentException();
}
if (items.containsKey(name)) {
throw new IllegalArgumentException("already an item '" + name + "'");
}
itemsPut(item.getName(), item);
return item;
}
@Override public void remove(TopLevelItem item) throws IOException, IllegalArgumentException {
items.remove(item.getName());
}
@Extension
public static class DescriptorImpl extends AbstractFolderDescriptor {
/**
* Needed if it wants Folders are categorized in Jenkins 2.x.
*
* TODO: Override when the baseline is upgraded to 2.x
*
* @return A string with the Item description.
*/
public String getDescription() {
return Messages.Folder_Description();
}
@Override
public TopLevelItem newInstance(ItemGroup parent, String name) {
return new Folder(parent, name);
}
static {
IconSet.icons.addIcon(new Icon("icon-item-move-folder icon-sm", "plugin/cloudbees-folder/images/16x16/move.png", Icon.ICON_SMALL_STYLE));
IconSet.icons.addIcon(new Icon("icon-item-move-folder icon-md", "plugin/cloudbees-folder/images/24x24/move.png", Icon.ICON_MEDIUM_STYLE));
IconSet.icons.addIcon(new Icon("icon-item-move-folder icon-lg", "plugin/cloudbees-folder/images/32x32/move.png", Icon.ICON_LARGE_STYLE));
IconSet.icons.addIcon(new Icon("icon-item-move-folder icon-xlg", "plugin/cloudbees-folder/images/48x48/move.png", Icon.ICON_XLARGE_STYLE));
// fix the IconSet defaults because some of them are .gif files and icon-folder should really be here and not in core
IconSet.icons.addIcon(new Icon("icon-folder icon-sm", "plugin/cloudbees-folder/images/16x16/folder.png", Icon.ICON_SMALL_STYLE));
IconSet.icons.addIcon(new Icon("icon-folder icon-md", "plugin/cloudbees-folder/images/24x24/folder.png", Icon.ICON_MEDIUM_STYLE));
IconSet.icons.addIcon(new Icon("icon-folder icon-lg", "plugin/cloudbees-folder/images/32x32/folder.png", Icon.ICON_LARGE_STYLE));
IconSet.icons.addIcon(new Icon("icon-folder icon-xlg", "plugin/cloudbees-folder/images/48x48/folder.png", Icon.ICON_XLARGE_STYLE));
}
}
private class MixInImpl extends ItemGroupMixIn {
private MixInImpl(Folder parent) {
super(parent, parent);
}
@Override
protected void add(TopLevelItem item) {
itemsPut(item.getName(), item);
}
@Override
protected File getRootDirFor(String name) {
return Folder.this.getRootDirFor(name);
}
}
private static final Logger LOGGER = Logger.getLogger(Folder.class.getName());
}