/*
* 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.dsl.util;
import java.io.IOException;
import java.util.Collections;
import javax.servlet.ServletException;
import org.directwebremoting.util.FakeHttpServletRequest;
import org.directwebremoting.util.FakeHttpServletResponse;
import org.kohsuke.stapler.RequestImpl;
import org.kohsuke.stapler.ResponseImpl;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebApp;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.jenkins.plugins.dsl.restrict.BadTypeException;
import com.google.jenkins.plugins.dsl.restrict.RestrictedTypeException;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.FreeStyleProject;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.TopLevelItemDescriptor;
import hudson.model.View;
import hudson.model.ViewGroupMixIn;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
/**
* This interface abstracts how our various modules bind a {@link JSONObject} to
* a particular type.
*/
public interface Binder {
/**
* Bind the {@link JSONObject} to the specified type.
*
* @param <T> The type to which we are binding the JSON.
* @param json The serialized object we are instantiating
* @return The object of the specified type, from the JSON.
*/
<T extends Describable> T bind(JSONObject json)
throws IOException, FormException;
/**
* Bind the {@link JSONObject} to the specified type of {@link Job}.
*
* @param <T> The type to which we are binding the JSON.
* @param parent The item group within which we are creating the job
* @param name The name to give the bound job.
* @param json The serialized job we are instantiating
* @return The object of the specified type, from the JSON.
*/
<T extends Job> T bindJob(ItemGroup<? super T> parent,
String name, JSONObject json) throws IOException;
/**
* Bind the {@link JSONObject} to the specified type of {@link View}.
*
* @param <T> The type to which we are binding the JSON.
* @param parent The mixin for the view group where we are creating the view
* @param name The name to give the bound view.
* @param json The serialized view we are instantiating
* @return The object of the specified type, from the JSON.
*/
<T extends View> T bindView(ViewGroupMixIn parent,
String name, JSONObject json) throws IOException, FormException;
/**
* The default implementation of {@link Binder}, which leverages
* {@link Stapler} to perform the binding as it would when handing
* the object as form input.
*/
public class Default implements Binder {
public Default(ClassLoader classLoader) {
this.classLoader = checkNotNull(classLoader);
}
/** {@inheritDoc} */
@Override
public <T extends Describable> T bind(JSONObject json)
throws IOException, FormException {
final String clazz = checkNotNull(json.optString("$class", null));
final Descriptor descriptor = getDescriptor(clazz);
final Stapler stapler = getStapler();
final StaplerRequest request = getRequest(stapler, json);
// We do this instead of 'request.bindJson' because this doesn't
// require a DataBoundConstructor.
// TODO(mattmoor): Should we do the rewrite of describable lists
// here as well?
return (T) descriptor.newInstance(request, json);
}
/** {@inheritDoc} */
@Override
public <T extends Job> T bindJob(ItemGroup<? super T> parent,
String name, JSONObject json) throws IOException {
// TODO(mattmoor): What if: FreeStyleProject != T?
final String clazz = json.optString("$class",
FreeStyleProject.class.getName());
final TopLevelItemDescriptor descriptor =
(TopLevelItemDescriptor) getDescriptor(clazz);
final T project = (T) descriptor.newInstance(parent, name);
final Predicate<Descriptor> hasClazz = new Predicate<Descriptor>() {
public boolean apply(Descriptor descriptor) {
try {
return null != getClassLoader().loadClass(
descriptor.clazz.getName());
} catch (RestrictedTypeException e) {
return false;
} catch (ClassNotFoundException e) {
return false;
}
}
};
FilteredDescribableList.rewrite(project, hasClazz);
final Stapler stapler = getStapler();
final StaplerRequest request = getRequest(stapler, json);
final StaplerResponse response = getResponse(stapler);
try {
project.doConfigSubmit(request, response);
} catch (FormException e) {
throw new IllegalStateException(Messages.DefaultBinder_BadJsonBlob(
json.toString()), e);
} catch (ServletException e) {
throw new IllegalStateException(Messages.DefaultBinder_BadJsonBlob(
json.toString()), e);
}
return project;
}
/** {@inheritDoc} */
@Override
public <T extends View> T bindView(ViewGroupMixIn parentMixIn,
String name, JSONObject json) throws IOException, FormException {
T view = bind(json);
// We need to do this to set the owner, so doConfigSubmit works
// properly with things like ListView.
parentMixIn.addView(view);
// TODO(mattmoor): Should we do the rewrite of describable lists
// here as well?
final Stapler stapler = getStapler();
final StaplerRequest request = getRequest(stapler, json);
final StaplerResponse response = getResponse(stapler);
try {
view.doConfigSubmit(request, response);
view.save();
} catch (FormException e) {
throw new IllegalStateException(Messages.DefaultBinder_BadJsonBlob(
json.toString()), e);
} catch (ServletException e) {
throw new IllegalStateException(Messages.DefaultBinder_BadJsonBlob(
json.toString()), e);
}
return view;
}
/**
* Gets the {@link Descriptor} of the {@link Describable} we are
* looking to instantiate.
*/
@VisibleForTesting
Descriptor getDescriptor(String clazzName) throws IOException {
Class<? extends Describable<?>> implClass;
try {
implClass = (Class<? extends Describable<?>>) getClassLoader()
.loadClass(clazzName);
} catch (ClassNotFoundException ex) {
throw new BadTypeException(
Messages.DefaultBinder_CannotLoadClass(clazzName));
}
final Descriptor descriptor =
checkNotNull(Jenkins.getInstance()).getDescriptor(implClass);
if (descriptor == null) {
throw new BadTypeException(
Messages.DefaultBinder_NoDescriptor(implClass.getName()));
}
return descriptor;
}
/** @return the class loader through which to load classes */
public ClassLoader getClassLoader() {
// This class loader is used in three ways:
// 1) To determine the initial descriptor via which we allocate our
// root describable object.
//
// 2) It is embedded in the Stapler we create, so bound objects must
// have a type available from it.
//
// 3) It is used as a Predicate in the FilteredDescribableList's we
// install in every DescribableList field of instantiated objects.
return classLoader;
}
private final ClassLoader classLoader;
/** @return a response to use with {@link Stapler} binding utilities */
@VisibleForTesting
StaplerResponse getResponse(Stapler stapler) {
return new ResponseImpl(stapler,
new FakeHttpServletResponse()) {
@Override
public void sendRedirect(String url) throws IOException {
;
}
};
}
/**
* A request to use with {@link Stapler} that pretends the given
* json block is submitted form data, to allow it to be sent through
* the object creation process.
*/
@VisibleForTesting
StaplerRequest getRequest(Stapler stapler, final JSONObject json) {
return new RequestImpl(stapler,
new FakeHttpServletRequest() {
@Override
public String getRequestURI() {
return "/";
}
}, Collections.EMPTY_LIST, null) {
@Override
public JSONObject getSubmittedForm() {
return json;
}
@Override
public String getParameter(String name) {
Object o = getSubmittedForm().opt(name);
if (o instanceof JSONObject) {
return ((JSONObject) o).optString("value");
} else if (o != null) {
return o.toString();
}
return null;
}
};
}
/** Gets an instance of stapler for use in lazy json binding. */
@VisibleForTesting
Stapler getStapler() {
final WebApp webapp = new WebApp(null /* context */);
webapp.setClassLoader(getClassLoader());
Stapler stapler = new Stapler() {
@Override
public WebApp getWebApp() {
return webapp;
}
};
return stapler;
}
}
}