/*
* Copyright 2013 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jenkins.plugins.delegate;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.interceptor.RequirePOST;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static hudson.model.ItemGroupMixIn.KEYED_BY_NAME;
import static hudson.model.ItemGroupMixIn.loadChildren;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildableItemWithBuildWrappers;
import hudson.model.DependencyGraph;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemGroupMixIn;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.model.ViewGroup;
import hudson.model.ViewGroupMixIn;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Publisher;
import hudson.util.DescribableList;
import hudson.views.ViewsTabBar;
import net.sf.json.JSONObject;
/**
* This hoists the common logic for our job container project types,
* some things shared include:
* <ul>
* <li> {@link ItemGroupMixIn} configuration, and reloading
* <li> {@link ViewGroupMixIn} configuration, reloading and stub routines.
* <li> Boilerplace {@link Publisher} code.
* </ul>
*
* @param <T> The type of element contained by this entity
* @param <P> The ultimate project type of this container
* @param <B> The built type associated with {@code P}
*/
public abstract class AbstractRunnableItemGroup<
T extends TopLevelItem,
P extends AbstractRunnableItemGroup<T, P, B>,
B extends AbstractBuild<P, B>>
extends AbstractBranchAwareProject<P, B>
implements ViewGroup, ItemGroup<T>, BuildableItemWithBuildWrappers {
/**
* Build up our Yaml project shell, which gets populated in
* {@link #submit(StaplerRequest, StaplerResponse)}.
*/
public AbstractRunnableItemGroup(ItemGroup parent, String name)
throws IOException {
super(parent, name);
this.projects = Maps.newHashMap();
init();
}
public AbstractProject<?, ?> asProject() {
return this;
}
/** {@inheritDoc} */
@Override
public List<Action> getViewActions() {
return ImmutableList.<Action>of();
}
/** {@inheritDoc} */
@Override
public ItemGroup<? extends TopLevelItem> getItemGroup() {
return this;
}
/** {@inheritDoc} */
@Override
public ViewsTabBar getViewsTabBar() {
return viewsTabBar;
}
protected volatile ViewsTabBar viewsTabBar;
/** Perform the deletion. */
@RequirePOST
@Override
public synchronized void doDoDelete(StaplerRequest req, StaplerResponse rsp)
throws IOException, ServletException, InterruptedException {
delete();
rsp.sendRedirect2(Joiner.on("/").join(
req.getContextPath(), getParent().getUrl()));
}
/** {@inheritDoc} */
@Override
public void onViewRenamed(View view, String oldName, String newName) {
getViewGroupMixIn().onViewRenamed(view, oldName, newName);
}
/** {@inheritDoc} */
@Override
public void deleteView(View view) throws IOException {
getViewGroupMixIn().deleteView(view);
}
/** {@inheritDoc} */
@Override
public void onDeleted(T item) throws IOException {
projects.remove(item.getName());
save();
}
/** {@inheritDoc} */
@Override
public View getView(String name) {
return getViewGroupMixIn().getView(name);
}
/** {@inheritDoc} */
@Override
public Collection<View> getViews() {
return getViewGroupMixIn().getViews();
}
/** {@inheritDoc} */
@Override
public View getPrimaryView() {
return getViewGroupMixIn().getPrimaryView();
}
protected List<View> views;
protected String primaryViewName;
/** {@inheritDoc} */
@Override
public boolean canDelete(View view) {
return getViewGroupMixIn().canDelete(view);
}
/** {@inheritDoc} */
@Override
public void onRenamed(T item, String oldName, String newName)
throws IOException {
projects.remove(oldName);
projects.put(newName, item);
save();
}
/** {@inheritDoc} */
@Override
public DescribableList<Publisher, Descriptor<Publisher>> getPublishersList() {
// TODO(mattmoor): switch to utilize something with an atomic
// compare/exchange semantic.
if (publishers != null) {
return publishers;
}
// NOTE: I believe this is lazily initialized vs. created in the
// constructor so that lazy API consumers can omit an empty publisher list
// from their serialized XML blob.
synchronized (this) {
if (publishers == null) {
publishers =
new DescribableList<Publisher, Descriptor<Publisher>>(this);
}
}
return publishers;
}
@Nullable
private volatile DescribableList<Publisher, Descriptor<Publisher>> publishers;
/**
* List of active {@link BuildWrapper}s configured for this project.
*/
private volatile DescribableList<BuildWrapper, Descriptor<BuildWrapper>>
buildWrappers;
private static final AtomicReferenceFieldUpdater<AbstractRunnableItemGroup,
DescribableList>
buildWrappersSetter = AtomicReferenceFieldUpdater.newUpdater(
AbstractRunnableItemGroup.class, DescribableList.class,
"buildWrappers");
@Nullable
protected Map<String, T> projects;
/**
* Shared method for retrieving the directory in which we store nested
* sub jobs that we create as part of our DSL processing.
*/
private File getJobsDir() {
return new File(getRootDir(), getUrlChildPrefix());
}
/**
* Shared method for retrieving the directory in which a particular
* sub job's configuration is stored after it was created from its Yaml.
*/
private File getJobDir(String name) {
return new File(getJobsDir(), name);
}
/** {@inheritDoc} */
@Override
public File getRootDirFor(T child) {
return getJobDir(child.getName());
}
/** {@inheritDoc} */
@Override
public T getItem(String name) {
return projects.get(name);
}
/** {@inheritDoc} */
@Override
public Collection<T> getItems() {
return Collections.unmodifiableCollection(projects.values());
}
public Map<Descriptor<BuildWrapper>, BuildWrapper> getBuildWrappers() {
return getBuildWrappersList().toMap();
}
public DescribableList<BuildWrapper, Descriptor<BuildWrapper>>
getBuildWrappersList() {
if (buildWrappers == null) {
buildWrappersSetter.compareAndSet(this, null,
new DescribableList<BuildWrapper, Descriptor<BuildWrapper>>(this));
}
return buildWrappers;
}
/** This surfaces the embedded projects to the Jenkins UI. */
@Exported
public T getJob(String name) {
return getItem(name);
}
/** {@inheritDoc} */
@Override
public String getUrlChildPrefix() {
return "job";
}
/** {@inheritDoc} */
@Override
protected void buildDependencyGraph(DependencyGraph graph) {
getPublishersList().buildDependencyGraph(this, graph);
getBuildWrappersList().buildDependencyGraph(this, graph);
// Anything that implements DependencyDeclarer can contribute to
// the dependency graph, so if our build or build wrappers (or
// whatever else) might implement this, then we should delegate to
// them to populate any dependencies.
}
/** {@inheritDoc} */
@Override
public boolean isFingerprintConfigured() {
return false;
}
/** {@inheritDoc} */
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name)
throws IOException {
super.onLoad(parent, name);
getPublishersList().setOwner(this);
getBuildWrappersList().setOwner(this);
this.projects = loadChildren(this, getJobsDir(), KEYED_BY_NAME);
init();
}
/**
* Common initialization method for constructor and {@link #onLoad}.
*
* NOTE: This should be overriden and the override should end in a call to
* {@code super.init()} to setup the mixins.
*/
protected void init() throws IOException {
checkNotNull(viewsTabBar);
checkNotNull(views);
checkState(views.size() > 0);
checkState(!Strings.isNullOrEmpty(this.primaryViewName));
this.itemGroupMixIn = new ItemGroupMixIn(this, this) {
/** {@inheritDoc} */
@Override
protected void add(TopLevelItem item) {
try {
checkState(item instanceof AbstractProject);
AbstractRunnableItemGroup.this.addItem((T) item);
} catch (IOException e) {
// TODO(mattmoor): log
e.printStackTrace();
}
}
/** {@inheritDoc} */
@Override
protected File getRootDirFor(String name) {
// Use the host implementation
return AbstractRunnableItemGroup.this.getJobDir(name);
}
};
this.viewGroupMixIn = new ViewGroupMixIn(this) {
/** {@inheritDoc} */
@Override
protected List<View> views() {
return AbstractRunnableItemGroup.this.views;
}
/** {@inheritDoc} */
@Override
protected String primaryView() {
return AbstractRunnableItemGroup.this.primaryViewName;
}
/** {@inheritDoc} */
@Override
protected void primaryView(String name) {
AbstractRunnableItemGroup.this.primaryViewName = name;
}
};
}
/** Useful hook for testing, but also for child access */
protected ItemGroupMixIn getItemGroupMixIn() {
return itemGroupMixIn;
}
protected transient ItemGroupMixIn itemGroupMixIn;
/** Useful hook for testing, but also for child access */
protected ViewGroupMixIn getViewGroupMixIn() {
return viewGroupMixIn;
}
protected transient ViewGroupMixIn viewGroupMixIn;
/** Add an item to our {@link ItemGroup}. */
public void addItem(T project) throws IOException {
checkNotNull(project);
checkNotNull(projects);
checkArgument(!projects.containsKey(project.getName()));
projects.put(project.getName(), project);
}
/** {@inheritDoc} */
@Override
protected void submit(StaplerRequest req, StaplerResponse rsp)
throws IOException, ServletException, FormException {
super.submit(req, rsp);
final JSONObject json = req.getSubmittedForm();
getBuildWrappersList().rebuild(req, json, BuildWrappers.getFor(this));
getPublishersList().rebuildHetero(req, json, Publisher.all(), "publisher");
}
}